diff --git a/openmetadata-ui/pom.xml b/openmetadata-ui/pom.xml index 2731d546a49a..bb0539c119cc 100644 --- a/openmetadata-ui/pom.xml +++ b/openmetadata-ui/pom.xml @@ -152,6 +152,7 @@ --max-old-space-size=${node.heap.size} + ${project.version} diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ActivityFeed.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ActivityFeed.spec.ts index 6f35a103ef5d..8bf8897ff0ad 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ActivityFeed.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ActivityFeed.spec.ts @@ -411,7 +411,6 @@ test.describe('Mention notifications in Notification Box', () => { await apiContext.post('/api/v1/feed', { data: { - from: adminUser.responseData.name, message: 'Initial conversation thread for mention test', about: `<#E::table::${entity.entityResponseData.fullyQualifiedName}>`, type: 'Conversation', diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/RTL.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/RTL.spec.ts index 19aff83d36d3..46c36f33e345 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/RTL.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/RTL.spec.ts @@ -20,6 +20,7 @@ import { clickOutside, redirectToHomePage } from '../../utils/common'; import { followEntity, validateFollowedEntityToWidget, + waitForAllLoadersToDisappear, } from '../../utils/entity'; const user = new UserClass(); @@ -52,6 +53,7 @@ test.describe('Verify RTL Layout for landing page', () => { .locator('.ant-dropdown:visible [data-menu-id*="-he-HE"]') .click(); await page.waitForLoadState('domcontentloaded'); + await waitForAllLoadersToDisappear(page); }); test('Verify DataAssets widget functionality', async ({ diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Customproperties-part1.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Customproperties-part1.spec.ts index 6e048e5f4296..b953ece8346a 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Customproperties-part1.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Customproperties-part1.spec.ts @@ -142,7 +142,7 @@ test.describe( await page.locator("pre[role='presentation']").last().click(); await page.keyboard.type( - "SELECT id, name, email\nFROM users\nWHERE active = true\nAND department = 'engineering'\nORDER BY created_at DESC\nLIMIT 100" + "SELECT id, name, email\n\nFROM users\n\nWHERE active = true\n\nAND department = 'engineering'\n\nORDER BY created_at DESC\n\nLIMIT 100" ); const patchResponse = page.waitForResponse( diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Login.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Login.spec.ts index ab15a0d61478..36c2cdd8e7dc 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Login.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Login.spec.ts @@ -15,9 +15,14 @@ import { JWT_EXPIRY_TIME_MAP, LOGIN_ERROR_MESSAGE } from '../../constant/login'; import { AdminClass } from '../../support/user/AdminClass'; import { UserClass } from '../../support/user/UserClass'; import { performAdminLogin } from '../../utils/admin'; -import { clickOutside, redirectToHomePage } from '../../utils/common'; +import { + clickOutside, + getDefaultAdminAPIContext, + redirectToHomePage, + visitOwnProfilePage, +} from '../../utils/common'; +import { waitForAllLoadersToDisappear } from '../../utils/entity'; import { updateJWTTokenExpiryTime } from '../../utils/login'; -import { visitUserProfilePage } from '../../utils/user'; const user = new UserClass(); const CREDENTIALS = user.data; @@ -149,36 +154,56 @@ test.describe('Login flow should work properly', () => { await page.locator('[data-testid="go-back-button"]').click(); }); - test('Refresh should work', async ({ browser }) => { - const browserContext = await browser.newContext(); - const { apiContext, afterAction } = await performAdminLogin(browser); - const page1 = await browserContext.newPage(), - page2 = await browserContext.newPage(); + test('Refresh should work', async ({ page: page1, browser }) => { + test.slow(); + + const { apiContext, afterAction } = await getDefaultAdminAPIContext( + browser + ); + const context = page1.context(); + const page2 = await context.newPage(); const testUser = new UserClass(); await testUser.create(apiContext); + await testUser.setAdminRole(apiContext); await test.step('Login and wait for refresh call is made', async () => { // User login await testUser.login(page1); await redirectToHomePage(page1); + await waitForAllLoadersToDisappear(page1); await redirectToHomePage(page2); + await waitForAllLoadersToDisappear(page2); + await page2.reload(); // eslint-disable-next-line playwright/no-wait-for-timeout -- wait for token refresh timer to fire await page1.waitForTimeout(3 * 60 * 1000); - await redirectToHomePage(page1); + await page1.bringToFront(); + await visitOwnProfilePage(page1); + await waitForAllLoadersToDisappear(page1); + await expect(page1.getByTestId('user-display-name')).toHaveText( + testUser.responseData.displayName ?? testUser.responseData.name + ); - await visitUserProfilePage(page1, testUser.responseData.name); - await redirectToHomePage(page2); - await visitUserProfilePage(page2, testUser.responseData.name); + await page2.bringToFront(); + await page2.evaluate(() => { + document.dispatchEvent( + new Event('visibilitychange', { bubbles: true }) + ); + }); + + await visitOwnProfilePage(page2); + await waitForAllLoadersToDisappear(page2); + await expect(page2.getByTestId('user-display-name')).toHaveText( + testUser.responseData.displayName ?? testUser.responseData.name + ); await page1.close(); await page2.close(); }); - await browserContext.close(); await afterAction(); }); diff --git a/openmetadata-ui/src/main/resources/ui/src/App.test.tsx b/openmetadata-ui/src/main/resources/ui/src/App.test.tsx index 5bf0fbfab636..afeca7c9a0ab 100644 --- a/openmetadata-ui/src/main/resources/ui/src/App.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/App.test.tsx @@ -12,30 +12,58 @@ */ import { render } from '@testing-library/react'; +import React from 'react'; import App from './App'; -import { AuthProvider } from './components/Auth/AuthProviders/AuthProvider'; +import AppRouter from './components/AppRouter/AppRouter'; -jest.mock('./components/AppRouter/AppRouter', () => { - return jest.fn().mockReturnValue(

AppRouter

); -}); +const mockAuthProvider = jest.fn(); -jest.mock('./components/Auth/AuthProviders/AuthProvider', () => { - return { - AuthProvider: jest - .fn() - .mockImplementation(({ children }) => <>{children}), - AuthContext: { - Provider: jest.fn().mockImplementation(({ children }) => <>{children}), - }, - }; -}); +jest.mock('./components/AppRouter/AppRouter', () => ({ + __esModule: true, + default: function AppRouter() { + return React.createElement( + 'div', + { 'data-testid': 'app-router' }, + 'AppRouter' + ); + }, +})); + +jest.mock('./components/Auth/AuthProviders/AuthProvider', () => ({ + AuthProvider: function AuthProvider({ + children, + childComponentType, + }: { + children: React.ReactNode; + childComponentType: React.ComponentType; + }) { + mockAuthProvider({ childComponentType }); + + return React.createElement( + 'div', + { 'data-testid': 'auth-provider' }, + children + ); + }, +})); + +describe('App', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should render AuthProvider wrapping AppRouter', () => { + const { getByTestId } = render(React.createElement(App)); + + expect(getByTestId('auth-provider')).toBeInTheDocument(); + expect(getByTestId('app-router')).toBeInTheDocument(); + }); + + it('should pass AppRouter as childComponentType to AuthProvider', () => { + render(React.createElement(App)); -it('renders learn react link', () => { - const { getAllByTestId } = render( - - - - ); - const linkElement = getAllByTestId(/content-wrapper/i); - linkElement.map((elm) => expect(elm).toBeInTheDocument()); + expect(mockAuthProvider).toHaveBeenCalledWith({ + childComponentType: AppRouter, + }); + }); }); diff --git a/openmetadata-ui/src/main/resources/ui/src/App.tsx b/openmetadata-ui/src/main/resources/ui/src/App.tsx index 5841453ae058..35f6d39c8ce1 100644 --- a/openmetadata-ui/src/main/resources/ui/src/App.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/App.tsx @@ -11,171 +11,15 @@ * limitations under the License. */ -import { isEmpty } from 'lodash'; -import { FC, ReactNode, useEffect, useMemo } from 'react'; -import { RouterProvider } from 'react-aria-components'; -import { HelmetProvider } from 'react-helmet-async'; -import { I18nextProvider } from 'react-i18next'; -import { BrowserRouter, useNavigate } from 'react-router-dom'; -import { useShallow } from 'zustand/react/shallow'; +import { FC } from 'react'; 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 AirflowStatusProvider from './context/AirflowStatusProvider/AirflowStatusProvider'; -import AntDConfigProvider from './context/AntDConfigProvider/AntDConfigProvider'; -import AsyncDeleteProvider from './context/AsyncDeleteProvider/AsyncDeleteProvider'; -import PermissionProvider from './context/PermissionProvider/PermissionProvider'; -import TourProvider from './context/TourProvider/TourProvider'; -import WebSocketProvider from './context/WebSocketProvider/WebSocketProvider'; -import { useApplicationStore } from './hooks/useApplicationStore'; -import { - getCustomUiThemePreference, - getSystemConfig, -} from './rest/settingConfigAPI'; -import { getBasePath } from './utils/HistoryUtils'; - -import GlobalStyles from '@mui/material/GlobalStyles'; -import { ThemeProvider } from '@mui/material/styles'; -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'; -import RuleEnforcementProvider from './context/RuleEnforcementProvider/RuleEnforcementProvider'; -import { ThemeProvider as UntitledUIThemeProvider } from './context/UntitledUIThemeProvider/theme-provider'; -import i18n from './utils/i18next/LocalUtil'; -import { getThemeConfig } from './utils/ThemeUtils'; - -const ReactAriaRouterBridge = ({ children }: { children: ReactNode }) => { - const navigate = useNavigate(); - - return {children}; -}; const App: FC = () => { - const { applicationConfig, setApplicationConfig, setRdfEnabled } = - useApplicationStore( - useShallow((state) => ({ - applicationConfig: state.applicationConfig, - setApplicationConfig: state.setApplicationConfig, - setRdfEnabled: state.setRdfEnabled, - })) - ); - - // Create dynamic MUI theme based on user customizations - const muiTheme = useMemo( - () => createMuiTheme(applicationConfig?.customTheme, DEFAULT_THEME), - [applicationConfig?.customTheme] - ); - - const fetchApplicationConfig = async () => { - try { - const [themeData, systemConfig] = await Promise.all([ - getCustomUiThemePreference(), - getSystemConfig(), - ]); - - setApplicationConfig({ - ...themeData, - customTheme: getThemeConfig(themeData.customTheme), - }); - - // Set RDF enabled state - setRdfEnabled(systemConfig.rdfEnabled || false); - } catch (error) { - // eslint-disable-next-line no-console - console.error(error); - } - }; - - useEffect(() => { - fetchApplicationConfig(); - }, []); - - useEffect(() => { - const faviconHref = isEmpty( - applicationConfig?.customLogoConfig?.customFaviconUrlPath - ) - ? '/favicon.png' - : applicationConfig?.customLogoConfig?.customFaviconUrlPath ?? - '/favicon.png'; - const link = document.querySelectorAll('link[rel~="icon"]'); - - if (!isEmpty(link)) { - link.forEach((item) => { - item.setAttribute('href', faviconHref); - }); - } - }, [applicationConfig]); - return ( -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
+ + + ); }; diff --git a/openmetadata-ui/src/main/resources/ui/src/AppRoot.tsx b/openmetadata-ui/src/main/resources/ui/src/AppRoot.tsx new file mode 100644 index 000000000000..fd614fdd0ad0 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/AppRoot.tsx @@ -0,0 +1,103 @@ +/* + * 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 { isEmpty } from 'lodash'; +import { FC, useEffect } from 'react'; +import { HelmetProvider } from 'react-helmet-async'; +import { I18nextProvider } from 'react-i18next'; +import { BrowserRouter } from 'react-router-dom'; +import { useShallow } from 'zustand/react/shallow'; +import App from './App'; +import ErrorBoundary from './components/common/ErrorBoundary/ErrorBoundary'; +import AntDConfigProvider from './context/AntDConfigProvider/AntDConfigProvider'; +import { useApplicationStore } from './hooks/useApplicationStore'; +import { + getCustomUiThemePreference, + getSystemConfig, +} from './rest/settingConfigAPI'; +import { getBasePath } from './utils/HistoryUtils'; +import i18n from './utils/i18next/LocalUtil'; +import { getThemeConfig } from './utils/ThemeUtils'; + +const AppRoot: FC = () => { + const { initializeAuthState } = useApplicationStore(); + + const { applicationConfig, setApplicationConfig, setRdfEnabled } = + useApplicationStore( + useShallow((state) => ({ + applicationConfig: state.applicationConfig, + setApplicationConfig: state.setApplicationConfig, + setRdfEnabled: state.setRdfEnabled, + })) + ); + + const fetchApplicationConfig = async () => { + try { + const [themeData, systemConfig] = await Promise.all([ + getCustomUiThemePreference(), + getSystemConfig(), + ]); + + setApplicationConfig({ + ...themeData, + customTheme: getThemeConfig(themeData.customTheme), + }); + + setRdfEnabled(systemConfig.rdfEnabled || false); + } catch (error) { + // eslint-disable-next-line no-console + console.error(error); + } + }; + + useEffect(() => { + fetchApplicationConfig(); + initializeAuthState(); + }, []); + + useEffect(() => { + const faviconHref = isEmpty( + applicationConfig?.customLogoConfig?.customFaviconUrlPath + ) + ? '/favicon.png' + : applicationConfig?.customLogoConfig?.customFaviconUrlPath ?? + '/favicon.png'; + const link = document.querySelectorAll('link[rel~="icon"]'); + + if (!isEmpty(link)) { + link.forEach((item) => { + item.setAttribute('href', faviconHref); + }); + } + }, [applicationConfig]); + + return ( +
+
+ + + + + + + + + + + +
+
+ ); +}; + +export default AppRoot; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Alerts/AlertDetails/AlertRecentEventsTab/AlertRecentEventsTab.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Alerts/AlertDetails/AlertRecentEventsTab/AlertRecentEventsTab.tsx index 9cba3f503ef2..db464a02b9fc 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Alerts/AlertDetails/AlertRecentEventsTab/AlertRecentEventsTab.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Alerts/AlertDetails/AlertRecentEventsTab/AlertRecentEventsTab.tsx @@ -43,9 +43,9 @@ import { getChangeEventDataFromTypedEvent, getLabelsForEventDetails, } from '../../../../utils/Alerts/AlertsUtil'; -import { Transi18next } from '../../../../utils/CommonUtils'; import { formatDateTime } from '../../../../utils/date-time/DateTimeUtils'; import { getEntityName } from '../../../../utils/EntityUtils'; +import { Transi18next } from '../../../../utils/i18next/LocalUtil'; import searchClassBase from '../../../../utils/SearchClassBase'; import { showErrorToast } from '../../../../utils/ToastUtils'; import ErrorPlaceHolder from '../../../common/ErrorWithPlaceholder/ErrorPlaceHolder'; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Alerts/DestinationFormItem/DestinationSelectItem/DestinationSelectItem.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Alerts/DestinationFormItem/DestinationSelectItem/DestinationSelectItem.test.tsx index e7cc33e3430c..4d6127c024b9 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Alerts/DestinationFormItem/DestinationSelectItem/DestinationSelectItem.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Alerts/DestinationFormItem/DestinationSelectItem/DestinationSelectItem.test.tsx @@ -59,10 +59,15 @@ jest.mock('antd', () => { }; }); -jest.mock('../../../../utils/CommonUtils', () => ({ +jest.mock('../../../../utils/i18next/LocalUtil', () => ({ Transi18next: jest.fn().mockImplementation(({ i18nKey }) => { return {i18nKey}; }), + __esModule: true, + default: { + t: jest.fn().mockImplementation((key) => key), + }, + t: jest.fn().mockImplementation((key) => key), })); describe('DestinationSelectItem component', () => { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Alerts/DestinationFormItem/DestinationSelectItem/DestinationSelectItem.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Alerts/DestinationFormItem/DestinationSelectItem/DestinationSelectItem.tsx index f98e0fa8fe3a..6d67c03300ba 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Alerts/DestinationFormItem/DestinationSelectItem/DestinationSelectItem.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Alerts/DestinationFormItem/DestinationSelectItem/DestinationSelectItem.tsx @@ -48,7 +48,7 @@ import { getSubscriptionTypeOptions, normalizeDestinationConfig, } from '../../../../utils/Alerts/AlertsUtil'; -import { Transi18next } from '../../../../utils/CommonUtils'; +import { Transi18next } from '../../../../utils/i18next/LocalUtil'; import { checkIfDestinationIsInternal } from '../../../../utils/ObservabilityUtils'; import { DestinationSelectItemProps } from './DestinationSelectItem.interface'; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/AppBar/Suggestions.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/AppBar/Suggestions.test.tsx index a783bf0343ee..c1215220b4d2 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/AppBar/Suggestions.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/AppBar/Suggestions.test.tsx @@ -11,7 +11,6 @@ * limitations under the License. */ import { fireEvent, render, screen } from '@testing-library/react'; -import { useTranslation } from 'react-i18next'; import { useTourProvider } from '../../context/TourProvider/TourProvider'; import { SearchIndex } from '../../enums/search.enum'; import { searchQuery } from '../../rest/searchAPI'; @@ -19,9 +18,6 @@ import Suggestions from './Suggestions'; // Mock dependencies jest.mock('../../rest/searchAPI'); -jest.mock('react-i18next', () => ({ - useTranslation: jest.fn(), -})); jest.mock('../../context/TourProvider/TourProvider'); jest.mock('../../utils/SearchUtils', () => ({ filterOptionsByIndex: jest.fn((options, index) => { @@ -39,12 +35,15 @@ jest.mock('../../utils/SearchUtils', () => ({ jest.mock('../../utils/SearchClassBase', () => ({ getEntitiesSuggestions: jest.fn(() => []), })); -jest.mock('../../utils/CommonUtils', () => ({ - Transi18next: ({ i18nKey, values }: { i18nKey: string; values: any }) => ( - - {i18nKey} {values?.keyword || ''} - - ), +jest.mock('../../utils/i18next/LocalUtil', () => ({ + Transi18next: jest.fn().mockImplementation(({ i18nKey }) => { + return {i18nKey}; + }), + __esModule: true, + default: { + t: jest.fn().mockImplementation((key) => key), + }, + t: jest.fn().mockImplementation((key) => key), })); // Mock location.search for the component @@ -56,7 +55,6 @@ Object.defineProperty(window, 'location', { }); const mockSearchQuery = searchQuery as jest.Mock; -const mockUseTranslation = useTranslation as jest.Mock; const mockUseTourProvider = useTourProvider as jest.Mock; const defaultProps = { @@ -71,15 +69,11 @@ const defaultProps = { describe('Suggestions Component', () => { beforeEach(() => { jest.clearAllMocks(); - mockUseTranslation.mockReturnValue({ - t: jest.fn((key: string) => key), - i18n: { language: 'en' }, - } as any); mockUseTourProvider.mockReturnValue({ isTourOpen: false, updateTourPage: jest.fn(), updateTourSearch: jest.fn(), - } as any); + }); }); describe('AI Query Suggestions', () => { @@ -139,7 +133,9 @@ describe('Suggestions Component', () => { render(); // The component should show the no results message - expect(screen.getByTestId('transi18next')).toBeInTheDocument(); + expect( + screen.getByText('message.please-enter-to-find-data-assets') + ).toBeInTheDocument(); }); it('should not call searchQuery when tour is open', () => { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/AppBar/Suggestions.tsx b/openmetadata-ui/src/main/resources/ui/src/components/AppBar/Suggestions.tsx index 345e182d883e..ba153c33a4b5 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/AppBar/Suggestions.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/AppBar/Suggestions.tsx @@ -27,7 +27,7 @@ import { import { useTourProvider } from '../../context/TourProvider/TourProvider'; import { SearchIndex } from '../../enums/search.enum'; import { searchQuery } from '../../rest/searchAPI'; -import { Transi18next } from '../../utils/CommonUtils'; +import { Transi18next } from '../../utils/i18next/LocalUtil'; import searchClassBase from '../../utils/SearchClassBase'; import { filterOptionsByIndex, diff --git a/openmetadata-ui/src/main/resources/ui/src/components/AppContainer/AppContainer.tsx b/openmetadata-ui/src/main/resources/ui/src/components/AppContainer/AppContainer.tsx index a349aa562504..3b77aefeed40 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/AppContainer/AppContainer.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/AppContainer/AppContainer.tsx @@ -12,17 +12,19 @@ */ import { Layout } from 'antd'; import classNames from 'classnames'; +import { isNil } from 'lodash'; import { useCallback, useEffect } from 'react'; +import { useAnalytics } from 'use-analytics'; import { useLimitStore } from '../../context/LimitsProvider/useLimitsStore'; +import { CustomEventTypes } from '../../generated/analytics/webAnalyticEventData'; import { LineageSettings } from '../../generated/configuration/lineageSettings'; import { SettingType } from '../../generated/settings/settings'; -import { useCurrentUserPreferences } from '../../hooks/currentUserStore/useCurrentUserStore'; import { useApplicationStore } from '../../hooks/useApplicationStore'; +import useCustomLocation from '../../hooks/useCustomLocation/useCustomLocation'; import { useLineageStore } from '../../hooks/useLineageStore'; import { getLimitConfig } from '../../rest/limitsAPI'; import { getSettingsByType } from '../../rest/settingConfigAPI'; import applicationRoutesClass from '../../utils/ApplicationRoutesClassBase'; -import i18n from '../../utils/i18next/LocalUtil'; import { isNewLayoutRoute } from '../../utils/LayoutUtils'; import AppSidebar from '../AppSidebar/AppSidebar.component'; import { LimitBanner } from '../common/LimitBanner/LimitBanner'; @@ -33,11 +35,10 @@ import './app-container.less'; const { Content } = Layout; const AppContainer = () => { + const location = useCustomLocation(); + const analytics = useAnalytics(); const { currentUser, setAppPreferences, appPreferences } = useApplicationStore(); - const { - preferences: { language }, - } = useCurrentUserPreferences(); const AuthenticatedRouter = applicationRoutesClass.getRouteElements(); const ApplicationExtras = applicationsClassBase.getApplicationExtension(); const { isAuthenticated } = useApplicationStore(); @@ -87,10 +88,28 @@ const AppContainer = () => { }, [currentUser?.id]); useEffect(() => { - if (language) { - i18n.changeLanguage(language); + const { pathname } = location; + + if (pathname !== '/' && !isNil(analytics)) { + analytics.page(); } - }, [language]); + }, [location.pathname, analytics]); + + useEffect(() => { + const handleClickEvent = (event: MouseEvent) => { + const eventValue = + (event.target as HTMLElement)?.textContent || CustomEventTypes.Click; + + if (eventValue && !isNil(analytics)) { + analytics.track(eventValue); + } + }; + + const targetNode = document.body; + targetNode.addEventListener('click', handleClickEvent); + + return () => targetNode.removeEventListener('click', handleClickEvent); + }, [analytics]); return ( diff --git a/openmetadata-ui/src/main/resources/ui/src/components/AppRouter/AppRouter.tsx b/openmetadata-ui/src/main/resources/ui/src/components/AppRouter/AppRouter.tsx index 135c5548b8e7..8fc8921459a3 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/AppRouter/AppRouter.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/AppRouter/AppRouter.tsx @@ -11,32 +11,57 @@ * limitations under the License. */ -import { isEmpty, isNil } from 'lodash'; -import { useCallback, useEffect } from 'react'; +import { isEmpty } from 'lodash'; +import { lazy } from 'react'; import { Navigate, Route, Routes } from 'react-router-dom'; -import { useAnalytics } from 'use-analytics'; import { useShallow } from 'zustand/react/shallow'; -import { ROUTES } from '../../constants/constants'; -import { CustomEventTypes } from '../../generated/analytics/webAnalyticEventData'; +import { APP_ROUTER_ROUTES } from '../../constants/router.constants'; import { useApplicationStore } from '../../hooks/useApplicationStore'; -import useCustomLocation from '../../hooks/useCustomLocation/useCustomLocation'; -import AccessNotAllowedPage from '../../pages/AccessNotAllowedPage/AccessNotAllowedPage'; -import { LogoutPage } from '../../pages/LogoutPage/LogoutPage'; -import PageNotFound from '../../pages/PageNotFound/PageNotFound'; -import SamlCallback from '../../pages/SamlCallback'; -import SignUpPage from '../../pages/SignUp/SignUpPage'; import applicationRoutesClass from '../../utils/ApplicationRoutesClassBase'; -import AppContainer from '../AppContainer/AppContainer'; import Loader from '../common/Loader/Loader'; -import { useApplicationsProvider } from '../Settings/Applications/ApplicationsProvider/ApplicationsProvider'; -import { RoutePosition } from '../Settings/Applications/plugins/AppPlugin'; +import withSuspenseFallback from './withSuspenseFallback'; + +const AuthenticatedApp = withSuspenseFallback( + lazy(() => import('./AuthenticatedApp')) +); + +const AuthenticatedRoutes = withSuspenseFallback( + lazy(() => + import('./AuthenticatedRoutes').then((m) => ({ + default: m.AuthenticatedRoutes, + })) + ) +); + +// Lazy-load infrequently-visited unauthenticated pages +const AccessNotAllowedPage = withSuspenseFallback( + lazy(() => import('../../pages/AccessNotAllowedPage/AccessNotAllowedPage')) +); + +const LogoutPage = withSuspenseFallback( + lazy(() => + import('../../pages/LogoutPage/LogoutPage').then((m) => ({ + default: m.LogoutPage, + })) + ) +); + +const PageNotFound = withSuspenseFallback( + lazy(() => import('../../pages/PageNotFound/PageNotFound')) +); + +const SamlCallback = withSuspenseFallback( + lazy(() => import('../../pages/SamlCallback')) +); + +const SignUpPage = withSuspenseFallback( + lazy(() => import('../../pages/SignUp/SignUpPage')) +); const AppRouter = () => { - const location = useCustomLocation(); const UnAuthenticatedAppRouter = applicationRoutesClass.getUnAuthenticatedRouteElements(); - const analytics = useAnalytics(); const { currentUser, isAuthenticated, @@ -50,43 +75,6 @@ const AppRouter = () => { isAuthenticating: state.isAuthenticating, })) ); - const { plugins = [] } = useApplicationsProvider(); - - useEffect(() => { - const { pathname } = location; - - /** - * Ignore the slash path because we are treating my data as - * default path. - * And check if analytics instance is available - */ - if (pathname !== '/' && !isNil(analytics)) { - // track page view on route change - analytics.page(); - } - }, [location.pathname]); - - const handleClickEvent = useCallback( - (event: MouseEvent) => { - const eventValue = - (event.target as HTMLElement)?.textContent || CustomEventTypes.Click; - /** - * Ignore the click event if the event value is undefined - * And analytics instance is not available - */ - if (eventValue && !isNil(analytics)) { - analytics.track(eventValue); - } - }, - [analytics] - ); - - useEffect(() => { - const targetNode = document.body; - targetNode.addEventListener('click', handleClickEvent); - - return () => targetNode.removeEventListener('click', handleClickEvent); - }, [handleClickEvent]); /** * isApplicationLoading is true when the application is loading in AuthProvider @@ -100,46 +88,37 @@ const AppRouter = () => { return ; } + if (isAuthenticated) { + return ( + + + + ); + } + return ( - } path={ROUTES.NOT_FOUND} /> - } path={ROUTES.LOGOUT} /> - } path={ROUTES.UNAUTHORISED} /> + } path={APP_ROUTER_ROUTES.NOT_FOUND} /> + } path={APP_ROUTER_ROUTES.LOGOUT} /> + } + path={APP_ROUTER_ROUTES.UNAUTHORISED} + /> ) : ( - + ) } - path={ROUTES.SIGNUP} + path={APP_ROUTER_ROUTES.SIGNUP} /> - {/* When authenticating from an SSO provider page (e.g., SAML Apps), if the user is already logged in, - * the callbacks should be available. This ensures consistent behavior across different authentication scenarios. - */} - } path={ROUTES.AUTH_CALLBACK} /> - - {/* Render APP position plugin routes (they handle their own layouts) */} - {isAuthenticated && - plugins?.flatMap((plugin) => { - const routes = plugin.getRoutes?.() || []; - // Filter routes with APP position - const appRoutes = routes.filter( - (route) => route.position === RoutePosition.APP - ); - - return appRoutes.map((route, idx) => ( - - )); - })} - - {/* Default authenticated and unauthenticated routes */} - {isAuthenticated ? ( - } path="*" /> - ) : ( - } path="*" /> - )} + } + path={APP_ROUTER_ROUTES.AUTH_CALLBACK} + /> + } path="*" /> ); }; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/AppRouter/AuthenticatedApp.tsx b/openmetadata-ui/src/main/resources/ui/src/components/AppRouter/AuthenticatedApp.tsx new file mode 100644 index 000000000000..e83f2abf2cb9 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/AppRouter/AuthenticatedApp.tsx @@ -0,0 +1,109 @@ +/* + * Copyright 2022 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 { GlobalStyles, ThemeProvider } from '@mui/material'; +import { + createMuiTheme, + SnackbarContent, +} from '@openmetadata/ui-core-components'; +import { SnackbarProvider } from 'notistack'; +import { FC, ReactNode, useMemo } from 'react'; +import { RouterProvider } from 'react-aria-components'; +import { DndProvider } from 'react-dnd'; +import { HTML5Backend } from 'react-dnd-html5-backend'; +import { useNavigate } from 'react-router-dom'; +import { useShallow } from 'zustand/react/shallow'; +import { DEFAULT_THEME } from '../../constants/Appearance.constants'; +import AirflowStatusProvider from '../../context/AirflowStatusProvider/AirflowStatusProvider'; +import AsyncDeleteProvider from '../../context/AsyncDeleteProvider/AsyncDeleteProvider'; +import PermissionProvider from '../../context/PermissionProvider/PermissionProvider'; +import RuleEnforcementProvider from '../../context/RuleEnforcementProvider/RuleEnforcementProvider'; +import TourProvider from '../../context/TourProvider/TourProvider'; +import WebSocketProvider from '../../context/WebSocketProvider/WebSocketProvider'; +import { useApplicationStore } from '../../hooks/useApplicationStore'; +import { EntityExportModalProvider } from '../Entity/EntityExportModalProvider/EntityExportModalProvider.component'; +import ApplicationsProvider from '../Settings/Applications/ApplicationsProvider/ApplicationsProvider'; +import WebAnalyticsProvider from '../WebAnalytics/WebAnalyticsProvider'; +import { ThemeProvider as UntitledUIThemeProvider } from './../../context/UntitledUIThemeProvider/theme-provider'; + +const ReactAriaRouterBridge = ({ children }: { children: ReactNode }) => { + const navigate = useNavigate(); + + return {children}; +}; + +interface AuthenticatedAppProps { + children: React.ReactNode; +} + +const AuthenticatedApp: FC = ({ children }) => { + const { applicationConfig } = useApplicationStore( + useShallow((state) => ({ + applicationConfig: state.applicationConfig, + })) + ); + + const muiTheme = useMemo( + () => createMuiTheme(applicationConfig?.customTheme, DEFAULT_THEME), + [applicationConfig?.customTheme] + ); + + return ( + + + + + + + + + + + + + + + + + {children} + + + + + + + + + + + + + + + ); +}; + +export default AuthenticatedApp; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/AppRouter/AuthenticatedAppRouter.tsx b/openmetadata-ui/src/main/resources/ui/src/components/AppRouter/AuthenticatedAppRouter.tsx index 5b770729b22e..1063967aeb34 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/AppRouter/AuthenticatedAppRouter.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/AppRouter/AuthenticatedAppRouter.tsx @@ -21,65 +21,64 @@ import { import { usePermissionProvider } from '../../context/PermissionProvider/PermissionProvider'; import { ResourceEntity } from '../../context/PermissionProvider/PermissionProvider.interface'; import { Operation } from '../../generated/entity/policies/policy'; -import AddCustomMetricPage from '../../pages/AddCustomMetricPage/AddCustomMetricPage'; -import { CustomizablePage } from '../../pages/CustomizablePage/CustomizablePage'; -import DataQualityPage from '../../pages/DataQuality/DataQualityPage'; -import ForbiddenPage from '../../pages/ForbiddenPage/ForbiddenPage'; -import PlatformLineage from '../../pages/PlatformLineage/PlatformLineage'; -import TagPage from '../../pages/TagPage/TagPage'; import { checkPermission, userPermissions } from '../../utils/PermissionsUtils'; import { useApplicationsProvider } from '../Settings/Applications/ApplicationsProvider/ApplicationsProvider'; import { RoutePosition } from '../Settings/Applications/plugins/AppPlugin'; import AdminProtectedRoute from './AdminProtectedRoute'; import withSuspenseFallback from './withSuspenseFallback'; -const DomainRouter = withSuspenseFallback( +// Previously statically imported — lazify so they stay out of the main chunk +const AddCustomMetricPage = withSuspenseFallback( React.lazy( - () => import(/* webpackChunkName: "DomainRouter" */ './DomainRouter') + () => import('../../pages/AddCustomMetricPage/AddCustomMetricPage') ) ); -const DataProductListPage = withSuspenseFallback( - React.lazy( - () => - import( - /* webpackChunkName: "DataProductListPage" */ '../DataProduct/DataProductListPage' - ) + +const CustomizablePage = withSuspenseFallback( + React.lazy(() => + import('../../pages/CustomizablePage/CustomizablePage').then((m) => ({ + default: m.CustomizablePage, + })) ) ); + +const DataQualityPage = withSuspenseFallback( + React.lazy(() => import('../../pages/DataQuality/DataQualityPage')) +); + +const ForbiddenPage = withSuspenseFallback( + React.lazy(() => import('../../pages/ForbiddenPage/ForbiddenPage')) +); + +const PlatformLineage = withSuspenseFallback( + React.lazy(() => import('../../pages/PlatformLineage/PlatformLineage')) +); + +const TagPage = withSuspenseFallback( + React.lazy(() => import('../../pages/TagPage/TagPage')) +); + +const DomainRouter = withSuspenseFallback( + React.lazy(() => import('./DomainRouter')) +); +const DataProductListPage = withSuspenseFallback( + React.lazy(() => import('../DataProduct/DataProductListPage')) +); const SettingsRouter = withSuspenseFallback( - React.lazy( - () => import(/* webpackChunkName: "SettingsRouter" */ './SettingsRouter') - ) + React.lazy(() => import('./SettingsRouter')) ); const EntityRouter = withSuspenseFallback( - React.lazy( - () => import(/* webpackChunkName: "EntityRouter" */ './EntityRouter') - ) + React.lazy(() => import('./EntityRouter')) ); const ClassificationRouter = withSuspenseFallback( - React.lazy( - () => - import( - /* webpackChunkName: "ClassificationRouter" */ './ClassificationRouter' - ) - ) + React.lazy(() => import('./ClassificationRouter')) ); const GlossaryRouter = withSuspenseFallback( - React.lazy( - () => - import( - /* webpackChunkName: "GlossaryRouter" */ './GlossaryRouter/GlossaryRouter' - ) - ) + React.lazy(() => import('./GlossaryRouter/GlossaryRouter')) ); const GlossaryTermRouter = withSuspenseFallback( - React.lazy( - () => - import( - /* webpackChunkName: "GlossaryTermRouter" */ './GlossaryTermRouter/GlossaryTermRouter' - ) - ) + React.lazy(() => import('./GlossaryTermRouter/GlossaryTermRouter')) ); const MyDataPage = withSuspenseFallback( diff --git a/openmetadata-ui/src/main/resources/ui/src/components/AppRouter/AuthenticatedRoutes.tsx b/openmetadata-ui/src/main/resources/ui/src/components/AppRouter/AuthenticatedRoutes.tsx new file mode 100644 index 000000000000..748c4ca2e108 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/AppRouter/AuthenticatedRoutes.tsx @@ -0,0 +1,76 @@ +/* + * Copyright 2022 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 { isEmpty } from 'lodash'; +import { Navigate, Route, Routes } from 'react-router-dom'; +import { useShallow } from 'zustand/react/shallow'; +import { APP_ROUTER_ROUTES } from '../../constants/router.constants'; +import { useApplicationStore } from '../../hooks/useApplicationStore'; +import AccessNotAllowedPage from '../../pages/AccessNotAllowedPage/AccessNotAllowedPage'; +import { LogoutPage } from '../../pages/LogoutPage/LogoutPage'; +import PageNotFound from '../../pages/PageNotFound/PageNotFound'; +import SamlCallback from '../../pages/SamlCallback'; +import SignUpPage from '../../pages/SignUp/SignUpPage'; +import AppContainer from '../AppContainer/AppContainer'; +import { useApplicationsProvider } from '../Settings/Applications/ApplicationsProvider/ApplicationsProvider'; +import { RoutePosition } from '../Settings/Applications/plugins/AppPlugin'; + +export const AuthenticatedRoutes = () => { + const { currentUser } = useApplicationStore( + useShallow((state) => ({ + currentUser: state.currentUser, + })) + ); + + const { plugins = [] } = useApplicationsProvider() ?? {}; + + return ( + + } path={APP_ROUTER_ROUTES.NOT_FOUND} /> + } path={APP_ROUTER_ROUTES.LOGOUT} /> + } + path={APP_ROUTER_ROUTES.UNAUTHORISED} + /> + + ) : ( + + ) + } + path={APP_ROUTER_ROUTES.SIGNUP} + /> + } + path={APP_ROUTER_ROUTES.AUTH_CALLBACK} + /> + + {/* Render APP position plugin routes (they handle their own layouts) */} + {plugins?.flatMap((plugin) => { + const routes = plugin.getRoutes?.() || []; + // Filter routes with APP position + const appRoutes = routes.filter( + (route) => route.position === RoutePosition.APP + ); + + return appRoutes.map((route, idx) => ( + + )); + })} + + } path="*" /> + + ); +}; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/AppRouter/UnAuthenticatedAppRouter.tsx b/openmetadata-ui/src/main/resources/ui/src/components/AppRouter/UnAuthenticatedAppRouter.tsx index 3ab9fd01af6d..1dc50b4ff6d1 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/AppRouter/UnAuthenticatedAppRouter.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/AppRouter/UnAuthenticatedAppRouter.tsx @@ -10,18 +10,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { LoginCallback } from '@okta/okta-react'; + import { lazy, useMemo } from 'react'; import { Navigate, Route, Routes } from 'react-router-dom'; import { useShallow } from 'zustand/react/shallow'; -import { ROUTES } from '../../constants/constants'; +import { APP_ROUTER_ROUTES } from '../../constants/router.constants'; import { AuthProvider } from '../../generated/configuration/authenticationConfiguration'; import { useApplicationStore } from '../../hooks/useApplicationStore'; import useCustomLocation from '../../hooks/useCustomLocation/useCustomLocation'; -import PageNotFound from '../../pages/PageNotFound/PageNotFound'; -import AccountActivationConfirmation from '../../pages/SignUp/account-activation-confirmation.component'; import applicationRoutesClass from '../../utils/ApplicationRoutesClassBase'; -import Auth0Callback from '../Auth/AppCallbacks/Auth0Callback/Auth0Callback'; import withSuspenseFallback from './withSuspenseFallback'; const SigninPage = withSuspenseFallback( @@ -40,6 +37,26 @@ const BasicSignupPage = withSuspenseFallback( lazy(() => import('../../pages/SignUp/BasicSignup.component')) ); +const PageNotFound = withSuspenseFallback( + lazy(() => import('../../pages/PageNotFound/PageNotFound')) +); + +const AccountActivationConfirmation = withSuspenseFallback( + lazy( + () => import('../../pages/SignUp/account-activation-confirmation.component') + ) +); + +const Auth0Callback = withSuspenseFallback( + lazy(() => import('../Auth/AppCallbacks/Auth0Callback/Auth0Callback')) +); + +const LoginCallback = withSuspenseFallback( + lazy(() => + import('@okta/okta-react').then((m) => ({ default: m.LoginCallback })) + ) +); + export const UnAuthenticatedAppRouter = () => { const location = useCustomLocation(); const { authConfig, isSigningUp } = useApplicationStore( @@ -69,31 +86,43 @@ export const UnAuthenticatedAppRouter = () => { }, [authConfig?.provider]); if (applicationRoutesClass.isProtectedRoute(location.pathname)) { - return ; + return ; } return ( - } path={ROUTES.SIGNIN} /> + } path={APP_ROUTER_ROUTES.SIGNIN} /> {CallbackComponent && ( - } path={ROUTES.CALLBACK} /> + } + path={APP_ROUTER_ROUTES.CALLBACK} + /> )} {!isSigningUp && ( } - path={ROUTES.HOME} + element={} + path={APP_ROUTER_ROUTES.HOME} /> )} {/* keep this route before any conditional JSX.Element rendering */} - } path={ROUTES.NOT_FOUND} /> + } path={APP_ROUTER_ROUTES.NOT_FOUND} /> {isBasicAuthProvider && ( <> - } path={ROUTES.REGISTER} /> - } path={ROUTES.FORGOT_PASSWORD} /> - } path={ROUTES.RESET_PASSWORD} /> + } + path={APP_ROUTER_ROUTES.REGISTER} + /> + } + path={APP_ROUTER_ROUTES.FORGOT_PASSWORD} + /> + } + path={APP_ROUTER_ROUTES.RESET_PASSWORD} + /> } - path={ROUTES.ACCOUNT_ACTIVATION} + path={APP_ROUTER_ROUTES.ACCOUNT_ACTIVATION} /> )} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Auth/AppAuthenticators/LazyAuthenticators.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Auth/AppAuthenticators/LazyAuthenticators.tsx new file mode 100644 index 000000000000..0fa6a7a5d1d9 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/Auth/AppAuthenticators/LazyAuthenticators.tsx @@ -0,0 +1,98 @@ +/* + * 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 type { WebStorageStateStore } from 'oidc-client'; +import { ComponentType, forwardRef, lazy, ReactNode, Suspense } from 'react'; +import Loader from '../../common/Loader/Loader'; +import { AuthenticatorRef } from '../AuthProviders/AuthProvider.interface'; + +const Auth0Authenticator = lazy(() => import('./Auth0Authenticator')); +const BasicAuthAuthenticator = lazy(() => import('./BasicAuthAuthenticator')); +const MsalAuthenticator = lazy(() => import('./MsalAuthenticator')); +const OidcAuthenticator = lazy(() => import('./OidcAuthenticator')); +const OktaAuthenticator = lazy(() => import('./OktaAuthenticator')); +const GenericAuthenticator = lazy(() => + import('./GenericAuthenticator').then((m) => ({ + default: m.GenericAuthenticator, + })) +); + +export const LazyAuth0Authenticator = forwardRef< + AuthenticatorRef, + { children: ReactNode } +>((props, ref) => ( + }> + + +)); + +LazyAuth0Authenticator.displayName = 'LazyAuth0Authenticator'; + +export const LazyBasicAuthAuthenticator = forwardRef< + AuthenticatorRef, + { children: ReactNode } +>((props, ref) => ( + }> + + +)); + +LazyBasicAuthAuthenticator.displayName = 'LazyBasicAuthAuthenticator'; + +export const LazyMsalAuthenticator = forwardRef< + AuthenticatorRef, + { children: ReactNode } +>((props, ref) => ( + }> + + +)); + +LazyMsalAuthenticator.displayName = 'LazyMsalAuthenticator'; + +export const LazyOidcAuthenticator = forwardRef< + AuthenticatorRef, + { + children: ReactNode; + childComponentType: ComponentType; + userConfig: Record; + } +>((props, ref) => ( + }> + + +)); + +LazyOidcAuthenticator.displayName = 'LazyOidcAuthenticator'; + +export const LazyOktaAuthenticator = forwardRef< + AuthenticatorRef, + { children: ReactNode } +>((props, ref) => ( + }> + + +)); + +LazyOktaAuthenticator.displayName = 'LazyOktaAuthenticator'; + +export const LazyGenericAuthenticator = forwardRef< + AuthenticatorRef, + { children: ReactNode } +>((props, ref) => ( + }> + + +)); + +LazyGenericAuthenticator.displayName = 'LazyGenericAuthenticator'; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Auth/AppAuthenticators/MsalAuthenticator.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Auth/AppAuthenticators/MsalAuthenticator.test.tsx index f6f3419ae9e8..490712bfe451 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Auth/AppAuthenticators/MsalAuthenticator.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Auth/AppAuthenticators/MsalAuthenticator.test.tsx @@ -15,7 +15,8 @@ import { InteractionStatus, } from '@azure/msal-browser'; import { useMsal } from '@azure/msal-react'; -import { act, render, screen } from '@testing-library/react'; +import { render, screen } from '@testing-library/react'; +import { act } from 'react'; import { msalLoginRequest } from '../../../utils/AuthProvider.util'; import { AuthenticatorRef } from '../AuthProviders/AuthProvider.interface'; import MsalAuthenticator from './MsalAuthenticator'; @@ -76,11 +77,11 @@ describe('MsalAuthenticator', () => { it('should handle login in iframe using popup', async () => { // Mock window.self !== window.top for iframe detection - Object.defineProperty(window, 'self', { + Object.defineProperty(globalThis, 'self', { value: { location: {} }, writable: true, }); - Object.defineProperty(window, 'top', { + Object.defineProperty(globalThis, 'top', { value: { location: {} }, writable: true, }); @@ -106,12 +107,12 @@ describe('MsalAuthenticator', () => { it('should handle login in normal window using redirect', async () => { // Mock window.self === window.top for normal window detection - Object.defineProperty(window, 'self', { - value: window, + Object.defineProperty(globalThis, 'self', { + value: globalThis, writable: true, }); - Object.defineProperty(window, 'top', { - value: window, + Object.defineProperty(globalThis, 'top', { + value: globalThis, writable: true, }); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Auth/AuthProviders/AuthProvider.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Auth/AuthProviders/AuthProvider.test.tsx index 9d998a62a804..becce8ebb152 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Auth/AuthProviders/AuthProvider.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Auth/AuthProviders/AuthProvider.test.tsx @@ -25,7 +25,7 @@ const localStorageMock = { clear: jest.fn(), }; -Object.defineProperty(window, 'localStorage', { +Object.defineProperty(globalThis, 'localStorage', { value: localStorageMock, }); @@ -100,6 +100,7 @@ jest.mock('../../../hooks/useApplicationStore', () => ({ isApplicationLoading: false, setApplicationLoading: jest.fn(), initializeAuthState: jest.fn(), + isAuthenticating: false, authConfig: { provider: AuthProviderProps.Basic, providerName: 'Basic', @@ -131,7 +132,7 @@ describe('Test auth provider', () => { ); - const logoutButton = screen.getByTestId('logout-button'); + const logoutButton = await screen.findByTestId('logout-button'); expect(logoutButton).toBeInTheDocument(); }); @@ -151,7 +152,7 @@ describe('Test auth provider', () => { ); - const logoutButton = screen.getByTestId('logout-button'); + const logoutButton = await screen.findByTestId('logout-button'); expect(logoutButton).toBeInTheDocument(); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Auth/AuthProviders/AuthProvider.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Auth/AuthProviders/AuthProvider.tsx index c517cfec6e94..74ecf1275491 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Auth/AuthProviders/AuthProvider.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Auth/AuthProviders/AuthProvider.tsx @@ -12,13 +12,10 @@ */ import { removeSession } from '@analytics/session-utils'; -import { Auth0Provider } from '@auth0/auth0-react'; -import { +import type { Configuration, IPublicClientApplication, - PublicClientApplication, } from '@azure/msal-browser'; -import { MsalProvider } from '@azure/msal-react'; import { AxiosError, AxiosRequestHeaders, @@ -41,7 +38,10 @@ import { import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; import { UN_AUTHORIZED_EXCLUDED_PATHS } from '../../../constants/Auth.constants'; -import { REDIRECT_PATHNAME, ROUTES } from '../../../constants/constants'; +import { + APP_ROUTER_ROUTES as ROUTES, + REDIRECT_PATHNAME, +} from '../../../constants/router.constants'; import { ClientErrors } from '../../../enums/Axios.enum'; import { TabSpecificField } from '../../../enums/entity.enum'; import { @@ -50,6 +50,7 @@ import { } from '../../../generated/configuration/authenticationConfiguration'; import { User } from '../../../generated/entity/teams/user'; import { AuthProvider as AuthProviderEnum } from '../../../generated/settings/settings'; +import { withDomainFilter } from '../../../hoc/withDomainFilter'; import { useApplicationStore } from '../../../hooks/useApplicationStore'; import useCustomLocation from '../../../hooks/useCustomLocation/useCustomLocation'; import axiosClient from '../../../rest'; @@ -68,7 +69,6 @@ import { prepareUserProfileFromClaims, validateAuthFields, } from '../../../utils/AuthProvider.util'; -import { withDomainFilter } from '../../../utils/DomainUtils'; import { clearOidcToken, getOidcToken, @@ -78,15 +78,21 @@ import { showErrorToast, showInfoToast } from '../../../utils/ToastUtils'; import { checkIfUpdateRequired } from '../../../utils/UserDataUtils'; import { resetWebAnalyticSession } from '../../../utils/WebAnalyticsUtils'; import Loader from '../../common/Loader/Loader'; -import Auth0Authenticator from '../AppAuthenticators/Auth0Authenticator'; -import BasicAuthAuthenticator from '../AppAuthenticators/BasicAuthAuthenticator'; -import { GenericAuthenticator } from '../AppAuthenticators/GenericAuthenticator'; -import MsalAuthenticator from '../AppAuthenticators/MsalAuthenticator'; -import OidcAuthenticator from '../AppAuthenticators/OidcAuthenticator'; -import OktaAuthenticator from '../AppAuthenticators/OktaAuthenticator'; +import { + LazyAuth0Authenticator, + LazyBasicAuthAuthenticator, + LazyGenericAuthenticator, + LazyMsalAuthenticator, + LazyOidcAuthenticator, + LazyOktaAuthenticator, +} from '../AppAuthenticators/LazyAuthenticators'; import { AuthenticatorRef, OidcUser } from './AuthProvider.interface'; -import BasicAuthProvider from './BasicAuthProvider'; -import OktaAuthProvider from './OktaAuthProvider'; +import { + LazyAuth0ProviderWrapper, + LazyBasicAuthProviderWrapper, + LazyMsalProviderWrapper, + LazyOktaAuthProviderWrapper, +} from './LazyAuthProviderWrappers'; interface AuthProviderProps { childComponentType: ComponentType; @@ -109,7 +115,11 @@ const isEmailVerifyField = 'isEmailVerified'; let requestInterceptor: number | null = null; let responseInterceptor: number | null = null; -let pendingRequests: any[] = []; +let pendingRequests: { + resolve: (value?: unknown) => void; + reject: (reason?: unknown) => void; + config: InternalAxiosRequestConfig; +}[] = []; type AuthContextType = { onLoginHandler: () => void; @@ -142,7 +152,6 @@ export const AuthProvider = ({ isApplicationLoading, setApplicationLoading, isAuthenticating, - initializeAuthState, } = useApplicationStore(); const tokenService = useRef(TokenService.getInstance()); @@ -323,10 +332,6 @@ export const AuthProvider = ({ } }; - useEffect(() => { - initializeAuthState(); - }, []); - useEffect(() => { if (authenticatorRef.current?.renewIdToken) { tokenService.current.updateRenewToken( @@ -424,15 +429,15 @@ export const AuthProvider = ({ } catch (error) { const err = error as AxiosError; if (err?.response?.status === 404) { - if (!authConfig?.enableSelfSignup) { - resetUserDetails(); - navigate(ROUTES.UNAUTHORISED); - showErrorToast(err); - } else { + if (authConfig?.enableSelfSignup) { setNewUserProfile(user.profile); setCurrentUser({} as User); setIsSigningUp(true); navigate(ROUTES.SIGNUP); + } else { + resetUserDetails(); + navigate(ROUTES.UNAUTHORISED); + showErrorToast(err); } } else { // eslint-disable-next-line no-console @@ -472,20 +477,17 @@ export const AuthProvider = ({ configJson: AuthenticationConfiguration ) => { const { provider, ...otherConfigs } = configJson; - switch (provider) { - case AuthProviderEnum.Azure: - { - const instance = new PublicClientApplication( - otherConfigs as unknown as Configuration - ); - - // Need to initialize the instance before setting it - await instance.initialize(); + if (provider === AuthProviderEnum.Azure) { + const AzureBrowser = await import('@azure/msal-browser'); + const { PublicClientApplication } = AzureBrowser; + const instance = new PublicClientApplication( + otherConfigs as unknown as Configuration + ); - setMsalInstance(instance); - } + // Need to initialize the instance before setting it + await instance.initialize(); - break; + setMsalInstance(instance); } }; @@ -504,7 +506,7 @@ export const AuthProvider = ({ } requestInterceptor = axiosClient.interceptors.request.use(async function ( - config: InternalAxiosRequestConfig + config: InternalAxiosRequestConfig ) { // Need to read token from local storage as it might have been updated with refresh const token: string = await getOidcToken(); @@ -544,7 +546,16 @@ export const AuthProvider = ({ handleStoreProtectedRedirectPath(); // If 401 error and refresh is not in progress, trigger the refresh - if (!tokenService.current?.isTokenUpdateInProgress()) { + if (tokenService.current?.isTokenUpdateInProgress()) { + // If refresh is in progress, queue the request + return new Promise((resolve, reject) => { + pendingRequests.push({ + resolve, + reject, + config: error.config, + }); + }); + } else { // Start the refresh process return new Promise((resolve, reject) => { // Add this request to the pending queue @@ -577,15 +588,6 @@ export const AuthProvider = ({ return Promise.reject(error); }); }); - } else { - // If refresh is in progress, queue the request - return new Promise((resolve, reject) => { - pendingRequests.push({ - resolve, - reject, - config: error.config, - }); - }); } } } @@ -666,64 +668,64 @@ export const AuthProvider = ({ authConfig?.provider === AuthProviderEnum.Saml ) { return ( - + {childElement} - + ); } switch (authConfig?.provider) { case AuthProviderEnum.LDAP: case AuthProviderEnum.Basic: { return ( - - + + {childElement} - - + + ); } case AuthProviderEnum.Auth0: { return ( - - + redirectUri={authConfig.callbackUrl?.toString() ?? ''}> + {childElement} - - + + ); } case AuthProviderEnum.Okta: { return ( - - + + {childElement} - - + + ); } case AuthProviderEnum.Google: case AuthProviderEnum.CustomOidc: case AuthProviderEnum.AwsCognito: { return ( - {childElement} - + ); } case AuthProviderEnum.Azure: { return msalInstance ? ( - - + + {childElement} - - + + ) : ( ); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Auth/AuthProviders/BasicAuthProvider.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Auth/AuthProviders/BasicAuthProvider.tsx index f63de85268d3..b753b30483a5 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Auth/AuthProviders/BasicAuthProvider.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Auth/AuthProviders/BasicAuthProvider.tsx @@ -19,7 +19,7 @@ import { HTTP_STATUS_CODE, LOGIN_FAILED_ERROR, } from '../../../constants/Auth.constants'; -import { ROUTES } from '../../../constants/constants'; +import { APP_ROUTER_ROUTES as ROUTES } from '../../../constants/router.constants'; import { PasswordResetRequest } from '../../../generated/auth/passwordResetRequest'; import { RegistrationRequest } from '../../../generated/auth/registrationRequest'; import { @@ -29,7 +29,6 @@ import { logoutUser, resetPassword, } from '../../../rest/auth-API'; -import { getBase64EncodedString } from '../../../utils/CommonUtils'; import { showErrorToast, showInfoToast, @@ -89,7 +88,7 @@ const BasicAuthProvider = ({ children }: BasicAuthProps) => { try { const response = await basicAuthSignIn({ email, - password: getBase64EncodedString(password), + password: btoa(password), }); if (response.accessToken) { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Auth/AuthProviders/LazyAuthProviderWrappers.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Auth/AuthProviders/LazyAuthProviderWrappers.tsx new file mode 100644 index 000000000000..6a047313dcc2 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/Auth/AuthProviders/LazyAuthProviderWrappers.tsx @@ -0,0 +1,104 @@ +/* + * 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 { CacheLocation } from '@auth0/auth0-react'; +import type { IPublicClientApplication } from '@azure/msal-browser'; +import { lazy, ReactNode } from 'react'; +import withSuspenseFallback from '../../AppRouter/withSuspenseFallback'; + +const Auth0ProviderComponent = withSuspenseFallback( + lazy(() => + import('@auth0/auth0-react').then((m) => ({ default: m.Auth0Provider })) + ) +); + +const MsalProviderComponent = withSuspenseFallback( + lazy(() => + import('@azure/msal-react').then((m) => ({ default: m.MsalProvider })) + ) +); + +const OktaAuthProviderComponent = withSuspenseFallback( + lazy(() => + import('./OktaAuthProvider').then((m) => ({ default: m.OktaAuthProvider })) + ) +); + +const BasicAuthProviderComponent = withSuspenseFallback( + lazy(() => import('./BasicAuthProvider')) +); + +interface Auth0ProviderWrapperProps { + clientId: string; + domain: string; + redirectUri: string; + children: ReactNode; + useRefreshTokens: boolean; + cacheLocation?: CacheLocation; +} + +export const LazyAuth0ProviderWrapper = ({ + clientId, + domain, + redirectUri, + children, + useRefreshTokens, + cacheLocation, +}: Auth0ProviderWrapperProps) => { + return ( + + {children} + + ); +}; + +interface MsalProviderWrapperProps { + instance: IPublicClientApplication; + children: ReactNode; +} + +export const LazyMsalProviderWrapper = ({ + instance, + children, +}: MsalProviderWrapperProps) => { + return ( + + {children} + + ); +}; + +interface OktaAuthProviderWrapperProps { + children: ReactNode; +} + +export const LazyOktaAuthProviderWrapper = ({ + children, +}: OktaAuthProviderWrapperProps) => { + return {children}; +}; + +interface BasicAuthProviderWrapperProps { + children: ReactNode; +} + +export const LazyBasicAuthProviderWrapper = ({ + children, +}: BasicAuthProviderWrapperProps) => { + return {children}; +}; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataAssetSummaryPanelV1/DataAssetSummaryPanelV1.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataAssetSummaryPanelV1/DataAssetSummaryPanelV1.test.tsx index 6f58fbf5c85c..88fd6c7855dc 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataAssetSummaryPanelV1/DataAssetSummaryPanelV1.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataAssetSummaryPanelV1/DataAssetSummaryPanelV1.test.tsx @@ -12,7 +12,6 @@ */ import { - act, cleanup, fireEvent, render, @@ -20,6 +19,7 @@ import { waitFor, } from '@testing-library/react'; import { AxiosError } from 'axios'; +import { act } from 'react'; import { useTranslation } from 'react-i18next'; import { usePermissionProvider } from '../../context/PermissionProvider/PermissionProvider'; import { useTourProvider } from '../../context/TourProvider/TourProvider'; @@ -31,16 +31,18 @@ import { patchDashboardDetails } from '../../rest/dashboardAPI'; import { getListTestCaseIncidentStatus } from '../../rest/incidentManagerAPI'; import { patchTableDetails } from '../../rest/tableAPI'; import { listTestCases } from '../../rest/testAPI'; +import { getEntityOverview } from '../../utils/DataAssetSummaryPanelUtils'; import { getCurrentMillis, getEpochMillisForPastDays, } from '../../utils/date-time/DateTimeUtils'; -import { getEntityOverview } from '../../utils/EntityUtils'; import { generateEntityLink } from '../../utils/TableUtils'; import { showErrorToast, showSuccessToast } from '../../utils/ToastUtils'; import { DataAssetSummaryPanelV1 } from './DataAssetSummaryPanelV1'; import { DataAssetSummaryPanelProps } from './DataAssetSummaryPanelV1.interface'; +type DataAssetType = DataAssetSummaryPanelProps['dataAsset']; + // Mock TableUtils first to ensure getTierTags is available jest.mock('../../utils/TableUtils', () => { const mockGetTierTags = jest.fn(() => null); @@ -93,7 +95,7 @@ jest.mock('../../utils/ToastUtils', () => ({ showSuccessToast: jest.fn(), })); -jest.mock('../../utils/EntityUtils', () => { +jest.mock('../../utils/DataAssetSummaryPanelUtils', () => { const mockGetEntityOverview = jest.fn(() => []); return { @@ -199,9 +201,7 @@ jest.mock('../common/DescriptionSection/DescriptionSection', () => {
@@ -211,10 +211,10 @@ jest.mock('../common/DescriptionSection/DescriptionSection', () => { jest.mock('../common/OverviewSection/OverviewSection', () => { return jest.fn().mockImplementation(({ entityInfoV1 }) => (
- {(entityInfoV1 || []).map((item: any, index: number) => ( + {(entityInfoV1 || []).map((item: { name: string; value: string }) => (
+ key={item.name + item.value}> {item.name} {item.value}
))} @@ -226,8 +226,8 @@ jest.mock('../common/DataQualitySection/DataQualitySection', () => { return jest.fn().mockImplementation(({ tests, totalTests }) => (
{totalTests}
- {tests.map((test: any, index: number) => ( -
+ {tests.map((test: { type: string; count: number }) => ( +
{test.type}: {test.count}
))} @@ -387,7 +387,7 @@ describe('DataAssetSummaryPanelV1', () => { const mockOnDescriptionUpdate = jest.fn(); const defaultProps: DataAssetSummaryPanelProps = { - dataAsset: mockDataAsset as any, + dataAsset: mockDataAsset as unknown as DataAssetType, entityType: EntityType.TABLE, isLoading: false, onOwnerUpdate: mockOnOwnerUpdate, @@ -434,10 +434,7 @@ describe('DataAssetSummaryPanelV1', () => { { name: 'Queries', value: 250, visible: ['explore'] }, { name: 'Incidents', - value: - (additionalInfo && additionalInfo.incidentCount) !== undefined - ? additionalInfo.incidentCount - : 0, + value: additionalInfo?.incidentCount ?? 0, visible: ['explore'], }, ] @@ -608,7 +605,7 @@ describe('DataAssetSummaryPanelV1', () => { dataAsset: { ...mockDataAsset, deleted: true, - } as any, + } as unknown as DataAssetType, }; await act(async () => { @@ -916,7 +913,7 @@ describe('DataAssetSummaryPanelV1', () => { name: 'new-table', displayName: 'New Table', fullyQualifiedName: 'new.fqn', - } as any; + } as unknown as DataAssetType; await act(async () => { rerender( diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataAssetSummaryPanelV1/DataAssetSummaryPanelV1.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataAssetSummaryPanelV1/DataAssetSummaryPanelV1.tsx index 3a30eca1f29a..eec9149e74b5 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataAssetSummaryPanelV1/DataAssetSummaryPanelV1.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataAssetSummaryPanelV1/DataAssetSummaryPanelV1.tsx @@ -10,30 +10,19 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { AxiosError } from 'axios'; +import { Operation } from 'fast-json-patch'; import { isEmpty } from 'lodash'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; +import { ENTITY_PATH } from '../../constants/constants'; +import { PROFILER_FILTER_RANGE } from '../../constants/profiler.constant'; import { usePermissionProvider } from '../../context/PermissionProvider/PermissionProvider'; import { OperationPermission, ResourceEntity, } from '../../context/PermissionProvider/PermissionProvider.interface'; import { useTourProvider } from '../../context/TourProvider/TourProvider'; -import { - getCurrentMillis, - getEpochMillisForPastDays, -} from '../../utils/date-time/DateTimeUtils'; -import EntityLink from '../../utils/EntityLink'; -import { - DRAWER_NAVIGATION_OPTIONS, - getEntityOverview, - hasLineageTab, -} from '../../utils/EntityUtils'; - -import { AxiosError } from 'axios'; -import { Operation } from 'fast-json-patch'; -import { ENTITY_PATH } from '../../constants/constants'; -import { PROFILER_FILTER_RANGE } from '../../constants/profiler.constant'; import { EntityType } from '../../enums/entity.enum'; import { EntityReference } from '../../generated/entity/type'; import { TagLabel, TestCaseStatus } from '../../generated/tests/testCase'; @@ -42,7 +31,17 @@ import { useChangeSummary } from '../../hooks/useChangeSummary'; import { getListTestCaseIncidentStatus } from '../../rest/incidentManagerAPI'; import { updateTableColumn } from '../../rest/tableAPI'; import { listTestCases } from '../../rest/testAPI'; +import { getEntityOverview } from '../../utils/DataAssetSummaryPanelUtils'; +import { + getCurrentMillis, + getEpochMillisForPastDays, +} from '../../utils/date-time/DateTimeUtils'; +import EntityLink from '../../utils/EntityLink'; import entityUtilClassBase from '../../utils/EntityUtilClassBase'; +import { + DRAWER_NAVIGATION_OPTIONS, + hasLineageTab, +} from '../../utils/EntityUtils'; import { DEFAULT_ENTITY_PERMISSION } from '../../utils/PermissionsUtils'; import { generateEntityLink, getTierTags } from '../../utils/TableUtils'; import { showErrorToast, showSuccessToast } from '../../utils/ToastUtils'; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataContract/ContractSLACard/ContractSLA.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataContract/ContractSLACard/ContractSLA.component.tsx index 5bcb872b67b2..a63fad044937 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataContract/ContractSLACard/ContractSLA.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataContract/ContractSLACard/ContractSLA.component.tsx @@ -20,8 +20,8 @@ import { ReactComponent as DefaultIcon } from '../../../assets/svg/ic-task.svg'; import { DATA_CONTRACT_SLA } from '../../../constants/DataContract.constants'; import { DataContract } from '../../../generated/entity/data/dataContract'; import { Table } from '../../../generated/entity/data/table'; -import { Transi18next } from '../../../utils/CommonUtils'; import { getEntityName } from '../../../utils/EntityUtils'; +import { Transi18next } from '../../../utils/i18next/LocalUtil'; import { useGenericContext } from '../../Customization/GenericProvider/GenericProvider'; import './contract-sla.less'; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataContract/ContractSLACard/ContractSLA.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataContract/ContractSLACard/ContractSLA.test.tsx index 26ca45c264ae..1f3ec3324e7b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataContract/ContractSLACard/ContractSLA.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataContract/ContractSLACard/ContractSLA.test.tsx @@ -22,12 +22,23 @@ import { MOCK_DATA_CONTRACT } from '../../../mocks/DataContract.mock'; import { mockTableData } from '../../../mocks/TableVersion.mock'; import ContractSLA from './ContractSLA.component'; -jest.mock('../../../utils/CommonUtils', () => ({ - Transi18next: ({ i18nKey, values }: any) => ( +jest.mock('../../../utils/i18next/LocalUtil', () => ({ + Transi18next: ({ + i18nKey, + values, + }: { + i18nKey: string; + values: Record; + }) => ( {i18nKey} - {values?.label}: {values?.data} ), + __esModule: true, + default: { + t: jest.fn().mockImplementation((key) => key), + }, + t: jest.fn().mockImplementation((key) => key), })); jest.mock('../../../assets/svg/ic-check-circle-2.svg', () => ({ diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataInsight/EmptyGraphPlaceholder.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataInsight/EmptyGraphPlaceholder.tsx index ff60ae271c23..3d3cf5bf7da5 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataInsight/EmptyGraphPlaceholder.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataInsight/EmptyGraphPlaceholder.tsx @@ -16,7 +16,7 @@ import { ReactElement } from 'react'; import { useTranslation } from 'react-i18next'; import { DATA_INSIGHT_DOCS } from '../../constants/docs.constants'; import { ERROR_PLACEHOLDER_TYPE, SIZE } from '../../enums/common.enum'; -import { Transi18next } from '../../utils/CommonUtils'; +import { Transi18next } from '../../utils/i18next/LocalUtil'; import ErrorPlaceHolder from '../common/ErrorWithPlaceholder/ErrorPlaceHolder'; export const EmptyGraphPlaceholder = ({ icon }: { icon?: ReactElement }) => { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataInsight/KPIChart.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataInsight/KPIChart.test.tsx index f8fa7e2c1f0c..9e24397d17e8 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataInsight/KPIChart.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataInsight/KPIChart.test.tsx @@ -22,14 +22,6 @@ jest.mock('../../rest/KpiAPI', () => ({ .mockImplementation(() => Promise.resolve({ data: KPI_LIST })), })); -jest.mock('../../utils/i18next/LocalUtil', () => ({ - t: jest.fn((key: string) => key), - translateWithNestedKeys: jest.fn((key: string, nestedKey?: string) => { - return nestedKey ? `${key}.${nestedKey}` : key; - }), - detectBrowserLanguage: jest.fn(() => 'en-US'), -})); - describe('Test KPIChart Component', () => { const mockProps = { chartFilter: { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataMarketplace/MarketplaceNavBar/MarketplaceNavBar.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataMarketplace/MarketplaceNavBar/MarketplaceNavBar.component.tsx index 8454dd3ad87c..8d6c103601ca 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataMarketplace/MarketplaceNavBar/MarketplaceNavBar.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataMarketplace/MarketplaceNavBar/MarketplaceNavBar.component.tsx @@ -15,7 +15,6 @@ import { Alert, Badge, Button, Dropdown, Tooltip } from 'antd'; import { Header } from 'antd/lib/layout/layout'; import { AxiosError } from 'axios'; import { CookieStorage } from 'cookie-storage'; -import i18next from 'i18next'; import { startCase, upperCase } from 'lodash'; import { MenuInfo } from 'rc-menu/lib/interface'; import { useCallback, useEffect, useMemo, useState } from 'react'; @@ -58,7 +57,8 @@ import { prepareFeedLink, } from '../../../utils/FeedUtils'; import { languageSelectOptions } from '../../../utils/i18next/i18nextUtil'; -import { SupportedLocales } from '../../../utils/i18next/LocalUtil.interface'; +import i18n from '../../../utils/i18next/LocalUtil'; +import localUtilClassBase from '../../../utils/i18next/LocalUtilClassBase'; import { getHelpDropdownItems } from '../../../utils/NavbarUtils'; import { getSettingPath } from '../../../utils/RouterUtils'; import { showErrorToast } from '../../../utils/ToastUtils'; @@ -90,7 +90,7 @@ const MarketplaceNavBar = () => { const [activeTab, setActiveTab] = useState('Task'); const { appVersion: version, setAppVersion } = useApplicationStore(); const { - preferences: { isSidebarCollapsed, language }, + preferences: { isSidebarCollapsed }, setPreference, } = useCurrentUserPreferences(); @@ -363,12 +363,16 @@ const MarketplaceNavBar = () => { fetchOMVersion(); }, []); - const handleLanguageChange = useCallback(({ key }: MenuInfo) => { - i18next.changeLanguage(key); - setPreference({ language: key as SupportedLocales }); + const handleLanguageChange = useCallback(async ({ key }: MenuInfo) => { + await localUtilClassBase.loadLocales(key); + await i18n.changeLanguage(key); navigate(0); }, []); + const currentLanguage = i18n.language + ? upperCase(i18n.language.split('-')[0]) + : ''; + return ( <>
{ className="flex-center gap-2 p-x-xs font-medium" data-testid="language-selector-button" type="text"> - {language ? upperCase(language.split('-')[0]) : ''}{' '} - + {currentLanguage} ({ - useTranslation: jest.fn(() => ({ - t: (key: string, params?: Record) => { - if (key.includes('.') && params) { - return key.replace( - /\{\{(\w+)\}\}/g, - (_, paramKey) => params[paramKey] || '' - ); - } - - return key; - }, - })), - Trans: jest.fn(({ children, i18nKey }) => { - // Simple mock for Trans component that renders children - if (typeof children === 'string') { - return children; - } - - return children || i18nKey; - }), -})); - jest.mock('@openmetadata/ui-core-components', () => ({ Alert: ({ title, diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/AddDataQualityTest/components/TestCaseFormV1.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/AddDataQualityTest/components/TestCaseFormV1.tsx index f497157a02da..80da4b0d0e98 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/AddDataQualityTest/components/TestCaseFormV1.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/AddDataQualityTest/components/TestCaseFormV1.tsx @@ -99,7 +99,6 @@ import { import { filterSelectOptions, replaceAllSpacialCharWith_, - Transi18next, } from '../../../../utils/CommonUtils'; import { convertSearchSourceToTable, @@ -111,6 +110,7 @@ import { generateFormFields, getPopupContainer, } from '../../../../utils/formUtils'; +import { Transi18next } from '../../../../utils/i18next/LocalUtil'; import { getScheduleOptionsFromSchedules } from '../../../../utils/SchedularUtils'; import { getIngestionName } from '../../../../utils/ServiceUtils'; import { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Database/Profiler/DataQualityTab/DataQualityTab.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Database/Profiler/DataQualityTab/DataQualityTab.tsx index 340b5a39c799..fffd19b7c25e 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Database/Profiler/DataQualityTab/DataQualityTab.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Database/Profiler/DataQualityTab/DataQualityTab.tsx @@ -47,12 +47,13 @@ import { TestSuite } from '../../../../generated/tests/testSuite'; import { TestCasePageTabs } from '../../../../pages/IncidentManager/IncidentManager.interface'; import { getListTestCaseIncidentByStateId } from '../../../../rest/incidentManagerAPI'; import { removeTestCaseFromTestSuite } from '../../../../rest/testAPI'; -import { getNameFromFQN, Transi18next } from '../../../../utils/CommonUtils'; +import { getNameFromFQN } from '../../../../utils/CommonUtils'; import { getColumnNameFromEntityLink, getEntityName, } from '../../../../utils/EntityUtils'; import { getEntityFQN } from '../../../../utils/FeedUtils'; +import { Transi18next } from '../../../../utils/i18next/LocalUtil'; import { getEntityDetailsPath, getTestCaseDetailPagePath, diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Database/Profiler/TableProfiler/SingleColumnProfile.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Database/Profiler/TableProfiler/SingleColumnProfile.tsx index e469e58f89ef..2b7d69b97678 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Database/Profiler/TableProfiler/SingleColumnProfile.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Database/Profiler/TableProfiler/SingleColumnProfile.tsx @@ -34,11 +34,9 @@ import { import { Table } from '../../../../generated/entity/data/table'; import useCustomLocation from '../../../../hooks/useCustomLocation/useCustomLocation'; import { getColumnProfilerList } from '../../../../rest/tableAPI'; -import { - formatNumberWithComma, - Transi18next, -} from '../../../../utils/CommonUtils'; +import { formatNumberWithComma } from '../../../../utils/CommonUtils'; import documentationLinksClassBase from '../../../../utils/DocumentationLinksClassBase'; +import { Transi18next } from '../../../../utils/i18next/LocalUtil'; import { calculateColumnProfilerMetrics, calculateCustomMetrics, diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Database/Profiler/TableProfiler/TableProfilerChart/TableProfilerChart.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Database/Profiler/TableProfiler/TableProfilerChart/TableProfilerChart.tsx index 86a46dbd4a0f..1e7559c8effa 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Database/Profiler/TableProfiler/TableProfilerChart/TableProfilerChart.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Database/Profiler/TableProfiler/TableProfilerChart/TableProfilerChart.tsx @@ -31,8 +31,8 @@ import { getSystemProfileList, getTableProfilesList, } from '../../../../../rest/tableAPI'; -import { Transi18next } from '../../../../../utils/CommonUtils'; import documentationLinksClassBase from '../../../../../utils/DocumentationLinksClassBase'; +import { Transi18next } from '../../../../../utils/i18next/LocalUtil'; import { calculateCustomMetrics, calculateRowCountMetrics, diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Database/SampleDataTable/SampleDataTable.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Database/SampleDataTable/SampleDataTable.component.tsx index daa2528cb95c..332ca93d6844 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Database/SampleDataTable/SampleDataTable.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Database/SampleDataTable/SampleDataTable.component.tsx @@ -33,11 +33,9 @@ import { deleteSampleDataByTableId, getSampleDataByTableId, } from '../../../rest/tableAPI'; -import { - getEntityDeleteMessage, - Transi18next, -} from '../../../utils/CommonUtils'; +import { getEntityDeleteMessage } from '../../../utils/CommonUtils'; import { downloadFile } from '../../../utils/Export/ExportUtils'; +import { Transi18next } from '../../../utils/i18next/LocalUtil'; import { showErrorToast } from '../../../utils/ToastUtils'; import ErrorPlaceHolder from '../../common/ErrorWithPlaceholder/ErrorPlaceHolder'; import Loader from '../../common/Loader/Loader'; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Database/SampleDataWithMessages/SampleDataWithMessages.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Database/SampleDataWithMessages/SampleDataWithMessages.tsx index ebe6ac527d33..c461fbc5a11c 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Database/SampleDataWithMessages/SampleDataWithMessages.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Database/SampleDataWithMessages/SampleDataWithMessages.tsx @@ -22,7 +22,7 @@ import { TopicSampleData } from '../../../generated/entity/data/topic'; import { useApplicationStore } from '../../../hooks/useApplicationStore'; import { getSampleDataBySearchIndexId } from '../../../rest/SearchIndexAPI'; import { getSampleDataByTopicId } from '../../../rest/topicsAPI'; -import { Transi18next } from '../../../utils/CommonUtils'; +import { Transi18next } from '../../../utils/i18next/LocalUtil'; import ErrorPlaceHolder from '../../common/ErrorWithPlaceholder/ErrorPlaceHolder'; import Loader from '../../common/Loader/Loader'; import MessageCard from './MessageCard'; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Domain/DomainDetailPage/DomainDetailPage.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Domain/DomainDetailPage/DomainDetailPage.test.tsx index 6f342eca829b..8a97f23e0c4d 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Domain/DomainDetailPage/DomainDetailPage.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Domain/DomainDetailPage/DomainDetailPage.test.tsx @@ -26,7 +26,6 @@ jest.mock('../../../utils/i18next/LocalUtil', () => ({ t: (key: string) => key, }, t: (key: string) => key, - detectBrowserLanguage: () => 'en-US', })); // Mock react-helmet-async diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Domain/DomainDetails/DomainDetails.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Domain/DomainDetails/DomainDetails.component.tsx index 6c03c3380d1d..06f6fd712e1d 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Domain/DomainDetails/DomainDetails.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Domain/DomainDetails/DomainDetails.component.tsx @@ -64,7 +64,7 @@ import { import { addDomains, patchDomains } from '../../../rest/domainAPI'; import { getActiveAnnouncement } from '../../../rest/feedsAPI'; import { searchQuery } from '../../../rest/searchAPI'; -import { getFeedCounts, getIsErrorMatch } from '../../../utils/CommonUtils'; +import { getFeedCounts } from '../../../utils/CommonUtils'; import { createEntityWithCoverImage } from '../../../utils/CoverImageUploadUtils'; import { checkIfExpandViewSupported, @@ -107,6 +107,7 @@ import { useBreadcrumbs } from '../../common/atoms/navigation/useBreadcrumbs'; import { Avatar } from '@openmetadata/ui-core-components'; import { LEARNING_PAGE_IDS } from '../../../constants/Learning.constants'; import { FeedCounts } from '../../../interface/feed.interface'; +import { getIsErrorMatch } from '../../../utils/APIUtils'; import { getEntityAvatarProps } from '../../../utils/IconUtils'; import { withActivityFeed } from '../../AppRouter/withActivityFeed'; import { CoverImage } from '../../common/CoverImage/CoverImage.component'; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Explore/AdvanceSearchModal.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Explore/AdvanceSearchModal.component.tsx index 1c48413543ab..e29185044e39 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Explore/AdvanceSearchModal.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Explore/AdvanceSearchModal.component.tsx @@ -13,7 +13,6 @@ import { Builder, Query } from '@react-awesome-query-builder/antd'; import { Button, Modal, Space, Typography } from 'antd'; -import 'antd/dist/antd.css'; import { FunctionComponent } from 'react'; import { useTranslation } from 'react-i18next'; import './advanced-search-modal.less'; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Explore/EntitySummaryPanel/CustomPropertiesSection/CustomPropertiesSection.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Explore/EntitySummaryPanel/CustomPropertiesSection/CustomPropertiesSection.test.tsx index e771ba0fd73a..0ae9421248d3 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Explore/EntitySummaryPanel/CustomPropertiesSection/CustomPropertiesSection.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Explore/EntitySummaryPanel/CustomPropertiesSection/CustomPropertiesSection.test.tsx @@ -32,18 +32,6 @@ jest.mock('react-router-dom', () => ({ )), })); -// Mock Transi18next component -jest.mock('../../../../utils/CommonUtils', () => ({ - Transi18next: jest - .fn() - .mockImplementation(({ i18nKey, renderElement, values }) => ( -
- {i18nKey} - {values?.entity} - {values?.docs} - {renderElement} -
- )), -})); - // Mock Loader component jest.mock('../../../common/Loader/Loader', () => { return jest.fn().mockImplementation(({ size }) => ( @@ -273,10 +261,9 @@ describe('CustomPropertiesSection', () => { expect(errorPlaceholder).toBeInTheDocument(); expect(errorPlaceholder).toHaveAttribute('data-type', 'PERMISSION'); - const transComponent = screen.getByTestId('trans-component'); - - expect(transComponent).toBeInTheDocument(); - expect(transComponent).toHaveTextContent('message.no-access-placeholder'); + expect(errorPlaceholder).toHaveTextContent( + 'message.no-access-placeholder' + ); expect(screen.queryByTestId('search-bar')).not.toBeInTheDocument(); expect(screen.queryByTestId('property-name')).not.toBeInTheDocument(); @@ -297,10 +284,7 @@ describe('CustomPropertiesSection', () => { expect(errorPlaceholder).toBeInTheDocument(); expect(errorPlaceholder).toHaveAttribute('data-type', 'CUSTOM'); - const transComponent = screen.getByTestId('trans-component'); - - expect(transComponent).toBeInTheDocument(); - expect(transComponent).toHaveTextContent( + expect(errorPlaceholder).toHaveTextContent( 'message.no-custom-properties-entity' ); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Explore/EntitySummaryPanel/CustomPropertiesSection/CustomPropertiesSection.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Explore/EntitySummaryPanel/CustomPropertiesSection/CustomPropertiesSection.tsx index f153f4ee3a40..fe2130b7e7c9 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Explore/EntitySummaryPanel/CustomPropertiesSection/CustomPropertiesSection.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Explore/EntitySummaryPanel/CustomPropertiesSection/CustomPropertiesSection.tsx @@ -18,7 +18,7 @@ import { ReactComponent as AddPlaceHolderIcon } from '../../../../assets/svg/ic- import { CUSTOM_PROPERTIES_DOCS } from '../../../../constants/docs.constants'; import { ERROR_PLACEHOLDER_TYPE } from '../../../../enums/common.enum'; import { CustomProperty } from '../../../../generated/entity/type'; -import { Transi18next } from '../../../../utils/CommonUtils'; +import { Transi18next } from '../../../../utils/i18next/LocalUtil'; import { PropertyValue } from '../../../common/CustomPropertyTable/PropertyValue'; import ErrorPlaceHolderNew from '../../../common/ErrorWithPlaceholder/ErrorPlaceHolderNew'; import Loader from '../../../common/Loader/Loader'; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Explore/EntitySummaryPanel/DataQualityTab/DataQualityTab.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Explore/EntitySummaryPanel/DataQualityTab/DataQualityTab.test.tsx index b92573748be8..b753447d5ee9 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Explore/EntitySummaryPanel/DataQualityTab/DataQualityTab.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Explore/EntitySummaryPanel/DataQualityTab/DataQualityTab.test.tsx @@ -236,7 +236,7 @@ jest.mock('../../../../utils/EntityUtils', () => ({ if (entityLink.includes('::columns::')) { const parts = entityLink.split('::columns::'); - return parts[parts.length - 1]; + return parts.at(-1); } return null; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Explore/EntitySummaryPanel/DataQualityTab/DataQualityTab.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Explore/EntitySummaryPanel/DataQualityTab/DataQualityTab.tsx index da5b16cf07cb..8d11246e13dc 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Explore/EntitySummaryPanel/DataQualityTab/DataQualityTab.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Explore/EntitySummaryPanel/DataQualityTab/DataQualityTab.tsx @@ -33,15 +33,13 @@ import { import { Include } from '../../../../generated/type/include'; import { getListTestCaseIncidentStatus } from '../../../../rest/incidentManagerAPI'; import { getListTestCaseBySearch } from '../../../../rest/testAPI'; -import { - getTableFQNFromColumnFQN, - Transi18next, -} from '../../../../utils/CommonUtils'; +import { getTableFQNFromColumnFQN } from '../../../../utils/CommonUtils'; import { getCurrentMillis, getEpochMillisForPastDays, } from '../../../../utils/date-time/DateTimeUtils'; import { getColumnNameFromEntityLink } from '../../../../utils/EntityUtils'; +import { Transi18next } from '../../../../utils/i18next/LocalUtil'; import { getTestCaseDetailPagePath } from '../../../../utils/RouterUtils'; import { generateEntityLink } from '../../../../utils/TableUtils'; import { showErrorToast } from '../../../../utils/ToastUtils'; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Explore/EntitySummaryPanel/EntitySummaryPanel.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Explore/EntitySummaryPanel/EntitySummaryPanel.test.tsx index 249909833fb9..9bc36b61d308 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Explore/EntitySummaryPanel/EntitySummaryPanel.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Explore/EntitySummaryPanel/EntitySummaryPanel.test.tsx @@ -89,17 +89,20 @@ jest.mock('../../../utils/EntityUtils', () => { return { getEntityLinkFromType: jest.fn().mockImplementation(() => 'link'), getEntityName: jest.fn().mockImplementation(() => 'displayName'), - getEntityOverview: jest.fn().mockImplementation(() => []), hasLineageTab: jest.fn((entityType) => LINEAGE_TABS_SET.has(entityType)), hasSchemaTab: jest.fn((entityType) => SCHEMA_TABS_SET.has(entityType)), + getEntityOverview: jest.fn().mockImplementation(() => []), hasCustomPropertiesTab: jest.fn((entityType) => CUSTOM_PROPERTIES_TABS_SET.has(entityType) ), + DRAWER_NAVIGATION_OPTIONS: [], }; }); jest.mock('../../../utils/StringsUtils', () => ({ getEncodedFqn: jest.fn().mockImplementation((fqn) => fqn), stringToHTML: jest.fn(), + bytesToSize: jest.fn(), + ordinalize: jest.fn(), })); jest.mock('react-router-dom', () => ({ diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Explore/ExploreTree/ExploreTree.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Explore/ExploreTree/ExploreTree.test.tsx index aa9ef2473a89..e5dbb069d0f3 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Explore/ExploreTree/ExploreTree.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Explore/ExploreTree/ExploreTree.test.tsx @@ -19,12 +19,6 @@ jest.mock('react-router-dom', () => ({ }), })); -jest.mock('react-i18next', () => ({ - useTranslation: jest.fn().mockReturnValue({ - t: jest.fn().mockImplementation((key) => key), - }), -})); - describe('ExploreTree', () => { it('renders the correct tree nodes', async () => { const { getByText, queryByTestId } = render( diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Explore/ExploreTree/ExploreTree.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Explore/ExploreTree/ExploreTree.tsx index eb9418c9e116..4ae1f0cc8699 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Explore/ExploreTree/ExploreTree.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Explore/ExploreTree/ExploreTree.tsx @@ -27,7 +27,7 @@ import { EntityType } from '../../../enums/entity.enum'; import { ExplorePageTabs } from '../../../enums/Explore.enum'; import { SearchIndex } from '../../../enums/search.enum'; import { searchQuery } from '../../../rest/searchAPI'; -import { getCountBadge, Transi18next } from '../../../utils/CommonUtils'; +import { getCountBadge } from '../../../utils/CommonUtils'; import entityUtilClassBase from '../../../utils/EntityUtilClassBase'; import { getPluralizeEntityName } from '../../../utils/EntityUtils'; import { @@ -38,6 +38,7 @@ import { updateTreeData, updateTreeDataWithCounts, } from '../../../utils/ExploreUtils'; +import { Transi18next } from '../../../utils/i18next/LocalUtil'; import searchClassBase from '../../../utils/SearchClassBase'; import serviceUtilClassBase from '../../../utils/ServiceUtilClassBase'; import { generateUUID } from '../../../utils/StringsUtils'; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ExploreV1/ExploreV1.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ExploreV1/ExploreV1.test.tsx index c10e38470805..57a0880a0c8f 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/ExploreV1/ExploreV1.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/ExploreV1/ExploreV1.test.tsx @@ -245,17 +245,6 @@ jest.mock('react-i18next', () => ({ Trans: ({ children }: { children: React.ReactNode }) => <>{children}, })); -jest.mock('../../utils/CommonUtils', () => ({ - Transi18next: jest - .fn() - .mockImplementation(({ i18nKey, renderElement, values }) => ( -
- {i18nKey} {values && JSON.stringify(values)} - {renderElement} -
- )), -})); - jest.mock('../../utils/AdvancedSearchUtils', () => ({ getDropDownItems: jest.fn().mockReturnValue([]), })); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ExploreV1/IndexNotFoundBanner.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ExploreV1/IndexNotFoundBanner.test.tsx index 51bd41bc57b9..6b7c1a44d0eb 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/ExploreV1/IndexNotFoundBanner.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/ExploreV1/IndexNotFoundBanner.test.tsx @@ -16,12 +16,6 @@ import { SEARCH_INDEXING_APPLICATION } from '../../constants/explore.constants'; import { getApplicationDetailsPath } from '../../utils/RouterUtils'; import { IndexNotFoundBanner } from './IndexNotFoundBanner'; -jest.mock('react-i18next', () => ({ - useTranslation: jest.fn().mockReturnValue({ - t: (key: string) => key, - }), -})); - jest.mock('../../hooks/useApplicationStore', () => ({ useApplicationStore: jest.fn().mockReturnValue({ theme: { @@ -34,18 +28,6 @@ jest.mock('../../utils/RouterUtils', () => ({ getApplicationDetailsPath: jest.fn().mockReturnValue('/settings/search'), })); -jest.mock('../../utils/CommonUtils', () => ({ - Transi18next: jest - .fn() - .mockImplementation(({ i18nKey, renderElement, values }) => ( -
- {i18nKey} - {values?.settings} - {renderElement} -
- )), -})); - jest.mock('react-router-dom', () => ({ ...jest.requireActual('react-router-dom'), Link: ({ @@ -86,17 +68,17 @@ jest.mock('antd', () => ({ })); describe('IndexNotFoundBanner', () => { - it('renders indexing error details and re-index help text', () => { + it('renders indexing error details and re-index help text', async () => { render(); expect(screen.getByTestId('index-not-found-alert')).toBeInTheDocument(); expect(screen.getByText('server.indexing-error')).toBeInTheDocument(); - expect(screen.getByTestId('trans-i18next')).toHaveTextContent( - 'message.configure-search-re-index' - ); - expect(screen.getByTestId('trans-settings-value')).toHaveTextContent( - 'label.search-index-setting-plural' - ); + expect( + await screen.findByText(/message.configure-search-re-index/) + ).toBeInTheDocument(); + expect( + await screen.findByText(/label.search-index-setting-plural/) + ).toBeInTheDocument(); }); it('builds the settings link using search indexing application path', () => { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ExploreV1/IndexNotFoundBanner.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ExploreV1/IndexNotFoundBanner.tsx index fe54bddd81bd..2af63492c9d9 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/ExploreV1/IndexNotFoundBanner.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/ExploreV1/IndexNotFoundBanner.tsx @@ -17,7 +17,7 @@ import { useTranslation } from 'react-i18next'; import { Link } from 'react-router-dom'; import { SEARCH_INDEXING_APPLICATION } from '../../constants/explore.constants'; import { useApplicationStore } from '../../hooks/useApplicationStore'; -import { Transi18next } from '../../utils/CommonUtils'; +import { Transi18next } from '../../utils/i18next/LocalUtil'; import { getApplicationDetailsPath } from '../../utils/RouterUtils'; export const IndexNotFoundBanner = () => { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTermTab/GlossaryTermTab.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTermTab/GlossaryTermTab.component.tsx index 359b2b60f0b4..742db91b4855 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTermTab/GlossaryTermTab.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTermTab/GlossaryTermTab.component.tsx @@ -87,7 +87,6 @@ import { patchGlossaryTerm, searchGlossaryTermsPaginated, } from '../../../rest/glossaryAPI'; -import { Transi18next } from '../../../utils/CommonUtils'; import { getBulkEditButton } from '../../../utils/EntityBulkEdit/EntityBulkEditUtils'; import { EntityStatusClass } from '../../../utils/EntityStatusUtils'; import { @@ -101,6 +100,7 @@ import { glossaryTermTableColumnsWidth, permissionForApproveOrReject, } from '../../../utils/GlossaryUtils'; +import { Transi18next } from '../../../utils/i18next/LocalUtil'; import { getGlossaryPath } from '../../../utils/RouterUtils'; import { ownerTableObject } from '../../../utils/TableColumn.util'; import { showErrorToast, showSuccessToast } from '../../../utils/ToastUtils'; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Layout/CarouselLayout/CarouselLayout.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Layout/CarouselLayout/CarouselLayout.tsx index 6056176245ad..efae472360f8 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Layout/CarouselLayout/CarouselLayout.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Layout/CarouselLayout/CarouselLayout.tsx @@ -13,11 +13,15 @@ import { Col, Grid, Layout, Row } from 'antd'; import { Content } from 'antd/lib/layout/layout'; import classNames from 'classnames'; -import { ReactNode } from 'react'; -import LoginCarousel from '../../../pages/LoginPage/LoginCarousel'; +import { lazy, ReactNode } from 'react'; +import withSuspenseFallback from '../../AppRouter/withSuspenseFallback'; import DocumentTitle from '../../common/DocumentTitle/DocumentTitle'; import './carousel-layout.less'; +const LoginCarousel = withSuspenseFallback( + lazy(() => import('../../../pages/LoginPage/LoginCarousel')) +); + export const CarouselLayout = ({ pageTitle, children, diff --git a/openmetadata-ui/src/main/resources/ui/src/components/LineageTable/LineageTable.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/LineageTable/LineageTable.test.tsx index a194e42337e7..136a0a296cd6 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/LineageTable/LineageTable.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/LineageTable/LineageTable.test.tsx @@ -75,7 +75,6 @@ jest.mock('react-router-dom', () => ({ })); jest.mock('../../utils/CommonUtils', () => ({ - Transi18next: ({ i18nKey }: { i18nKey: string }) => i18nKey, getPartialNameFromTableFQN: jest .fn() .mockImplementation((fqn: string) => fqn), diff --git a/openmetadata-ui/src/main/resources/ui/src/components/LineageTable/LineageTable.tsx b/openmetadata-ui/src/main/resources/ui/src/components/LineageTable/LineageTable.tsx index fdd867dfb75c..564118389707 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/LineageTable/LineageTable.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/LineageTable/LineageTable.tsx @@ -55,7 +55,6 @@ import { getLineageByEntityCount, getLineageDataByFQN, } from '../../rest/lineageAPI'; -import { Transi18next } from '../../utils/CommonUtils'; import { getEntityLinkFromType, getEntityName, @@ -63,6 +62,7 @@ import { } from '../../utils/EntityUtils'; import { getQuickFilterQuery } from '../../utils/ExploreUtils'; import Fqn from '../../utils/Fqn'; +import { Transi18next } from '../../utils/i18next/LocalUtil'; import { getSearchNameEsQuery, LINEAGE_IMPACT_OPTIONS, diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Modals/ChangeParentHierarchy/ChangeParentHierarchy.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Modals/ChangeParentHierarchy/ChangeParentHierarchy.component.tsx index e2c52f9cb79c..970f072db257 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Modals/ChangeParentHierarchy/ChangeParentHierarchy.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Modals/ChangeParentHierarchy/ChangeParentHierarchy.component.tsx @@ -28,9 +28,9 @@ import { GlossaryTerm, } from '../../../generated/entity/data/glossaryTerm'; import { moveGlossaryTerm } from '../../../rest/glossaryAPI'; -import { Transi18next } from '../../../utils/CommonUtils'; import { EntityStatusClass } from '../../../utils/EntityStatusUtils'; import { getEntityName } from '../../../utils/EntityUtils'; +import { Transi18next } from '../../../utils/i18next/LocalUtil'; import { getGlossaryPath } from '../../../utils/RouterUtils'; import { showErrorToast } from '../../../utils/ToastUtils'; import Banner from '../../common/Banner/Banner'; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Modals/ChangeParentHierarchy/ChangeParentHierarchy.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Modals/ChangeParentHierarchy/ChangeParentHierarchy.test.tsx index 6fe3b74d3e05..20a3dfdeee8e 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Modals/ChangeParentHierarchy/ChangeParentHierarchy.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Modals/ChangeParentHierarchy/ChangeParentHierarchy.test.tsx @@ -11,14 +11,9 @@ * limitations under the License. */ -import { - act, - findByRole, - fireEvent, - render, - screen, -} from '@testing-library/react'; +import { findByRole, fireEvent, render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; +import { act } from 'react'; import { PageType } from '../../../generated/system/ui/page'; import { mockedGlossaryTerms } from '../../../mocks/Glossary.mock'; import ChangeParent from './ChangeParentHierarchy.component'; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Modals/EntityDeleteModal/EntityDeleteModal.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Modals/EntityDeleteModal/EntityDeleteModal.test.tsx index 565a492e3020..e79e9e8298c0 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Modals/EntityDeleteModal/EntityDeleteModal.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Modals/EntityDeleteModal/EntityDeleteModal.test.tsx @@ -11,9 +11,10 @@ * limitations under the License. */ -import { act, fireEvent, render, screen } from '@testing-library/react'; +import { fireEvent, render, screen } from '@testing-library/react'; +import { act } from 'react'; import { MemoryRouter } from 'react-router-dom'; -import * as CommonUtils from '../../../utils/CommonUtils'; +import { Transi18next } from '../../../utils/i18next/LocalUtil'; import EntityDeleteModal from './EntityDeleteModal'; const onCancel = jest.fn(); @@ -35,13 +36,6 @@ jest.mock('../../../utils/BrandData/BrandClassBase', () => ({ }, })); -jest.mock('react-i18next', () => ({ - Trans: jest.fn().mockImplementation(() =>
Trans
), - useTranslation: () => ({ - t: (key: string) => key, - }), -})); - describe('Test EntityDelete Modal Component', () => { it('Should render component', async () => { await act(async () => { @@ -141,18 +135,6 @@ describe('Test EntityDelete Modal Component', () => { }); it('should render with correct brandName (OpenMetadata or Collate)', async () => { - // Mock Transi18next to actually render interpolated values - const mockTransi18next = jest.fn(({ values }) => ( -
- {values?.entityName && `Entity: ${values.entityName}`} - {values?.brandName && ` Brand: ${values.brandName}`} -
- )); - - jest - .spyOn(CommonUtils, 'Transi18next') - .mockImplementation(mockTransi18next); - await act(async () => { render(, { wrapper: MemoryRouter, @@ -168,7 +150,7 @@ describe('Test EntityDelete Modal Component', () => { expect(bodyText.textContent).not.toContain('{{brandName}}'); // Verify Transi18next was called with brandName parameter - expect(mockTransi18next).toHaveBeenCalledWith( + expect(Transi18next).toHaveBeenCalledWith( expect.objectContaining({ i18nKey: 'message.permanently-delete-metadata', values: expect.objectContaining({ diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Modals/EntityDeleteModal/EntityDeleteModal.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Modals/EntityDeleteModal/EntityDeleteModal.tsx index 7bbc831c4935..42c4c40d3905 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Modals/EntityDeleteModal/EntityDeleteModal.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Modals/EntityDeleteModal/EntityDeleteModal.tsx @@ -15,7 +15,7 @@ import { Button, Input, InputRef, Modal, Typography } from 'antd'; import { ChangeEvent, useEffect, useMemo, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import brandClassBase from '../../../utils/BrandData/BrandClassBase'; -import { Transi18next } from '../../../utils/CommonUtils'; +import { Transi18next } from '../../../utils/i18next/LocalUtil'; import { EntityDeleteModalProp } from './EntityDeleteModal.interface'; const EntityDeleteModal = ({ diff --git a/openmetadata-ui/src/main/resources/ui/src/components/MyData/CustomizableComponents/CustomizablePageHeader/CustomizablePageHeader.tsx b/openmetadata-ui/src/main/resources/ui/src/components/MyData/CustomizableComponents/CustomizablePageHeader/CustomizablePageHeader.tsx index 27ad2d6a5b99..ec6f45fe2d79 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/MyData/CustomizableComponents/CustomizablePageHeader/CustomizablePageHeader.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/MyData/CustomizableComponents/CustomizablePageHeader/CustomizablePageHeader.tsx @@ -24,7 +24,7 @@ import { Link, useNavigate } from 'react-router-dom'; import { PageType } from '../../../../generated/system/ui/page'; import { useFqn } from '../../../../hooks/useFqn'; import { useCustomizeStore } from '../../../../pages/CustomizablePage/CustomizeStore'; -import { Transi18next } from '../../../../utils/CommonUtils'; +import { Transi18next } from '../../../../utils/i18next/LocalUtil'; import { getPersonaDetailsPath } from '../../../../utils/RouterUtils'; import { UnsavedChangesModal } from '../../../Modals/UnsavedChangesModal/UnsavedChangesModal.component'; import './customizable-page-header.less'; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/MyData/LeftSidebar/left-sidebar.less b/openmetadata-ui/src/main/resources/ui/src/components/MyData/LeftSidebar/left-sidebar.less index e46656063771..6434a41d33c2 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/MyData/LeftSidebar/left-sidebar.less +++ b/openmetadata-ui/src/main/resources/ui/src/components/MyData/LeftSidebar/left-sidebar.less @@ -36,8 +36,10 @@ } .left-sidebar-menu { - display: flex; - flex-direction: column; + &:not(.ant-menu-submenu-hidden) { + display: flex; + flex-direction: column; + } .ant-menu-item, .ant-menu-submenu { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/MyData/Widgets/MyTaskWidget/my-task-widget.less b/openmetadata-ui/src/main/resources/ui/src/components/MyData/Widgets/MyTaskWidget/my-task-widget.less index eaa06b019f9b..73289d967edb 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/MyData/Widgets/MyTaskWidget/my-task-widget.less +++ b/openmetadata-ui/src/main/resources/ui/src/components/MyData/Widgets/MyTaskWidget/my-task-widget.less @@ -115,18 +115,8 @@ } .widget-header-options { - padding: 10px; - height: 40px; min-width: 40px; max-width: 150px; - border-radius: 8px; - border: 1px solid @grey-15; - font-size: 20px; - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - background-color: @white; &:hover { background-color: @grey-1; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/NavBar/NavBar.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/NavBar/NavBar.test.tsx index 619f0fc33e4b..8066e4d6cacb 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/NavBar/NavBar.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/NavBar/NavBar.test.tsx @@ -10,7 +10,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { act, render, screen } from '@testing-library/react'; +import { render, screen } from '@testing-library/react'; +import { act } from 'react'; import { LAST_VERSION_FETCH_TIME_KEY, ONE_HOUR_MS, @@ -213,6 +214,11 @@ jest.mock('./PopupAlertClassBase', () => ({ }, })); +jest.mock('../../utils/i18next/i18nextUtil', () => ({ + languageSelectOptions: [], + getInitOptions: jest.fn().mockImplementation(() => ({})), +})); + describe('Test NavBar Component', () => { it('Should render NavBar component', async () => { render(); @@ -287,6 +293,7 @@ describe('handleDocumentVisibilityChange one hour threshold', () => { jest.resetModules(); jest.clearAllMocks(); global.Date.now = jest.fn(); + mockUseCustomLocation.pathname = '/'; }); afterEach(() => { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/NavBar/NavBar.tsx b/openmetadata-ui/src/main/resources/ui/src/components/NavBar/NavBar.tsx index 4659927d9249..0bed65667459 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/NavBar/NavBar.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/NavBar/NavBar.tsx @@ -24,7 +24,6 @@ import { Header } from 'antd/lib/layout/layout'; import { AxiosError } from 'axios'; import classNames from 'classnames'; import { CookieStorage } from 'cookie-storage'; -import i18next from 'i18next'; import { startCase, upperCase } from 'lodash'; import { MenuInfo } from 'rc-menu/lib/interface'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; @@ -73,7 +72,8 @@ import { prepareFeedLink, } from '../../utils/FeedUtils'; import { languageSelectOptions } from '../../utils/i18next/i18nextUtil'; -import { SupportedLocales } from '../../utils/i18next/LocalUtil.interface'; +import i18n from '../../utils/i18next/LocalUtil'; +import localUtilClassBase from '../../utils/i18next/LocalUtilClassBase'; import { isCommandKeyPress, Keys } from '../../utils/KeyboardUtil'; import { getHelpDropdownItems } from '../../utils/NavbarUtils'; import { getSettingPath } from '../../utils/RouterUtils'; @@ -111,7 +111,7 @@ const NavBar = () => { const { appVersion: version, setAppVersion } = useApplicationStore(); const [isDomainDropdownOpen, setIsDomainDropdownOpen] = useState(false); const { - preferences: { isSidebarCollapsed, language }, + preferences: { isSidebarCollapsed }, setPreference, } = useCurrentUserPreferences(); @@ -438,12 +438,16 @@ const NavBar = () => { [activeDomainEntityRef, activeDomain, t] ); - const handleLanguageChange = useCallback(({ key }: MenuInfo) => { - i18next.changeLanguage(key); - setPreference({ language: key as SupportedLocales }); + const handleLanguageChange = useCallback(async ({ key }: MenuInfo) => { + await localUtilClassBase.loadLocales(key); + await i18n.changeLanguage(key); navigate(0); }, []); + const currentLanguage = i18n.language + ? upperCase(i18n.language.split('-')[0]) + : ''; + return ( <>
{ className="flex-center gap-2 p-x-xs font-medium" data-testid="language-selector-button" type="text"> - {language ? upperCase(language.split('-')[0]) : ''}{' '} + {currentLanguage} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/NotificationBox/NotificationBox.utils.tsx b/openmetadata-ui/src/main/resources/ui/src/components/NotificationBox/NotificationBox.utils.tsx index 5ddb863e62e0..7680c59b148b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/NotificationBox/NotificationBox.utils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/NotificationBox/NotificationBox.utils.tsx @@ -12,16 +12,16 @@ */ import Icon from '@ant-design/icons'; -import i18next from 'i18next'; import { ReactComponent as IconMentions } from '../../assets/svg/ic-mentions.svg'; import { ReactComponent as IconTask } from '../../assets/svg/ic-task.svg'; import { FeedFilter } from '../../enums/mydata.enum'; import { NotificationTabsKey } from '../../enums/notification.enum'; import { ThreadType } from '../../generated/api/feed/createThread'; +import i18n from '../../utils/i18next/LocalUtil'; export const tabsInfo = [ { - name: i18next.t('label.task-plural'), + name: i18n.t('label.task-plural'), key: NotificationTabsKey.TASK, icon: ( ( + lazy(() => import('../ApplicationConfiguration/ApplicationConfiguration')) + ); + class ApplicationsClassBase { public async importSchema(fqn: string) { const module = await import( diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Applications/AppInstallVerifyCard/AppInstallVerifyCard.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Applications/AppInstallVerifyCard/AppInstallVerifyCard.component.tsx index 5f25fddad5ca..768612b3812d 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Applications/AppInstallVerifyCard/AppInstallVerifyCard.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Applications/AppInstallVerifyCard/AppInstallVerifyCard.component.tsx @@ -27,9 +27,9 @@ import { import { useTranslation } from 'react-i18next'; import { LIGHT_GREEN_COLOR } from '../../../../constants/constants'; import { useApplicationStore } from '../../../../hooks/useApplicationStore'; -import { Transi18next } from '../../../../utils/CommonUtils'; import { getRelativeTime } from '../../../../utils/date-time/DateTimeUtils'; import { getEntityName } from '../../../../utils/EntityUtils'; +import { Transi18next } from '../../../../utils/i18next/LocalUtil'; import BrandImage from '../../../common/BrandImage/BrandImage'; import UserPopOverCard from '../../../common/PopOverCard/UserPopOverCard'; import AppLogo from '../AppLogo/AppLogo.component'; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Applications/MarketPlaceAppDetails/MarketPlaceAppDetails.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Applications/MarketPlaceAppDetails/MarketPlaceAppDetails.component.tsx index 42f9b4845568..48d978694ee5 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Applications/MarketPlaceAppDetails/MarketPlaceAppDetails.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Applications/MarketPlaceAppDetails/MarketPlaceAppDetails.component.tsx @@ -35,8 +35,8 @@ import { useFqn } from '../../../../hooks/useFqn'; import { getApplicationByName } from '../../../../rest/applicationAPI'; import { getMarketPlaceApplicationByFqn } from '../../../../rest/applicationMarketPlaceAPI'; import brandClassBase from '../../../../utils/BrandData/BrandClassBase'; -import { Transi18next } from '../../../../utils/CommonUtils'; import { getEntityName } from '../../../../utils/EntityUtils'; +import { Transi18next } from '../../../../utils/i18next/LocalUtil'; import { getAppInstallPath } from '../../../../utils/RouterUtils'; import { showErrorToast } from '../../../../utils/ToastUtils'; import Loader from '../../../common/Loader/Loader'; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Ingestion/IngestionListTable/IngestionListTable.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Ingestion/IngestionListTable/IngestionListTable.test.tsx index 2c8ea78134d1..360e28d182c9 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Ingestion/IngestionListTable/IngestionListTable.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Ingestion/IngestionListTable/IngestionListTable.test.tsx @@ -11,8 +11,9 @@ * limitations under the License. */ -import { act, fireEvent, render, screen } from '@testing-library/react'; +import { fireEvent, render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; +import { act } from 'react'; import { MemoryRouter } from 'react-router-dom'; import { usePermissionProvider } from '../../../../../context/PermissionProvider/PermissionProvider'; import { mockIngestionData } from '../../../../../mocks/Ingestion.mock'; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Ingestion/IngestionListTable/IngestionListTable.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Ingestion/IngestionListTable/IngestionListTable.tsx index 3c059d5746f1..78d5f8cf2683 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Ingestion/IngestionListTable/IngestionListTable.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Ingestion/IngestionListTable/IngestionListTable.tsx @@ -40,12 +40,12 @@ import { deleteIngestionPipelineById, getRunHistoryForPipeline, } from '../../../../../rest/ingestionPipelineAPI'; -import { Transi18next } from '../../../../../utils/CommonUtils'; import { getColumnSorter, getEntityName, highlightSearchText, } from '../../../../../utils/EntityUtils'; +import { Transi18next } from '../../../../../utils/i18next/LocalUtil'; import { renderNameField, renderScheduleField, diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/ServiceConfig/ConnectionConfigForm.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/ServiceConfig/ConnectionConfigForm.test.tsx index 84c5b9402ae3..20e864d9ed8a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/ServiceConfig/ConnectionConfigForm.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/ServiceConfig/ConnectionConfigForm.test.tsx @@ -10,13 +10,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { act, fireEvent, render, screen } from '@testing-library/react'; -import { forwardRef } from 'react'; +import { fireEvent, render, screen } from '@testing-library/react'; +import { act, forwardRef } from 'react'; import { LOADING_STATE } from '../../../../enums/common.enum'; import { ServiceCategory } from '../../../../enums/service.enum'; import { MOCK_ATHENA_SERVICE } from '../../../../mocks/Service.mock'; import { getPipelineServiceHostIp } from '../../../../rest/ingestionPipelineAPI'; -import * as CommonUtils from '../../../../utils/CommonUtils'; +import * as LocalUtils from '../../../../utils/i18next/LocalUtil'; import { formatFormDataForSubmit } from '../../../../utils/JSONSchemaFormUtils'; import { getConnectionSchemas } from '../../../../utils/ServiceConnectionUtils'; import ConnectionConfigForm from './ConnectionConfigForm'; @@ -140,10 +140,6 @@ jest.mock('../../../common/AirflowMessageBanner/AirflowMessageBanner', () => { ); }); -jest.mock('../../../../utils/CommonUtils', () => ({ - Transi18next: jest.fn().mockReturnValue('message.airflow-host-ip-address'), -})); - jest.mock('../../../../utils/BrandData/BrandClassBase', () => ({ __esModule: true, default: { @@ -216,6 +212,12 @@ const mockProps = { }; describe('ServiceConfig', () => { + beforeEach(() => { + jest + .spyOn(LocalUtils, 'Transi18next') + .mockImplementation(() => <>message.airflow-host-ip-address); + }); + it('should render Service Config', async () => { render(); @@ -305,9 +307,7 @@ describe('ServiceConfig', () => {
)); - jest - .spyOn(CommonUtils, 'Transi18next') - .mockImplementation(mockTransi18next); + jest.spyOn(LocalUtils, 'Transi18next').mockImplementation(mockTransi18next); await act(async () => { render(); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/ServiceConfig/ConnectionConfigForm.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/ServiceConfig/ConnectionConfigForm.tsx index 4a4c7143ae2c..c95b5e11baee 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/ServiceConfig/ConnectionConfigForm.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/ServiceConfig/ConnectionConfigForm.tsx @@ -30,8 +30,7 @@ import { useApplicationStore } from '../../../../hooks/useApplicationStore'; import { ConfigData } from '../../../../interface/service.interface'; import { getPipelineServiceHostIp } from '../../../../rest/ingestionPipelineAPI'; import brandClassBase from '../../../../utils/BrandData/BrandClassBase'; -import { Transi18next } from '../../../../utils/CommonUtils'; -import i18n from '../../../../utils/i18next/LocalUtil'; +import i18n, { Transi18next } from '../../../../utils/i18next/LocalUtil'; import { formatFormDataForSubmit } from '../../../../utils/JSONSchemaFormUtils'; import { getConnectionSchemas, diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Team/TeamDetails/TeamDetailsV1.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Team/TeamDetails/TeamDetailsV1.tsx index 057e851f82cd..bb3a69934365 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Team/TeamDetails/TeamDetailsV1.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Team/TeamDetails/TeamDetailsV1.tsx @@ -68,13 +68,13 @@ import AddAttributeModal from '../../../../pages/RolesPage/AddAttributeModal/Add import { ImportType } from '../../../../pages/TeamsPage/ImportTeamsPage/ImportTeamsPage.interface'; import { searchQuery } from '../../../../rest/searchAPI'; import { exportTeam, restoreTeam } from '../../../../rest/teamsAPI'; -import { Transi18next } from '../../../../utils/CommonUtils'; import { getEntityName } from '../../../../utils/EntityUtils'; import { EXTENSION_POINTS, TabContribution, } from '../../../../utils/ExtensionPointTypes'; import { getSettingPageEntityBreadCrumb } from '../../../../utils/GlobalSettingsUtils'; +import { Transi18next } from '../../../../utils/i18next/LocalUtil'; import { getSettingsPathWithFqn, getTeamsWithFqnPath, diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Team/TeamDetails/TeamHierarchy.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Team/TeamDetails/TeamHierarchy.tsx index c45f34549ddb..1843a1405a3c 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Team/TeamDetails/TeamHierarchy.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Team/TeamDetails/TeamHierarchy.tsx @@ -25,8 +25,8 @@ import { TabSpecificField } from '../../../../enums/entity.enum'; import { Team } from '../../../../generated/entity/teams/team'; import { Include } from '../../../../generated/type/include'; import { getTeamByName, patchTeamDetail } from '../../../../rest/teamsAPI'; -import { Transi18next } from '../../../../utils/CommonUtils'; import { getEntityName } from '../../../../utils/EntityUtils'; +import { Transi18next } from '../../../../utils/i18next/LocalUtil'; import { descriptionTableObject } from '../../../../utils/TableColumn.util'; import { getTableExpandableConfig } from '../../../../utils/TableUtils'; import { isDropRestricted } from '../../../../utils/TeamUtils'; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/UploadFile/UploadFile.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/UploadFile/UploadFile.test.tsx index d2e5117f1a3a..39e61c001487 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/UploadFile/UploadFile.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/UploadFile/UploadFile.test.tsx @@ -28,12 +28,6 @@ jest.mock('../../utils/ToastUtils', () => ({ showErrorToast: jest.fn(), })); -jest.mock('../../utils/CommonUtils', () => ({ - Transi18next: jest - .fn() - .mockReturnValue('message.drag-and-drop-or-browse-csv-files-here'), -})); - describe('UploadFile Component', () => { const defaultProps: UploadFileProps = { fileType: '.csv', @@ -44,13 +38,13 @@ describe('UploadFile Component', () => { jest.clearAllMocks(); }); - it('should render the upload component with correct props', () => { + it('should render the upload component with correct props', async () => { render(); expect(screen.getByTestId('upload-file-widget')).toBeInTheDocument(); expect(screen.getByTestId('import-icon')).toBeInTheDocument(); expect( - screen.getByText('message.drag-and-drop-or-browse-csv-files-here') + await screen.findByText(/message.drag-and-drop-or-browse-csv-files-here/) ).toBeInTheDocument(); }); @@ -204,11 +198,11 @@ describe('UploadFile Component', () => { }); }); - it('should render browse text correctly', () => { + it('should render browse text correctly', async () => { render(); expect( - screen.getByText('message.drag-and-drop-or-browse-csv-files-here') + await screen.findByText(/message.drag-and-drop-or-browse-csv-files-here/) ).toBeInTheDocument(); }); }); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/UploadFile/UploadFile.tsx b/openmetadata-ui/src/main/resources/ui/src/components/UploadFile/UploadFile.tsx index 1d675a554d0f..1ceb7eeeaf2c 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/UploadFile/UploadFile.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/UploadFile/UploadFile.tsx @@ -17,7 +17,7 @@ import type { UploadRequestOption } from 'rc-upload/lib/interface'; import { FC, useCallback, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { ReactComponent as ImportIcon } from '../../assets/svg/ic-drag-drop.svg'; -import { Transi18next } from '../../utils/CommonUtils'; +import { Transi18next } from '../../utils/i18next/LocalUtil'; import { showErrorToast } from '../../utils/ToastUtils'; import Loader from '../common/Loader/Loader'; import './upload-file.less'; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/CustomPropertyTable/CustomPropertyTable.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/CustomPropertyTable/CustomPropertyTable.test.tsx index bef41340f168..ce489ed09450 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/CustomPropertyTable/CustomPropertyTable.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/CustomPropertyTable/CustomPropertyTable.test.tsx @@ -12,11 +12,11 @@ */ import { - act, render, screen, waitForElementToBeRemoved, } from '@testing-library/react'; +import { act } from 'react'; import { EntityType } from '../../../enums/entity.enum'; import { Table } from '../../../generated/entity/data/table'; import { getTypeByFQN } from '../../../rest/metadataTypeAPI'; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/CustomPropertyTable/CustomPropertyTable.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/CustomPropertyTable/CustomPropertyTable.tsx index 14b1c46e3175..83b359b316ea 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/CustomPropertyTable/CustomPropertyTable.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/CustomPropertyTable/CustomPropertyTable.tsx @@ -26,13 +26,13 @@ import { DetailPageWidgetKeys } from '../../../enums/CustomizeDetailPage.enum'; import { EntityTabs } from '../../../enums/entity.enum'; import { ChangeDescription, Type } from '../../../generated/entity/type'; import { getTypeByFQN } from '../../../rest/metadataTypeAPI'; -import { Transi18next } from '../../../utils/CommonUtils'; import entityUtilClassBase from '../../../utils/EntityUtilClassBase'; import { getChangedEntityNewValue, getDiffByFieldName, getUpdatedExtensionDiffFields, } from '../../../utils/EntityVersionUtils'; +import { Transi18next } from '../../../utils/i18next/LocalUtil'; import { showErrorToast } from '../../../utils/ToastUtils'; import { useGenericContext } from '../../Customization/GenericProvider/GenericProvider'; import ErrorPlaceHolder from '../ErrorWithPlaceholder/ErrorPlaceHolder'; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/DeleteWidget/DeleteWidgetModal.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/DeleteWidget/DeleteWidgetModal.test.tsx index 6bf00eb563c7..07b8eb24d1e0 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/DeleteWidget/DeleteWidgetModal.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/DeleteWidget/DeleteWidgetModal.test.tsx @@ -13,6 +13,7 @@ import { act, fireEvent, render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; +import { ReactNode } from 'react'; import { EntityType } from '../../../enums/entity.enum'; import { mockUserData } from '../../../mocks/MyDataPage.mock'; import { DeleteWidgetModalProps } from './DeleteWidget.interface'; @@ -36,11 +37,6 @@ const mockPropsUser: DeleteWidgetModalProps = { const mockOnLogoutHandler = jest.fn(); -jest.mock('lodash', () => ({ - ...jest.requireActual('lodash'), - startCase: jest.fn(), -})); - jest.mock('../../../rest/miscAPI', () => ({ deleteEntity: jest.fn().mockImplementation(() => Promise.resolve({ @@ -65,6 +61,11 @@ jest.mock('../../../utils/ToastUtils', () => ({ showSuccessToast: jest.fn(), })); +jest.mock('../../../utils/i18next/LocalUtil', () => ({ + Transi18next: ({ children }: { children: ReactNode }) => children, + t: jest.fn().mockImplementation((key: string) => key), +})); + describe('Test DeleteWidgetV1 Component', () => { it('Component should render properly', async () => { render(); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/DeleteWidget/DeleteWidgetModal.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/DeleteWidget/DeleteWidgetModal.tsx index b04ee82469d5..4c7545304bbe 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/DeleteWidget/DeleteWidgetModal.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/DeleteWidget/DeleteWidgetModal.tsx @@ -36,8 +36,8 @@ import { useAsyncDeleteProvider } from '../../../context/AsyncDeleteProvider/Asy import { EntityType } from '../../../enums/entity.enum'; import { useApplicationStore } from '../../../hooks/useApplicationStore'; import { deleteEntity } from '../../../rest/miscAPI'; -import { Transi18next } from '../../../utils/CommonUtils'; import deleteWidgetClassBase from '../../../utils/DeleteWidget/DeleteWidgetClassBase'; +import { Transi18next } from '../../../utils/i18next/LocalUtil'; import { showErrorToast, showSuccessToast } from '../../../utils/ToastUtils'; import { useAuthProvider } from '../../Auth/AuthProviders/AuthProvider'; import './delete-widget-modal.style.less'; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/ErrorBoundary/ErrorFallback.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/ErrorBoundary/ErrorFallback.tsx index 9fae684b4927..458fb33992a8 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/ErrorBoundary/ErrorFallback.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/ErrorBoundary/ErrorFallback.tsx @@ -27,7 +27,13 @@ const ErrorFallback: React.FC = ({ }) => { const navigate = useNavigate(); - const isChunkLoadError = error.message?.startsWith('Loading chunk'); + const isChunkLoadError = + error?.name === 'ChunkLoadError' || + error.message?.startsWith('Loading chunk') || // Legacy Webpack + error.message + ?.toLowerCase() + .includes('failed to fetch dynamically imported module') || // Vite + error.message?.toLowerCase().includes('importing a module script failed'); // Vite (Safari) const message = isChunkLoadError ? t('message.please-refresh-the-page') diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/ErrorWithPlaceholder/CreateErrorPlaceHolder.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/ErrorWithPlaceholder/CreateErrorPlaceHolder.tsx index c18ba89a8db4..b519c13bf53d 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/ErrorWithPlaceholder/CreateErrorPlaceHolder.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/ErrorWithPlaceholder/CreateErrorPlaceHolder.tsx @@ -17,7 +17,7 @@ import classNames from 'classnames'; import { useTranslation } from 'react-i18next'; import { ReactComponent as AddPlaceHolderIcon } from '../../../assets/svg/add-placeholder.svg'; import { useApplicationStore } from '../../../hooks/useApplicationStore'; -import { Transi18next } from '../../../utils/CommonUtils'; +import { Transi18next } from '../../../utils/i18next/LocalUtil'; import PermissionErrorPlaceholder from './PermissionErrorPlaceholder'; import { CreatePlaceholderProps } from './placeholder.interface'; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/ErrorWithPlaceholder/ErrorPlaceHolderES.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/ErrorWithPlaceholder/ErrorPlaceHolderES.test.tsx index 32b21017c789..e443809b4592 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/ErrorWithPlaceholder/ErrorPlaceHolderES.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/ErrorWithPlaceholder/ErrorPlaceHolderES.test.tsx @@ -41,6 +41,17 @@ jest.mock('../../../utils/BrandData/BrandClassBase', () => ({ }, })); +jest.mock('../../../utils/i18next/LocalUtil', () => ({ + Transi18next: jest.fn().mockImplementation(({ i18nKey }) => { + return {i18nKey}; + }), + __esModule: true, + default: { + t: jest.fn().mockImplementation((key) => key), + }, + t: jest.fn().mockImplementation((key) => key), +})); + const mockErrorMessage = 'An exception with message [Elasticsearch exception [type=index_not_found_exception, reason=no such index [test_search_index]]] was thrown while processing request.'; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/ErrorWithPlaceholder/ErrorPlaceHolderES.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/ErrorWithPlaceholder/ErrorPlaceHolderES.tsx index 954c49da6b74..f35f26d139d0 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/ErrorWithPlaceholder/ErrorPlaceHolderES.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/ErrorWithPlaceholder/ErrorPlaceHolderES.tsx @@ -34,8 +34,7 @@ import { import { useApplicationStore } from '../../../hooks/useApplicationStore'; import { useDomainStore } from '../../../hooks/useDomainStore'; import brandClassBase from '../../../utils/BrandData/BrandClassBase'; -import { Transi18next } from '../../../utils/CommonUtils'; -import i18n from '../../../utils/i18next/LocalUtil'; +import i18n, { Transi18next } from '../../../utils/i18next/LocalUtil'; import { useRequiredParams } from '../../../utils/useRequiredParams'; import ErrorPlaceHolder from './ErrorPlaceHolder'; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/ErrorWithPlaceholder/FilterErrorPlaceHolder.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/ErrorWithPlaceholder/FilterErrorPlaceHolder.tsx index 1816c3d6cf2a..70a3661302d7 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/ErrorWithPlaceholder/FilterErrorPlaceHolder.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/ErrorWithPlaceholder/FilterErrorPlaceHolder.tsx @@ -16,7 +16,7 @@ import classNames from 'classnames'; import { useTranslation } from 'react-i18next'; import { ReactComponent as FilterPlaceHolderIcon } from '../../../assets/svg/no-search-placeholder.svg'; import { useApplicationStore } from '../../../hooks/useApplicationStore'; -import { Transi18next } from '../../../utils/CommonUtils'; +import { Transi18next } from '../../../utils/i18next/LocalUtil'; import { FilterPlaceholderProps } from './placeholder.interface'; const FilterErrorPlaceHolder = ({ diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/ErrorWithPlaceholder/PermissionErrorPlaceholder.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/ErrorWithPlaceholder/PermissionErrorPlaceholder.tsx index 0234b2360af8..04bc36cc4077 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/ErrorWithPlaceholder/PermissionErrorPlaceholder.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/ErrorWithPlaceholder/PermissionErrorPlaceholder.tsx @@ -14,7 +14,7 @@ import { Space, Typography } from 'antd'; import classNames from 'classnames'; import { ReactComponent as NoAccessPlaceHolderIcon } from '../../../assets/svg/add-placeholder.svg'; import { SIZE } from '../../../enums/common.enum'; -import { Transi18next } from '../../../utils/CommonUtils'; +import { Transi18next } from '../../../utils/i18next/LocalUtil'; import { PermissionPlaceholderProps } from './placeholder.interface'; const PermissionErrorPlaceholder = ({ diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/Form/JSONSchema/JsonSchemaWidgets/QueryBuilderWidget/QueryBuilderWidget.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/Form/JSONSchema/JsonSchemaWidgets/QueryBuilderWidget/QueryBuilderWidget.test.tsx index ba1e4995f9b7..b15cb8a2f644 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/Form/JSONSchema/JsonSchemaWidgets/QueryBuilderWidget/QueryBuilderWidget.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/Form/JSONSchema/JsonSchemaWidgets/QueryBuilderWidget/QueryBuilderWidget.test.tsx @@ -10,7 +10,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { AntdConfig, BasicConfig } from '@react-awesome-query-builder/antd'; +import { AntdConfig } from '@react-awesome-query-builder/antd'; import { Registry } from '@rjsf/utils'; import { render, screen } from '@testing-library/react'; import React from 'react'; @@ -19,7 +19,7 @@ import QueryBuilderWidget from './QueryBuilderWidget'; const mockOnFocus = jest.fn(); const mockOnBlur = jest.fn(); const mockOnChange = jest.fn(); -const baseConfig = AntdConfig as BasicConfig; +const baseConfig = AntdConfig; jest.mock( '../../../../../Explore/AdvanceSearchProvider/AdvanceSearchProvider.component', diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/Form/JSONSchema/JsonSchemaWidgets/QueryBuilderWidget/QueryBuilderWidget.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/Form/JSONSchema/JsonSchemaWidgets/QueryBuilderWidget/QueryBuilderWidget.tsx index e919f8060cd0..ea361ba56d19 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/Form/JSONSchema/JsonSchemaWidgets/QueryBuilderWidget/QueryBuilderWidget.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/Form/JSONSchema/JsonSchemaWidgets/QueryBuilderWidget/QueryBuilderWidget.tsx @@ -11,6 +11,14 @@ * limitations under the License. */ import { InfoCircleOutlined } from '@ant-design/icons'; +import { + Actions, + Builder, + Config, + ImmutableTree, + Query, + Utils as QbUtils, +} from '@react-awesome-query-builder/antd'; import { WidgetProps } from '@rjsf/utils'; import { Alert, @@ -23,19 +31,8 @@ import { Typography, } from 'antd'; import classNames from 'classnames'; -import { useEffect } from 'react'; - -import { - Actions, - Builder, - Config, - ImmutableTree, - Query, - Utils as QbUtils, -} from '@react-awesome-query-builder/antd'; -import 'antd/dist/antd.css'; import { debounce, isEmpty, isUndefined } from 'lodash'; -import { FC, useCallback, useMemo, useState } from 'react'; +import { FC, useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { EntityType } from '../../../../../../enums/entity.enum'; import { SearchIndex } from '../../../../../../enums/search.enum'; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/TestConnection/TestConnection.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/TestConnection/TestConnection.test.tsx index 061422086808..5c3088f42542 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/TestConnection/TestConnection.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/TestConnection/TestConnection.test.tsx @@ -11,12 +11,12 @@ * limitations under the License. */ import { - act, fireEvent, render, screen, waitForElementToBeRemoved, } from '@testing-library/react'; +import { act } from 'react'; import { useAirflowStatus } from '../../../context/AirflowStatusProvider/AirflowStatusProvider'; import { ServiceCategory } from '../../../enums/service.enum'; import { ConfigData } from '../../../interface/service.interface'; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/TestConnection/TestConnection.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/TestConnection/TestConnection.tsx index 34526d993cde..d43e840e96ac 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/TestConnection/TestConnection.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/TestConnection/TestConnection.tsx @@ -50,7 +50,7 @@ import { getWorkflowById, triggerWorkflowById, } from '../../../rest/workflowAPI'; -import { Transi18next } from '../../../utils/CommonUtils'; +import { Transi18next } from '../../../utils/i18next/LocalUtil'; import { formatFormDataForSubmit } from '../../../utils/JSONSchemaFormUtils'; import { getServiceType, diff --git a/openmetadata-ui/src/main/resources/ui/src/constants/LoginClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/constants/LoginClassBase.ts index e1a6d6992fb7..add5ef1bbcc1 100644 --- a/openmetadata-ui/src/main/resources/ui/src/constants/LoginClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/constants/LoginClassBase.ts @@ -10,6 +10,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import dataCollaborationImg from '../assets/img/login-screen/data-collaboration/data-collaboration.png'; import discoveryImg from '../assets/img/login-screen/discovery/data-discovery.png'; import governanceImg from '../assets/img/login-screen/governance/governance.png'; diff --git a/openmetadata-ui/src/main/resources/ui/src/constants/ServiceType.constant.ts b/openmetadata-ui/src/main/resources/ui/src/constants/ServiceType.constant.ts new file mode 100644 index 000000000000..c378e8ff53fd --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/constants/ServiceType.constant.ts @@ -0,0 +1,303 @@ +/* + * Copyright 2022 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 { map, startCase } from 'lodash'; +import { ServiceTypes, StepperStepType } from 'Models'; +import { EntityType } from '../enums/entity.enum'; +import { ServiceCategory } from '../enums/service.enum'; +import { PipelineType } from '../generated/api/services/ingestionPipelines/createIngestionPipeline'; +import { WorkflowStatus } from '../generated/entity/automations/workflow'; +import { StorageServiceType } from '../generated/entity/data/container'; +import { APIServiceType } from '../generated/entity/services/apiService'; +import { DashboardServiceType } from '../generated/entity/services/dashboardService'; +import { DatabaseServiceType } from '../generated/entity/services/databaseService'; +import { DriveServiceType } from '../generated/entity/services/driveService'; +import { MessagingServiceType } from '../generated/entity/services/messagingService'; +import { MetadataServiceType } from '../generated/entity/services/metadataService'; +import { MlModelServiceType } from '../generated/entity/services/mlmodelService'; +import { PipelineServiceType } from '../generated/entity/services/pipelineService'; +import { SearchServiceType } from '../generated/entity/services/searchService'; +import { Type as SecurityServiceType } from '../generated/entity/services/securityService'; +import { ServiceType } from '../generated/entity/services/serviceType'; + +export const OPEN_METADATA = 'OpenMetadata'; +export const JWT_CONFIG = 'openMetadataJWTClientConfig'; + +export const excludedService = [ + MlModelServiceType.Sklearn, + MetadataServiceType.MetadataES, + MetadataServiceType.OpenMetadata, + PipelineServiceType.Spark, +]; + +export const arrServiceTypes: Array = [ + 'databaseServices', + 'messagingServices', + 'dashboardServices', + 'pipelineServices', + 'mlmodelServices', + 'storageServices', + 'apiServices', + 'securityServices', + 'driveServices', +]; + +export const SERVICE_CATEGORY: { [key: string]: ServiceCategory } = { + databases: ServiceCategory.DATABASE_SERVICES, + messaging: ServiceCategory.MESSAGING_SERVICES, + dashboards: ServiceCategory.DASHBOARD_SERVICES, + pipelines: ServiceCategory.PIPELINE_SERVICES, + mlmodels: ServiceCategory.ML_MODEL_SERVICES, + metadata: ServiceCategory.METADATA_SERVICES, + storages: ServiceCategory.STORAGE_SERVICES, + search: ServiceCategory.SEARCH_SERVICES, + apiServices: ServiceCategory.API_SERVICES, + security: ServiceCategory.SECURITY_SERVICES, + drives: ServiceCategory.DRIVE_SERVICES, +}; + +export const servicesDisplayName: Record< + string, + { key: string; entity: string } +> = { + databaseServices: { key: 'label.entity-service', entity: 'label.database' }, + messagingServices: { key: 'label.entity-service', entity: 'label.messaging' }, + dashboardServices: { key: 'label.entity-service', entity: 'label.dashboard' }, + pipelineServices: { key: 'label.entity-service', entity: 'label.pipeline' }, + mlmodelServices: { key: 'label.entity-service', entity: 'label.ml-model' }, + metadataServices: { key: 'label.entity-service', entity: 'label.metadata' }, + storageServices: { key: 'label.entity-service', entity: 'label.storage' }, + searchServices: { key: 'label.entity-service', entity: 'label.search' }, + dashboardDataModel: { + key: 'label.entity-service', + entity: 'label.data-model', + }, + apiServices: { key: 'label.entity-service', entity: 'label.api-uppercase' }, + securityServices: { key: 'label.entity-service', entity: 'label.security' }, + driveServices: { key: 'label.entity-service', entity: 'label.drive' }, +}; + +export const SERVICE_CATEGORY_OPTIONS = map(ServiceCategory, (value) => ({ + label: startCase(value), + value, +})); + +export const STEPS_FOR_ADD_SERVICE: Array = [ + { + name: 'label.select-field', + nameData: { field: 'label.service-type' }, + step: 1, + }, + { + name: 'label.configure-entity', + nameData: { entity: 'label.service' }, + step: 2, + }, + { + name: 'label.connection-entity', + nameData: { entity: 'label.detail-plural' }, + step: 3, + }, + { + name: 'label.set-default-filters', + step: 4, + }, +]; + +export const STEPS_FOR_EDIT_SERVICE: Array = [ + { + name: 'label.connection-entity', + nameData: { entity: 'label.detail-plural' }, + step: 1, + }, + { + name: 'label.set-default-filters', + step: 2, + }, +]; + +export const SERVICE_DEFAULT_ERROR_MAP = { + serviceType: false, +}; + +export const FETCHING_EXPIRY_TIME = 3 * 60 * 1000; +export const FETCH_INTERVAL = 2000; + +export const WORKFLOW_COMPLETE_STATUS = [ + WorkflowStatus.Failed, + WorkflowStatus.Successful, +]; + +export const TEST_CONNECTION_PROGRESS_PERCENTAGE = { + ZERO: 0, + ONE: 1, + TEN: 10, + TWENTY: 20, + FORTY: 40, + HUNDRED: 100, +}; + +export const SERVICE_TYPE_MAP = { + [ServiceCategory.DASHBOARD_SERVICES]: ServiceType.Dashboard, + [ServiceCategory.DATABASE_SERVICES]: ServiceType.Database, + [ServiceCategory.MESSAGING_SERVICES]: ServiceType.Messaging, + [ServiceCategory.ML_MODEL_SERVICES]: ServiceType.MlModel, + [ServiceCategory.METADATA_SERVICES]: ServiceType.Metadata, + [ServiceCategory.STORAGE_SERVICES]: ServiceType.Storage, + [ServiceCategory.PIPELINE_SERVICES]: ServiceType.Pipeline, + [ServiceCategory.SEARCH_SERVICES]: ServiceType.Search, + [ServiceCategory.API_SERVICES]: ServiceType.API, + [ServiceCategory.SECURITY_SERVICES]: ServiceType.Security, + [ServiceCategory.DRIVE_SERVICES]: ServiceType.Drive, +}; + +export const SERVICE_TYPES_ENUM = { + [ServiceCategory.DASHBOARD_SERVICES]: DashboardServiceType, + [ServiceCategory.DATABASE_SERVICES]: DatabaseServiceType, + [ServiceCategory.MESSAGING_SERVICES]: MessagingServiceType, + [ServiceCategory.ML_MODEL_SERVICES]: MlModelServiceType, + [ServiceCategory.METADATA_SERVICES]: MetadataServiceType, + [ServiceCategory.STORAGE_SERVICES]: StorageServiceType, + [ServiceCategory.PIPELINE_SERVICES]: PipelineServiceType, + [ServiceCategory.SEARCH_SERVICES]: SearchServiceType, + [ServiceCategory.API_SERVICES]: APIServiceType, + [ServiceCategory.SECURITY_SERVICES]: SecurityServiceType, + [ServiceCategory.DRIVE_SERVICES]: DriveServiceType, +}; + +export const BETA_SERVICES = [ + PipelineServiceType.OpenLineage, + PipelineServiceType.Wherescape, + DatabaseServiceType.Cassandra, + MetadataServiceType.AlationSink, + DatabaseServiceType.Cockroach, + SearchServiceType.OpenSearch, + PipelineServiceType.Ssis, + DatabaseServiceType.Ssas, + DashboardServiceType.ThoughtSpot, + SecurityServiceType.Ranger, + DatabaseServiceType.Epic, + DashboardServiceType.Grafana, + DashboardServiceType.Hex, + DatabaseServiceType.ServiceNow, + DatabaseServiceType.Timescale, + DatabaseServiceType.Dremio, + MetadataServiceType.Collibra, + PipelineServiceType.Mulesoft, + DatabaseServiceType.MicrosoftFabric, + PipelineServiceType.MicrosoftFabricPipeline, + DatabaseServiceType.BurstIQ, + DatabaseServiceType.StarRocks, + DriveServiceType.SFTP, + DatabaseServiceType.Informix, + DatabaseServiceType.MicrosoftAccess, +]; + +export const TEST_CONNECTION_INITIAL_MESSAGE = + 'message.test-your-connection-before-creating-service'; + +export const TEST_CONNECTION_SUCCESS_MESSAGE = + 'message.connection-test-successful'; + +export const TEST_CONNECTION_FAILURE_MESSAGE = 'message.connection-test-failed'; + +export const TEST_CONNECTION_TESTING_MESSAGE = + 'message.testing-your-connection-may-take-two-minutes'; + +export const TEST_CONNECTION_WARNING_MESSAGE = + 'message.connection-test-warning'; + +export const ADVANCED_PROPERTIES = [ + 'connectionArguments', + 'connectionOptions', + 'scheme', + 'sampleDataStorageConfig', + 'computeTableMetrics', + 'computeColumnMetrics', + 'includeViews', + 'useStatistics', + 'confidence', + 'profileSampleConfig', + 'randomizedSample', + 'sampleDataCount', + 'threadCount', + 'timeoutSeconds', + 'metrics', + 'sslConfig', + 'sslMode', + 'schemaRegistrySSL', + 'consumerConfigSSL', + 'verify', + 'useNonce', + 'disablePkce', + 'maxClockSkew', + 'tokenValidity', + 'maxAge', + 'sessionExpiry', +]; + +export const PIPELINE_SERVICE_PLATFORM = 'Airflow'; + +export const SERVICE_TYPES = [ + EntityType.DATABASE_SERVICE, + EntityType.DASHBOARD_SERVICE, + EntityType.MESSAGING_SERVICE, + EntityType.PIPELINE_SERVICE, + EntityType.MLMODEL_SERVICE, + EntityType.METADATA_SERVICE, + EntityType.STORAGE_SERVICE, + EntityType.SEARCH_SERVICE, + EntityType.API_SERVICE, + EntityType.SECURITY_SERVICE, + EntityType.DRIVE_SERVICE, +]; + +export const EXCLUDE_AUTO_PILOT_SERVICE_TYPES = [EntityType.SECURITY_SERVICE]; + +export const SERVICE_INGESTION_PIPELINE_TYPES = [ + PipelineType.Metadata, + PipelineType.Usage, + PipelineType.Lineage, + PipelineType.Profiler, + PipelineType.AutoClassification, + PipelineType.Dbt, +]; + +export const SERVICE_AUTOPILOT_AGENT_TYPES = [ + PipelineType.Metadata, + PipelineType.Lineage, + PipelineType.Usage, + PipelineType.AutoClassification, + PipelineType.Profiler, +]; + +export const SERVICE_TYPE_WITH_DISPLAY_NAME = new Map([ + [PipelineServiceType.GluePipeline, 'Glue Pipeline'], + [DatabaseServiceType.DomoDatabase, 'Domo Database'], + [DashboardServiceType.DomoDashboard, 'Domo Dashboard'], + [DashboardServiceType.MicroStrategy, 'Micro Strategy'], + [DashboardServiceType.PowerBIReportServer, 'PowerBI Report Server'], + [PipelineServiceType.DatabricksPipeline, 'Databricks Pipeline'], + [PipelineServiceType.DomoPipeline, 'Domo Pipeline'], + [PipelineServiceType.KafkaConnect, 'Kafka Connect'], + [DatabaseServiceType.SapERP, 'SAP ERP'], + [DatabaseServiceType.SapHana, 'SAP HANA'], + [DatabaseServiceType.UnityCatalog, 'Unity Catalog'], + [PipelineServiceType.DataFactory, 'Data Factory'], + [PipelineServiceType.DBTCloud, 'DBT Cloud'], + [PipelineServiceType.OpenLineage, 'Open Lineage'], + [MetadataServiceType.AlationSink, 'Alation Sink'], + [SearchServiceType.ElasticSearch, 'Elasticsearch'], + [DatabaseServiceType.MicrosoftFabric, 'Microsoft Fabric'], + [PipelineServiceType.MicrosoftFabricPipeline, 'Microsoft Fabric Pipeline'], +]); diff --git a/openmetadata-ui/src/main/resources/ui/src/constants/ServiceUISchema.constant.ts b/openmetadata-ui/src/main/resources/ui/src/constants/ServiceUISchema.constant.ts new file mode 100644 index 000000000000..5bc2a5ddfe85 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/constants/ServiceUISchema.constant.ts @@ -0,0 +1,79 @@ +/* + * Copyright 2022 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 { ServiceNestedConnectionFields } from '../enums/service.enum'; +import { SERVICE_FILTER_PATTERN_FIELDS } from './ServiceConnection.constants'; + +export const DEF_UI_SCHEMA = { + supportsIncrementalMetadataExtraction: { + 'ui:widget': 'hidden', + 'ui:hideError': true, + }, + supportsMetadataExtraction: { 'ui:widget': 'hidden', 'ui:hideError': true }, + supportsSystemProfile: { 'ui:widget': 'hidden', 'ui:hideError': true }, + supportsDataDiff: { 'ui:widget': 'hidden', 'ui:hideError': true }, + supportsUsageExtraction: { 'ui:widget': 'hidden', 'ui:hideError': true }, + supportsLineageExtraction: { 'ui:widget': 'hidden', 'ui:hideError': true }, + supportsViewLineageExtraction: { + 'ui:widget': 'hidden', + 'ui:hideError': true, + }, + supportsProfiler: { 'ui:widget': 'hidden', 'ui:hideError': true }, + supportsDatabase: { 'ui:widget': 'hidden', 'ui:hideError': true }, + supportsQueryComment: { 'ui:widget': 'hidden', 'ui:hideError': true }, + supportsDBTExtraction: { 'ui:widget': 'hidden', 'ui:hideError': true }, + type: { 'ui:widget': 'hidden' }, +}; + +export const INGESTION_ELASTIC_SEARCH_WORKFLOW_UI_SCHEMA = { + useSSL: { 'ui:widget': 'hidden', 'ui:hideError': true }, + verifyCerts: { 'ui:widget': 'hidden', 'ui:hideError': true }, + timeout: { 'ui:widget': 'hidden', 'ui:hideError': true }, + caCerts: { 'ui:widget': 'hidden', 'ui:hideError': true }, + useAwsCredentials: { 'ui:widget': 'hidden', 'ui:hideError': true }, + regionName: { 'ui:widget': 'hidden', 'ui:hideError': true }, +}; + +export const INGESTION_WORKFLOW_UI_SCHEMA = { + type: { 'ui:widget': 'hidden', 'ui:hideError': true }, + name: { 'ui:widget': 'hidden', 'ui:hideError': true }, + processingEngine: { 'ui:widget': 'hidden', 'ui:hideError': true }, + 'ui:order': [ + 'rootProcessingEngine', + 'name', + 'displayName', + ...SERVICE_FILTER_PATTERN_FIELDS, + 'enableDebugLog', + '*', + ], +}; + +export const EXCLUDE_INCREMENTAL_EXTRACTION_SUPPORT_UI_SCHEMA = { + incremental: { + 'ui:widget': 'hidden', + 'ui:hideError': true, + }, +}; + +export const COMMON_UI_SCHEMA = { + ...DEF_UI_SCHEMA, + [ServiceNestedConnectionFields.CONNECTION]: { + ...DEF_UI_SCHEMA, + }, + [ServiceNestedConnectionFields.METASTORE_CONNECTION]: { + ...DEF_UI_SCHEMA, + }, + [ServiceNestedConnectionFields.DATABASE_CONNECTION]: { + ...DEF_UI_SCHEMA, + }, +}; diff --git a/openmetadata-ui/src/main/resources/ui/src/constants/Services.constant.ts b/openmetadata-ui/src/main/resources/ui/src/constants/Services.constant.ts index fc57abb3c710..3feb917bc017 100644 --- a/openmetadata-ui/src/main/resources/ui/src/constants/Services.constant.ts +++ b/openmetadata-ui/src/main/resources/ui/src/constants/Services.constant.ts @@ -11,560 +11,21 @@ * limitations under the License. */ -import { map, startCase } from 'lodash'; -import { ServiceTypes, StepperStepType } from 'Models'; -import airbyte from '../assets/img/Airbyte.png'; -import airflow from '../assets/img/service-icon-airflow.png'; -import alationsink from '../assets/img/service-icon-alation-sink.png'; -import amazonS3 from '../assets/img/service-icon-amazon-s3.svg'; -import amundsen from '../assets/img/service-icon-amundsen.png'; -import athena from '../assets/img/service-icon-athena.png'; -import atlas from '../assets/img/service-icon-atlas.svg'; -import azuresql from '../assets/img/service-icon-azuresql.png'; -import bigtable from '../assets/img/service-icon-bigtable.png'; -import burstiq from '../assets/img/service-icon-burstiq.png'; -import cassandra from '../assets/img/service-icon-cassandra.png'; -import clickhouse from '../assets/img/service-icon-clickhouse.png'; -import cockroach from '../assets/img/service-icon-cockroach.png'; -import couchbase from '../assets/img/service-icon-couchbase.svg'; -import dagster from '../assets/img/service-icon-dagster.png'; -import databrick from '../assets/img/service-icon-databrick.png'; -import datalake from '../assets/img/service-icon-datalake.png'; -import dbt from '../assets/img/service-icon-dbt.png'; -import deltalake from '../assets/img/service-icon-delta-lake.png'; -import domo from '../assets/img/service-icon-domo.png'; -import doris from '../assets/img/service-icon-doris.png'; -import druid from '../assets/img/service-icon-druid.png'; -import dynamodb from '../assets/img/service-icon-dynamodb.png'; -import exasol from '../assets/img/service-icon-exasol.png'; -import fivetran from '../assets/img/service-icon-fivetran.png'; -import flink from '../assets/img/service-icon-flink.png'; -import gcs from '../assets/img/service-icon-gcs.png'; -import glue from '../assets/img/service-icon-glue.png'; -import grafana from '../assets/img/service-icon-grafana.png'; -import greenplum from '../assets/img/service-icon-greenplum.png'; -import hive from '../assets/img/service-icon-hive.png'; -import ibmdb2 from '../assets/img/service-icon-ibmdb2.png'; -import impala from '../assets/img/service-icon-impala.png'; -import iomete from '../assets/img/service-icon-iomete.png'; -import kafka from '../assets/img/service-icon-kafka.png'; -import kinesis from '../assets/img/service-icon-kinesis.png'; -import lightDash from '../assets/img/service-icon-lightdash.png'; -import looker from '../assets/img/service-icon-looker.png'; -import mariadb from '../assets/img/service-icon-mariadb.png'; -import metabase from '../assets/img/service-icon-metabase.png'; -import microstrategy from '../assets/img/service-icon-microstrategy.svg'; -import mode from '../assets/img/service-icon-mode.png'; -import mongodb from '../assets/img/service-icon-mongodb.png'; -import mssql from '../assets/img/service-icon-mssql.png'; -import nifi from '../assets/img/service-icon-nifi.png'; -import openlineage from '../assets/img/service-icon-openlineage.svg'; -import oracle from '../assets/img/service-icon-oracle.png'; -import pinot from '../assets/img/service-icon-pinot.png'; -import postgres from '../assets/img/service-icon-post.png'; -import powerbi from '../assets/img/service-icon-power-bi.png'; -import presto from '../assets/img/service-icon-presto.png'; -import qlikSense from '../assets/img/service-icon-qlik-sense.png'; -import query from '../assets/img/service-icon-query.png'; -import quicksight from '../assets/img/service-icon-quicksight.png'; -import redash from '../assets/img/service-icon-redash.png'; -import redpanda from '../assets/img/service-icon-redpanda.png'; -import redshift from '../assets/img/service-icon-redshift.png'; -import sagemaker from '../assets/img/service-icon-sagemaker.png'; -import salesforce from '../assets/img/service-icon-salesforce.png'; -import sapErp from '../assets/img/service-icon-sap-erp.png'; -import sapHana from '../assets/img/service-icon-sap-hana.png'; -import sas from '../assets/img/service-icon-sas.svg'; -import scikit from '../assets/img/service-icon-scikit.png'; -import sigma from '../assets/img/service-icon-sigma.png'; -import singlestore from '../assets/img/service-icon-singlestore.png'; -import snowflakes from '../assets/img/service-icon-snowflakes.png'; -import spark from '../assets/img/service-icon-spark.png'; -import spline from '../assets/img/service-icon-spline.png'; -import mysql from '../assets/img/service-icon-sql.png'; -import sqlite from '../assets/img/service-icon-sqlite.png'; -import starrocks from '../assets/img/service-icon-starrocks.png'; -import superset from '../assets/img/service-icon-superset.png'; -import synapse from '../assets/img/service-icon-synapse.png'; -import tableau from '../assets/img/service-icon-tableau.png'; -import timescale from '../assets/img/service-icon-timescale.png'; -import trino from '../assets/img/service-icon-trino.png'; -import unitycatalog from '../assets/img/service-icon-unitycatalog.svg'; -import vertica from '../assets/img/service-icon-vertica.png'; -import dashboardDefault from '../assets/svg/dashboard.svg'; -import iconDefaultService from '../assets/svg/default-service-icon.svg'; -import elasticSearch from '../assets/svg/elasticsearch.svg'; -import databaseDefault from '../assets/svg/ic-custom-database.svg'; -import { default as customDriveDefault } from '../assets/svg/ic-custom-drive.svg'; -import mlModelDefault from '../assets/svg/ic-custom-model.svg'; -import searchDefault from '../assets/svg/ic-custom-search.svg'; -import { default as storageDefault } from '../assets/svg/ic-custom-storage.svg'; -import { default as driveDefault } from '../assets/svg/ic-drive-service.svg'; -import restService from '../assets/svg/ic-service-rest-api.svg'; -import logo from '../assets/svg/logo-monogram.svg'; -import openSearch from '../assets/svg/open-search.svg'; -import pipelineDefault from '../assets/svg/pipeline.svg'; -import securitySafe from '../assets/svg/security-safe.svg'; -import googleDrive from '../assets/svg/service-icon-google-drive.svg'; -import hex from '../assets/svg/service-icon-hex.svg'; -import mlflow from '../assets/svg/service-icon-mlflow.svg'; -import pubsub from '../assets/svg/service-icon-pubsub.svg'; -import sftp from '../assets/svg/service-icon-sftp.svg'; -import teradata from '../assets/svg/teradata.svg'; -import topicDefault from '../assets/svg/topic.svg'; -import { EntityType } from '../enums/entity.enum'; -import { - ServiceCategory, - ServiceNestedConnectionFields, -} from '../enums/service.enum'; -import { PipelineType } from '../generated/api/services/ingestionPipelines/createIngestionPipeline'; -import { WorkflowStatus } from '../generated/entity/automations/workflow'; -import { StorageServiceType } from '../generated/entity/data/container'; -import { APIServiceType } from '../generated/entity/services/apiService'; -import { DashboardServiceType } from '../generated/entity/services/dashboardService'; -import { DatabaseServiceType } from '../generated/entity/services/databaseService'; -import { DriveServiceType } from '../generated/entity/services/driveService'; -import { MessagingServiceType } from '../generated/entity/services/messagingService'; -import { MetadataServiceType } from '../generated/entity/services/metadataService'; -import { MlModelServiceType } from '../generated/entity/services/mlmodelService'; -import { PipelineServiceType } from '../generated/entity/services/pipelineService'; -import { SearchServiceType } from '../generated/entity/services/searchService'; -import { Type as SecurityServiceType } from '../generated/entity/services/securityService'; -import { ServiceType } from '../generated/entity/services/serviceType'; -import { SERVICE_FILTER_PATTERN_FIELDS } from './ServiceConnection.constants'; - -export const MYSQL = mysql; -export const SQLITE = sqlite; -export const MSSQL = mssql; -export const REDSHIFT = redshift; -export const BIGQUERY = query; -export const BIGTABLE = bigtable; -export const HEX = hex; -export const HIVE = hive; -export const IMPALA = impala; -export const POSTGRES = postgres; -export const ORACLE = oracle; -export const SNOWFLAKE = snowflakes; -export const ATHENA = athena; -export const PRESTO = presto; -export const TRINO = trino; -export const GLUE = glue; -export const MARIADB = mariadb; -export const VERTICA = vertica; -export const KAFKA = kafka; -export const PUBSUB = pubsub; -export const REDPANDA = redpanda; -export const SUPERSET = superset; -export const SYNAPSE = synapse; -export const LOOKER = looker; -export const MICROSTRATEGY = microstrategy; -export const TABLEAU = tableau; -export const REDASH = redash; -export const METABASE = metabase; -export const AZURESQL = azuresql; -export const CLICKHOUSE = clickhouse; -export const DATABRICK = databrick; -export const UNITYCATALOG = unitycatalog; -export const IBMDB2 = ibmdb2; -export const DORIS = doris; -export const STARROCKS = starrocks; -export const DRUID = druid; -export const DYNAMODB = dynamodb; -export const SIGMA = sigma; -export const SINGLESTORE = singlestore; -export const SALESFORCE = salesforce; -export const MLFLOW = mlflow; -export const SAP_HANA = sapHana; -export const SAP_ERP = sapErp; -export const SCIKIT = scikit; -export const DELTALAKE = deltalake; -export const DEFAULT_SERVICE = iconDefaultService; -export const AIRBYTE = airbyte; -export const PINOT = pinot; -export const DATALAKE = datalake; -export const MODE = mode; -export const DAGSTER = dagster; -export const DBT = dbt; -export const FIVETRAN = fivetran; -export const AMUNDSEN = amundsen; -export const ATLAS = atlas; -export const ALATIONSINK = alationsink; -export const SAS = sas; -export const OPENLINEAGE = openlineage; -export const LOGO = logo; -export const EXASOL = exasol; -export const AIRFLOW = airflow; -export const POWERBI = powerbi; -export const DATABASE_DEFAULT = databaseDefault; -export const TOPIC_DEFAULT = topicDefault; -export const DASHBOARD_DEFAULT = dashboardDefault; -export const PIPELINE_DEFAULT = pipelineDefault; -export const ML_MODEL_DEFAULT = mlModelDefault; -export const CUSTOM_STORAGE_DEFAULT = storageDefault; -export const CUSTOM_DRIVE_DEFAULT = customDriveDefault; -export const DRIVE_DEFAULT = driveDefault; -export const NIFI = nifi; -export const KINESIS = kinesis; -export const QUICKSIGHT = quicksight; -export const DOMO = domo; -export const SAGEMAKER = sagemaker; -export const AMAZON_S3 = amazonS3; -export const GCS = gcs; -export const SPARK = spark; -export const SPLINE = spline; -export const MONGODB = mongodb; -export const CASSANDRA = cassandra; -export const QLIK_SENSE = qlikSense; -export const LIGHT_DASH = lightDash; -export const COUCHBASE = couchbase; -export const GREENPLUM = greenplum; -export const ELASTIC_SEARCH = elasticSearch; -export const OPEN_SEARCH = openSearch; -export const CUSTOM_SEARCH_DEFAULT = searchDefault; -export const TERADATA = teradata; -export const FLINK = flink; -export const REST_SERVICE = restService; -export const COCKROACH = cockroach; -export const SECURITY_DEFAULT = securitySafe; -export const GRAFANA = grafana; -export const GOOGLE_DRIVE = googleDrive; -export const SFTP = sftp; -export const TIMESCALE = timescale; -export const BURSTIQ = burstiq; -export const IOMETE = iomete; -export const excludedService = [ - MlModelServiceType.Sklearn, - MetadataServiceType.MetadataES, - MetadataServiceType.OpenMetadata, - PipelineServiceType.Spark, -]; - -export const arrServiceTypes: Array = [ - 'databaseServices', - 'messagingServices', - 'dashboardServices', - 'pipelineServices', - 'mlmodelServices', - 'storageServices', - 'apiServices', - 'securityServices', - 'driveServices', -]; - -export const SERVICE_CATEGORY: { [key: string]: ServiceCategory } = { - databases: ServiceCategory.DATABASE_SERVICES, - messaging: ServiceCategory.MESSAGING_SERVICES, - dashboards: ServiceCategory.DASHBOARD_SERVICES, - pipelines: ServiceCategory.PIPELINE_SERVICES, - mlmodels: ServiceCategory.ML_MODEL_SERVICES, - metadata: ServiceCategory.METADATA_SERVICES, - storages: ServiceCategory.STORAGE_SERVICES, - search: ServiceCategory.SEARCH_SERVICES, - apiServices: ServiceCategory.API_SERVICES, - security: ServiceCategory.SECURITY_SERVICES, - drives: ServiceCategory.DRIVE_SERVICES, -}; - -export const servicesDisplayName: Record< - string, - { key: string; entity: string } -> = { - databaseServices: { key: 'label.entity-service', entity: 'label.database' }, - messagingServices: { key: 'label.entity-service', entity: 'label.messaging' }, - dashboardServices: { key: 'label.entity-service', entity: 'label.dashboard' }, - pipelineServices: { key: 'label.entity-service', entity: 'label.pipeline' }, - mlmodelServices: { key: 'label.entity-service', entity: 'label.ml-model' }, - metadataServices: { key: 'label.entity-service', entity: 'label.metadata' }, - storageServices: { key: 'label.entity-service', entity: 'label.storage' }, - searchServices: { key: 'label.entity-service', entity: 'label.search' }, - dashboardDataModel: { - key: 'label.entity-service', - entity: 'label.data-model', - }, - apiServices: { key: 'label.entity-service', entity: 'label.api-uppercase' }, - securityServices: { key: 'label.entity-service', entity: 'label.security' }, - driveServices: { key: 'label.entity-service', entity: 'label.drive' }, -}; - -export const DEF_UI_SCHEMA = { - supportsIncrementalMetadataExtraction: { - 'ui:widget': 'hidden', - 'ui:hideError': true, - }, - supportsMetadataExtraction: { 'ui:widget': 'hidden', 'ui:hideError': true }, - supportsSystemProfile: { 'ui:widget': 'hidden', 'ui:hideError': true }, - supportsDataDiff: { 'ui:widget': 'hidden', 'ui:hideError': true }, - supportsUsageExtraction: { 'ui:widget': 'hidden', 'ui:hideError': true }, - supportsLineageExtraction: { 'ui:widget': 'hidden', 'ui:hideError': true }, - supportsViewLineageExtraction: { - 'ui:widget': 'hidden', - 'ui:hideError': true, - }, - supportsProfiler: { 'ui:widget': 'hidden', 'ui:hideError': true }, - supportsDatabase: { 'ui:widget': 'hidden', 'ui:hideError': true }, - supportsQueryComment: { 'ui:widget': 'hidden', 'ui:hideError': true }, - supportsDBTExtraction: { 'ui:widget': 'hidden', 'ui:hideError': true }, - type: { 'ui:widget': 'hidden' }, -}; - -export const INGESTION_ELASTIC_SEARCH_WORKFLOW_UI_SCHEMA = { - useSSL: { 'ui:widget': 'hidden', 'ui:hideError': true }, - verifyCerts: { 'ui:widget': 'hidden', 'ui:hideError': true }, - timeout: { 'ui:widget': 'hidden', 'ui:hideError': true }, - caCerts: { 'ui:widget': 'hidden', 'ui:hideError': true }, - useAwsCredentials: { 'ui:widget': 'hidden', 'ui:hideError': true }, - regionName: { 'ui:widget': 'hidden', 'ui:hideError': true }, -}; - -export const INGESTION_WORKFLOW_UI_SCHEMA = { - type: { 'ui:widget': 'hidden', 'ui:hideError': true }, - name: { 'ui:widget': 'hidden', 'ui:hideError': true }, - processingEngine: { 'ui:widget': 'hidden', 'ui:hideError': true }, - 'ui:order': [ - 'rootProcessingEngine', - 'name', - 'displayName', - ...SERVICE_FILTER_PATTERN_FIELDS, - 'enableDebugLog', - '*', - ], -}; - -export const EXCLUDE_INCREMENTAL_EXTRACTION_SUPPORT_UI_SCHEMA = { - incremental: { - 'ui:widget': 'hidden', - 'ui:hideError': true, - }, -}; - -export const COMMON_UI_SCHEMA = { - ...DEF_UI_SCHEMA, - [ServiceNestedConnectionFields.CONNECTION]: { - ...DEF_UI_SCHEMA, - }, - [ServiceNestedConnectionFields.METASTORE_CONNECTION]: { - ...DEF_UI_SCHEMA, - }, - [ServiceNestedConnectionFields.DATABASE_CONNECTION]: { - ...DEF_UI_SCHEMA, - }, -}; - -export const OPEN_METADATA = 'OpenMetadata'; -export const JWT_CONFIG = 'openMetadataJWTClientConfig'; - -export const SERVICE_CATEGORY_OPTIONS = map(ServiceCategory, (value) => ({ - label: startCase(value), - value, -})); - -export const STEPS_FOR_ADD_SERVICE: Array = [ - { - name: 'label.select-field', - nameData: { field: 'label.service-type' }, - step: 1, - }, - { - name: 'label.configure-entity', - nameData: { entity: 'label.service' }, - step: 2, - }, - { - name: 'label.connection-entity', - nameData: { entity: 'label.detail-plural' }, - step: 3, - }, - { - name: 'label.set-default-filters', - step: 4, - }, -]; - -export const STEPS_FOR_EDIT_SERVICE: Array = [ - { - name: 'label.connection-entity', - nameData: { entity: 'label.detail-plural' }, - step: 1, - }, - { - name: 'label.set-default-filters', - step: 2, - }, -]; - -export const SERVICE_DEFAULT_ERROR_MAP = { - serviceType: false, -}; -// 3 minutes timeout to wait for test connection status -// Increasing it temporarily while we investigate test connection delays -// @pmbrull -export const FETCHING_EXPIRY_TIME = 3 * 60 * 1000; -export const FETCH_INTERVAL = 2000; -export const WORKFLOW_COMPLETE_STATUS = [ - WorkflowStatus.Failed, - WorkflowStatus.Successful, -]; -export const TEST_CONNECTION_PROGRESS_PERCENTAGE = { - ZERO: 0, - ONE: 1, - TEN: 10, - TWENTY: 20, - FORTY: 40, - HUNDRED: 100, -}; - -export const SERVICE_TYPE_MAP = { - [ServiceCategory.DASHBOARD_SERVICES]: ServiceType.Dashboard, - [ServiceCategory.DATABASE_SERVICES]: ServiceType.Database, - [ServiceCategory.MESSAGING_SERVICES]: ServiceType.Messaging, - [ServiceCategory.ML_MODEL_SERVICES]: ServiceType.MlModel, - [ServiceCategory.METADATA_SERVICES]: ServiceType.Metadata, - [ServiceCategory.STORAGE_SERVICES]: ServiceType.Storage, - [ServiceCategory.PIPELINE_SERVICES]: ServiceType.Pipeline, - [ServiceCategory.SEARCH_SERVICES]: ServiceType.Search, - [ServiceCategory.API_SERVICES]: ServiceType.API, - [ServiceCategory.SECURITY_SERVICES]: ServiceType.Security, - [ServiceCategory.DRIVE_SERVICES]: ServiceType.Drive, -}; - -export const SERVICE_TYPES_ENUM = { - [ServiceCategory.DASHBOARD_SERVICES]: DashboardServiceType, - [ServiceCategory.DATABASE_SERVICES]: DatabaseServiceType, - [ServiceCategory.MESSAGING_SERVICES]: MessagingServiceType, - [ServiceCategory.ML_MODEL_SERVICES]: MlModelServiceType, - [ServiceCategory.METADATA_SERVICES]: MetadataServiceType, - [ServiceCategory.STORAGE_SERVICES]: StorageServiceType, - [ServiceCategory.PIPELINE_SERVICES]: PipelineServiceType, - [ServiceCategory.SEARCH_SERVICES]: SearchServiceType, - [ServiceCategory.API_SERVICES]: APIServiceType, - [ServiceCategory.SECURITY_SERVICES]: SecurityServiceType, - [ServiceCategory.DRIVE_SERVICES]: DriveServiceType, -}; - -export const BETA_SERVICES = [ - PipelineServiceType.OpenLineage, - PipelineServiceType.Wherescape, - DatabaseServiceType.Cassandra, - MetadataServiceType.AlationSink, - DatabaseServiceType.Cockroach, - SearchServiceType.OpenSearch, - PipelineServiceType.Ssis, - DatabaseServiceType.Ssas, - DashboardServiceType.ThoughtSpot, - SecurityServiceType.Ranger, - DatabaseServiceType.Epic, - DashboardServiceType.Grafana, - DashboardServiceType.Hex, - DatabaseServiceType.ServiceNow, - DatabaseServiceType.Timescale, - DatabaseServiceType.Dremio, - MetadataServiceType.Collibra, - PipelineServiceType.Mulesoft, - DatabaseServiceType.MicrosoftFabric, - PipelineServiceType.MicrosoftFabricPipeline, - DatabaseServiceType.BurstIQ, - DatabaseServiceType.StarRocks, - DriveServiceType.SFTP, - DatabaseServiceType.Informix, - DatabaseServiceType.MicrosoftAccess, - DatabaseServiceType.Iomete, -]; - -export const TEST_CONNECTION_INITIAL_MESSAGE = - 'message.test-your-connection-before-creating-service'; - -export const TEST_CONNECTION_SUCCESS_MESSAGE = - 'message.connection-test-successful'; - -export const TEST_CONNECTION_FAILURE_MESSAGE = 'message.connection-test-failed'; - -export const TEST_CONNECTION_TESTING_MESSAGE = - 'message.testing-your-connection-may-take-two-minutes'; - -export const TEST_CONNECTION_WARNING_MESSAGE = - 'message.connection-test-warning'; - -export const ADVANCED_PROPERTIES = [ - 'connectionArguments', - 'connectionOptions', - 'scheme', - 'sampleDataStorageConfig', - 'computeTableMetrics', - 'computeColumnMetrics', - 'includeViews', - 'useStatistics', - 'confidence', - 'profileSampleConfig', - 'randomizedSample', - 'sampleDataCount', - 'threadCount', - 'timeoutSeconds', - 'metrics', - 'sslConfig', - 'sslMode', - 'schemaRegistrySSL', - 'consumerConfigSSL', - 'verify', - 'useNonce', - 'disablePkce', - 'maxClockSkew', - 'tokenValidity', - 'maxAge', - 'sessionExpiry', -]; - -export const PIPELINE_SERVICE_PLATFORM = 'Airflow'; - -export const SERVICE_TYPES = [ - EntityType.DATABASE_SERVICE, - EntityType.DASHBOARD_SERVICE, - EntityType.MESSAGING_SERVICE, - EntityType.PIPELINE_SERVICE, - EntityType.MLMODEL_SERVICE, - EntityType.METADATA_SERVICE, - EntityType.STORAGE_SERVICE, - EntityType.SEARCH_SERVICE, - EntityType.API_SERVICE, - EntityType.SECURITY_SERVICE, - EntityType.DRIVE_SERVICE, -]; - -export const EXCLUDE_AUTO_PILOT_SERVICE_TYPES = [EntityType.SECURITY_SERVICE]; - -export const SERVICE_INGESTION_PIPELINE_TYPES = [ - PipelineType.Metadata, - PipelineType.Usage, - PipelineType.Lineage, - PipelineType.Profiler, - PipelineType.AutoClassification, - PipelineType.Dbt, -]; - -export const SERVICE_AUTOPILOT_AGENT_TYPES = [ - PipelineType.Metadata, - PipelineType.Lineage, - PipelineType.Usage, - PipelineType.AutoClassification, - PipelineType.Profiler, -]; +/** + * @deprecated This file previously contained 93 eager image imports that bloated the bundle. + * + * All exports have been moved to modular files. Update your imports: + * + * UI Schemas: + * - import { COMMON_UI_SCHEMA, DEF_UI_SCHEMA } from './ServiceUISchema.constant'; + * + * Service Types & Constants: + * - import { SERVICE_TYPES, BETA_SERVICES } from './ServiceType.constant'; + * + * Service Icons (lazy-loaded): + * - import { getServiceIcon } from '../utils/ServiceIconUtils'; + * - const icon = await getServiceIcon('mysql'); + */ -export const SERVICE_TYPE_WITH_DISPLAY_NAME = new Map([ - [PipelineServiceType.GluePipeline, 'Glue Pipeline'], - [DatabaseServiceType.DomoDatabase, 'Domo Database'], - [DashboardServiceType.DomoDashboard, 'Domo Dashboard'], - [DashboardServiceType.MicroStrategy, 'Micro Strategy'], - [DashboardServiceType.PowerBIReportServer, 'PowerBI Report Server'], - [PipelineServiceType.DatabricksPipeline, 'Databricks Pipeline'], - [PipelineServiceType.DomoPipeline, 'Domo Pipeline'], - [PipelineServiceType.KafkaConnect, 'Kafka Connect'], - [DatabaseServiceType.SapERP, 'SAP ERP'], - [DatabaseServiceType.SapHana, 'SAP HANA'], - [DatabaseServiceType.UnityCatalog, 'Unity Catalog'], - [PipelineServiceType.DataFactory, 'Data Factory'], - [PipelineServiceType.DBTCloud, 'DBT Cloud'], - [PipelineServiceType.OpenLineage, 'Open Lineage'], - [MetadataServiceType.AlationSink, 'Alation Sink'], - [SearchServiceType.ElasticSearch, 'Elasticsearch'], - [DatabaseServiceType.MicrosoftFabric, 'Microsoft Fabric'], - [PipelineServiceType.MicrosoftFabricPipeline, 'Microsoft Fabric Pipeline'], -]); +export * from './ServiceType.constant'; +export * from './ServiceUISchema.constant'; diff --git a/openmetadata-ui/src/main/resources/ui/src/constants/TestSuite.constant.ts b/openmetadata-ui/src/main/resources/ui/src/constants/TestSuite.constant.ts index 3ddb488d7825..4d3cfae42d13 100644 --- a/openmetadata-ui/src/main/resources/ui/src/constants/TestSuite.constant.ts +++ b/openmetadata-ui/src/main/resources/ui/src/constants/TestSuite.constant.ts @@ -11,15 +11,14 @@ * limitations under the License. */ -import i18next from 'i18next'; import { StepperStepType } from 'Models'; import { TestCaseResolutionStatusTypes } from '../generated/tests/testCaseResolutionStatus'; import { DataQualityPageTabs } from '../pages/DataQuality/DataQualityPage.interface'; import i18n from '../utils/i18next/LocalUtil'; -const TEST_SUITE_LABEL = i18next.t('label.test-suite'); -const ADD_TEST_SUITE_LABEL = i18next.t('label.add-entity', { - entity: i18next.t('label.test-suite'), +const TEST_SUITE_LABEL = i18n.t('label.test-suite'); +const ADD_TEST_SUITE_LABEL = i18n.t('label.add-entity', { + entity: i18n.t('label.test-suite'), }); export const STEPS_FOR_ADD_TEST_SUITE: Array = [ @@ -28,13 +27,13 @@ export const STEPS_FOR_ADD_TEST_SUITE: Array = [ step: 1, }, { - name: i18next.t('label.add-entity', { - entity: i18next.t('label.test-case'), + name: i18n.t('label.add-entity', { + entity: i18n.t('label.test-case'), }), step: 2, }, { - name: i18next.t('label.test-suite-status'), + name: i18n.t('label.test-suite-status'), step: 3, }, ]; diff --git a/openmetadata-ui/src/main/resources/ui/src/constants/constants.ts b/openmetadata-ui/src/main/resources/ui/src/constants/constants.ts index 83e465511b2d..3fcb09c38b43 100644 --- a/openmetadata-ui/src/main/resources/ui/src/constants/constants.ts +++ b/openmetadata-ui/src/main/resources/ui/src/constants/constants.ts @@ -82,19 +82,10 @@ export const LAST_VERSION_FETCH_TIME_KEY = 'versionFetchTime'; export const LOCALSTORAGE_RECENTLY_VIEWED = `recentlyViewedData`; export const LOCALSTORAGE_RECENTLY_SEARCHED = `recentlySearchedData`; export const VERSION = 'VERSION'; -export const REDIRECT_PATHNAME = 'redirectUrlPath'; export const TERM_ADMIN = 'Admin'; export const TERM_USER = 'User'; export const DISABLED = 'disabled'; -export const imageTypes = { - image: 's96-c', - image192: 's192-c', - image24: 's24-c', - image32: 's32-c', - image48: 's48-c', - image512: 's512-c', - image72: 's72-c', -}; + export const NO_DATA_PLACEHOLDER = '--'; export const PIPE_SYMBOL = '|'; export const NO_DATA = '-'; diff --git a/openmetadata-ui/src/main/resources/ui/src/constants/router.constants.ts b/openmetadata-ui/src/main/resources/ui/src/constants/router.constants.ts new file mode 100644 index 000000000000..75a501588f70 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/constants/router.constants.ts @@ -0,0 +1,46 @@ +/* + * Copyright 2022 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. + */ + +export const APP_ROUTER_ROUTES = { + HOME: '/', + MY_DATA: '/my-data', + NOT_FOUND: '/404', + LOGOUT: '/logout', + UNAUTHORISED: '/unauthorised', + SIGNUP: '/signup', + AUTH_CALLBACK: '/auth/callback', + SIGNIN: '/signin', + FORGOT_PASSWORD: '/forgot-password', + CALLBACK: '/callback', + SILENT_CALLBACK: '/silent-callback', + REGISTER: '/register', + RESET_PASSWORD: '/users/password/reset', + ACCOUNT_ACTIVATION: '/users/registrationConfirmation', +} as const; + +export const UNPROTECTED_ROUTES: Set = new Set([ + APP_ROUTER_ROUTES.SIGNUP, + APP_ROUTER_ROUTES.SIGNIN, + APP_ROUTER_ROUTES.FORGOT_PASSWORD, + APP_ROUTER_ROUTES.CALLBACK, + APP_ROUTER_ROUTES.SILENT_CALLBACK, + APP_ROUTER_ROUTES.REGISTER, + APP_ROUTER_ROUTES.RESET_PASSWORD, + APP_ROUTER_ROUTES.ACCOUNT_ACTIVATION, + APP_ROUTER_ROUTES.HOME, + APP_ROUTER_ROUTES.AUTH_CALLBACK, + APP_ROUTER_ROUTES.NOT_FOUND, + APP_ROUTER_ROUTES.LOGOUT, +]); + +export const REDIRECT_PATHNAME = 'redirectUrlPath'; diff --git a/openmetadata-ui/src/main/resources/ui/src/context/AntDConfigProvider/AntDConfigProvider.tsx b/openmetadata-ui/src/main/resources/ui/src/context/AntDConfigProvider/AntDConfigProvider.tsx index b42bb48be7a4..b976ac2644a1 100644 --- a/openmetadata-ui/src/main/resources/ui/src/context/AntDConfigProvider/AntDConfigProvider.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/context/AntDConfigProvider/AntDConfigProvider.tsx @@ -19,7 +19,9 @@ import { generatePalette } from '../../styles/colorPallet'; const AntDConfigProvider: FC<{ children: ReactNode }> = ({ children }) => { const { i18n } = useTranslation(); - const { applicationConfig } = useApplicationStore(); + const applicationConfig = useApplicationStore( + (state) => state.applicationConfig + ); useEffect(() => { const palette = generatePalette( diff --git a/openmetadata-ui/src/main/resources/ui/src/context/PermissionProvider/PermissionProvider.tsx b/openmetadata-ui/src/main/resources/ui/src/context/PermissionProvider/PermissionProvider.tsx index e72e99960193..52aeb541ab87 100644 --- a/openmetadata-ui/src/main/resources/ui/src/context/PermissionProvider/PermissionProvider.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/context/PermissionProvider/PermissionProvider.tsx @@ -24,20 +24,19 @@ import { } from 'react'; import { useNavigate } from 'react-router-dom'; import Loader from '../../components/common/Loader/Loader'; -import { REDIRECT_PATHNAME } from '../../constants/constants'; +import { REDIRECT_PATHNAME } from '../../constants/router.constants'; +import { useApplicationStore } from '../../hooks/useApplicationStore'; import { getEntityPermissionByFqn, getEntityPermissionById, getLoggedInUserPermissions, getResourcePermission, } from '../../rest/permissionAPI'; +import { setUrlPathnameExpiryAfterRoute } from '../../utils/AuthProvider.util'; import { getOperationPermissions, getUIPermission, } from '../../utils/PermissionsUtils'; - -import { useApplicationStore } from '../../hooks/useApplicationStore'; -import { setUrlPathnameExpiryAfterRoute } from '../../utils/AuthProvider.util'; import { EntityPermissionMap, PermissionContextType, diff --git a/openmetadata-ui/src/main/resources/ui/src/hoc/withDomainFilter.test.tsx b/openmetadata-ui/src/main/resources/ui/src/hoc/withDomainFilter.test.tsx new file mode 100644 index 000000000000..b31cde692332 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/hoc/withDomainFilter.test.tsx @@ -0,0 +1,515 @@ +/* + * Copyright 2026 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 { InternalAxiosRequestConfig } from 'axios'; +import { DEFAULT_DOMAIN_VALUE } from '../constants/constants'; +import { SearchIndex } from '../enums/search.enum'; +import { useDomainStore } from '../hooks/useDomainStore'; +import { getPathNameFromWindowLocation } from '../utils/LocationUtils'; +import { withDomainFilter } from './withDomainFilter'; + +jest.mock('../hooks/useDomainStore'); +jest.mock('../utils/LocationUtils', () => ({ + getPathNameFromWindowLocation: jest.fn(), +})); + +describe('withDomainFilter', () => { + const mockGetState = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + (useDomainStore as unknown as jest.Mock).mockImplementation(() => ({ + getState: mockGetState, + })); + (useDomainStore.getState as jest.Mock) = mockGetState; + }); + + const createMockConfig = ( + method: string = 'get', + url?: string, + params?: Record + ): InternalAxiosRequestConfig => + ({ + method, + url, + params, + headers: {}, + } as InternalAxiosRequestConfig); + + describe('should not intercept requests', () => { + it('should return config unchanged when path starts with /domain', () => { + (getPathNameFromWindowLocation as jest.Mock).mockImplementationOnce( + () => '/domain/test' + ); + mockGetState.mockReturnValue({ activeDomain: 'testDomain' }); + + const config = createMockConfig(); + const result = withDomainFilter(config); + + expect(result).toBe(config); + expect(result.params).toBeUndefined(); + }); + + it('should return config unchanged when path starts with /auth/logout', () => { + (getPathNameFromWindowLocation as jest.Mock).mockReturnValueOnce( + '/auth/logout' + ); + mockGetState.mockReturnValue({ activeDomain: 'testDomain' }); + + const config = createMockConfig(); + const result = withDomainFilter(config); + + expect(result).toBe(config); + expect(result.params).toBeUndefined(); + }); + + it('should return config unchanged when path starts with /auth/refresh', () => { + (getPathNameFromWindowLocation as jest.Mock).mockReturnValueOnce( + '/auth/refresh' + ); + mockGetState.mockReturnValue({ activeDomain: 'testDomain' }); + + const config = createMockConfig(); + const result = withDomainFilter(config); + + expect(result).toBe(config); + expect(result.params).toBeUndefined(); + }); + + it('should return config unchanged when method is not GET', () => { + (getPathNameFromWindowLocation as jest.Mock).mockReturnValueOnce( + '/api/test' + ); + mockGetState.mockReturnValue({ activeDomain: 'testDomain' }); + + const config = createMockConfig('post'); + const result = withDomainFilter(config); + + expect(result).toBe(config); + expect(result.params).toBeUndefined(); + }); + + it('should return config unchanged when activeDomain is DEFAULT_DOMAIN_VALUE', () => { + (getPathNameFromWindowLocation as jest.Mock).mockReturnValueOnce( + '/api/test' + ); + mockGetState.mockReturnValue({ activeDomain: DEFAULT_DOMAIN_VALUE }); + + const config = createMockConfig(); + const result = withDomainFilter(config); + + expect(result).toBe(config); + expect(result.params).toBeUndefined(); + }); + }); + + describe('regular GET requests', () => { + it('should add domain parameter for regular GET requests with active domain', () => { + (getPathNameFromWindowLocation as jest.Mock).mockReturnValueOnce( + '/api/tables' + ); + mockGetState.mockReturnValue({ activeDomain: 'engineering' }); + + const config = createMockConfig('get', '/api/tables'); + const result = withDomainFilter(config); + + expect(result.params).toEqual({ + domain: 'engineering', + }); + }); + + it('should preserve existing params when adding domain parameter', () => { + (getPathNameFromWindowLocation as jest.Mock).mockReturnValueOnce( + '/api/tables' + ); + mockGetState.mockReturnValue({ activeDomain: 'engineering' }); + + const config = createMockConfig('get', '/api/tables', { + limit: 10, + offset: 0, + }); + const result = withDomainFilter(config); + + expect(result.params).toEqual({ + limit: 10, + offset: 0, + domain: 'engineering', + }); + }); + }); + + describe('search query requests', () => { + it('should add should filter with term and prefix for /search/query with active domain', () => { + (getPathNameFromWindowLocation as jest.Mock).mockReturnValueOnce( + '/api/search' + ); + mockGetState.mockReturnValue({ activeDomain: 'engineering' }); + + const config = createMockConfig('get', '/search/query', { + index: SearchIndex.TABLE, + }); + const result = withDomainFilter(config); + + expect(result.params).toHaveProperty('query_filter'); + + const filter = JSON.parse(result.params?.query_filter as string); + + expect(filter).toEqual({ + query: { + bool: { + must: [ + { + bool: { + should: [ + { + term: { + 'domains.fullyQualifiedName': 'engineering', + }, + }, + { + prefix: { + 'domains.fullyQualifiedName': 'engineering.', + }, + }, + ], + }, + }, + ], + }, + }, + }); + }); + + it('should return config unchanged for TAG index searches', () => { + (getPathNameFromWindowLocation as jest.Mock).mockReturnValueOnce( + '/api/search' + ); + mockGetState.mockReturnValue({ activeDomain: 'engineering' }); + + const config = createMockConfig('get', '/search/query', { + index: SearchIndex.TAG, + }); + const result = withDomainFilter(config); + + expect(result).toBe(config); + expect(result.params?.query_filter).toBeUndefined(); + }); + + it('should use fullyQualifiedName field for DOMAIN index searches', () => { + (getPathNameFromWindowLocation as jest.Mock).mockReturnValueOnce( + '/api/search' + ); + mockGetState.mockReturnValue({ activeDomain: 'engineering' }); + + const config = createMockConfig('get', '/search/query', { + index: SearchIndex.DOMAIN, + }); + const result = withDomainFilter(config); + const queryFilter = JSON.parse(result.params?.query_filter as string); + const shouldClauses = + queryFilter.query.bool.must[queryFilter.query.bool.must.length - 1].bool + .should; + + expect(shouldClauses).toEqual([ + { term: { fullyQualifiedName: 'engineering' } }, + { prefix: { fullyQualifiedName: 'engineering.' } }, + ]); + }); + + it('should preserve existing query_filter and add should filter', () => { + (getPathNameFromWindowLocation as jest.Mock).mockReturnValueOnce( + '/api/search' + ); + mockGetState.mockReturnValue({ activeDomain: 'engineering' }); + + const existingFilter = { + query: { + bool: { + must: [ + { + term: { + entityType: 'table', + }, + }, + ], + }, + }, + }; + + const config = createMockConfig('get', '/search/query', { + index: SearchIndex.TABLE, + query_filter: JSON.stringify(existingFilter), + }); + const result = withDomainFilter(config); + + const filter = JSON.parse(result.params?.query_filter as string); + + expect(filter.query.bool.must).toHaveLength(2); + expect(filter.query.bool.must[0]).toEqual({ + term: { + entityType: 'table', + }, + }); + expect(filter.query.bool.must[1]).toEqual({ + bool: { + should: [ + { + term: { + 'domains.fullyQualifiedName': 'engineering', + }, + }, + { + prefix: { + 'domains.fullyQualifiedName': 'engineering.', + }, + }, + ], + }, + }); + }); + + it('should handle invalid JSON in query_filter gracefully', () => { + (getPathNameFromWindowLocation as jest.Mock).mockReturnValueOnce( + '/api/search' + ); + mockGetState.mockReturnValue({ activeDomain: 'engineering' }); + + const config = createMockConfig('get', '/search/query', { + index: SearchIndex.TABLE, + query_filter: 'invalid-json', + }); + const result = withDomainFilter(config); + + const filter = JSON.parse(result.params?.query_filter as string); + + expect(filter).toEqual({ + query: { + bool: { + must: [ + { + bool: { + should: [ + { + term: { + 'domains.fullyQualifiedName': 'engineering', + }, + }, + { + prefix: { + 'domains.fullyQualifiedName': 'engineering.', + }, + }, + ], + }, + }, + ], + }, + }, + }); + }); + + it('should handle query_filter with empty must array', () => { + (getPathNameFromWindowLocation as jest.Mock).mockReturnValueOnce( + '/api/search' + ); + mockGetState.mockReturnValue({ activeDomain: 'engineering' }); + + const existingFilter = { + query: { + bool: {}, + }, + }; + + const config = createMockConfig('get', '/search/query', { + index: SearchIndex.TABLE, + query_filter: JSON.stringify(existingFilter), + }); + const result = withDomainFilter(config); + + const filter = JSON.parse(result.params?.query_filter as string); + + expect(filter.query.bool.must).toHaveLength(1); + expect(filter.query.bool.must[0]).toEqual({ + bool: { + should: [ + { + term: { + 'domains.fullyQualifiedName': 'engineering', + }, + }, + { + prefix: { + 'domains.fullyQualifiedName': 'engineering.', + }, + }, + ], + }, + }); + }); + + it('should handle empty object query_filter gracefully', () => { + (getPathNameFromWindowLocation as jest.Mock).mockReturnValueOnce( + '/api/search' + ); + mockGetState.mockReturnValue({ activeDomain: 'engineering' }); + + const config = createMockConfig('get', '/search/query', { + index: SearchIndex.TABLE, + query_filter: '{}', + }); + const result = withDomainFilter(config); + + const filter = JSON.parse(result.params?.query_filter as string); + + expect(filter).toEqual({ + query: { + bool: { + must: [ + { + bool: { + should: [ + { + term: { + 'domains.fullyQualifiedName': 'engineering', + }, + }, + { + prefix: { + 'domains.fullyQualifiedName': 'engineering.', + }, + }, + ], + }, + }, + ], + }, + }, + }); + }); + + it('should preserve existing params when adding query_filter', () => { + (getPathNameFromWindowLocation as jest.Mock).mockReturnValueOnce( + '/api/search' + ); + mockGetState.mockReturnValue({ activeDomain: 'engineering' }); + + const config = createMockConfig('get', '/search/query', { + index: SearchIndex.TABLE, + limit: 10, + offset: 0, + }); + const result = withDomainFilter(config); + + expect(result.params).toHaveProperty('index', SearchIndex.TABLE); + expect(result.params).toHaveProperty('limit', 10); + expect(result.params).toHaveProperty('offset', 0); + expect(result.params).toHaveProperty('query_filter'); + }); + + it('should preserve non-bool top-level clauses when adding domain filter', () => { + (getPathNameFromWindowLocation as jest.Mock).mockReturnValueOnce( + '/api/search' + ); + mockGetState.mockReturnValue({ activeDomain: 'engineering' }); + + const existingFilter = JSON.stringify({ + query: { + term: { 'some.field': 'someValue' }, + bool: { must: [{ term: { 'other.field': 'otherValue' } }] }, + }, + }); + + const config = createMockConfig('get', '/search/query', { + index: SearchIndex.TABLE, + query_filter: existingFilter, + }); + const result = withDomainFilter(config); + + const parsed = JSON.parse(result.params?.query_filter as string); + + expect(parsed.query.bool.must).toContainEqual({ + term: { 'some.field': 'someValue' }, + }); + expect(parsed.query.bool.must).toContainEqual({ + term: { 'other.field': 'otherValue' }, + }); + expect(parsed.query.bool.must).toContainEqual({ + bool: { + should: [ + { + term: { + 'domains.fullyQualifiedName': 'engineering', + }, + }, + { + prefix: { + 'domains.fullyQualifiedName': 'engineering.', + }, + }, + ], + }, + }); + expect(parsed.query.bool.must).toHaveLength(3); + }); + }); + + describe('nested domain paths', () => { + it('should handle nested domain paths correctly', () => { + (getPathNameFromWindowLocation as jest.Mock).mockReturnValueOnce( + '/api/tables' + ); + mockGetState.mockReturnValue({ + activeDomain: 'engineering.backend.services', + }); + + const config = createMockConfig('get', '/api/tables'); + const result = withDomainFilter(config); + + expect(result.params).toEqual({ + domain: 'engineering.backend.services', + }); + }); + + it('should add should filter with nested domain for search queries', () => { + (getPathNameFromWindowLocation as jest.Mock).mockReturnValueOnce( + '/api/search' + ); + mockGetState.mockReturnValue({ + activeDomain: 'engineering.backend.services', + }); + + const config = createMockConfig('get', '/search/query', { + index: SearchIndex.TABLE, + }); + const result = withDomainFilter(config); + + const filter = JSON.parse(result.params?.query_filter as string); + + expect(filter.query.bool.must[0]).toEqual({ + bool: { + should: [ + { + term: { + 'domains.fullyQualifiedName': 'engineering.backend.services', + }, + }, + { + prefix: { + 'domains.fullyQualifiedName': 'engineering.backend.services.', + }, + }, + ], + }, + }); + }); + }); +}); diff --git a/openmetadata-ui/src/main/resources/ui/src/hoc/withDomainFilter.tsx b/openmetadata-ui/src/main/resources/ui/src/hoc/withDomainFilter.tsx new file mode 100644 index 000000000000..dac6cbbd787b --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/hoc/withDomainFilter.tsx @@ -0,0 +1,114 @@ +/* + * Copyright 2026 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 { InternalAxiosRequestConfig } from 'axios'; +import { DEFAULT_DOMAIN_VALUE } from '../constants/constants'; +import { SearchIndex } from '../enums/search.enum'; +import { useDomainStore } from '../hooks/useDomainStore'; +import { + QueryFieldInterface, + QueryFilterInterface, +} from '../pages/ExplorePage/ExplorePage.interface'; +import { getPathNameFromWindowLocation } from '../utils/LocationUtils'; + +export const withDomainFilter = ( + config: InternalAxiosRequestConfig +): InternalAxiosRequestConfig => { + const isGetRequest = config.method === 'get'; + const activeDomain = useDomainStore.getState().activeDomain; + const hasActiveDomain = activeDomain !== DEFAULT_DOMAIN_VALUE; + const currentPath = getPathNameFromWindowLocation(); + + const shouldNotIntercept = [ + '/domain', + '/auth/logout', + '/auth/refresh', + ].reduce((prev, curr) => { + return prev || currentPath.startsWith(curr); + }, false); + + if (shouldNotIntercept) { + return config; + } + + if (isGetRequest && hasActiveDomain) { + if (config.url?.includes('/search/query')) { + if (config.params?.index === SearchIndex.TAG) { + return config; + } + + const domainFilterField = + config.params?.index === SearchIndex.DOMAIN + ? 'fullyQualifiedName' + : 'domains.fullyQualifiedName'; + let filter: QueryFilterInterface = { query: { bool: {} } }; + if (config.params?.query_filter) { + try { + const parsed = JSON.parse(config.params.query_filter as string); + filter = parsed?.query ? parsed : { query: { bool: {} } }; + } catch { + filter = { query: { bool: {} } }; + } + } + + let mustArray: QueryFieldInterface[] = []; + const existingMust = filter.query?.bool?.must; + if (Array.isArray(existingMust)) { + mustArray = [...existingMust]; + } else if (existingMust) { + mustArray = [existingMust]; + } + + const { bool: existingBool, ...nonBoolClauses } = filter.query ?? {}; + for (const [key, value] of Object.entries(nonBoolClauses)) { + mustArray.push({ [key]: value } as QueryFieldInterface); + } + + filter.query = { + bool: { + ...existingBool, + must: [ + ...mustArray, + { + bool: { + should: [ + { + term: { + [domainFilterField]: activeDomain, + }, + }, + { + prefix: { + [domainFilterField]: `${activeDomain}.`, + }, + }, + ], + }, + } as QueryFieldInterface, + ], + }, + }; + + config.params = { + ...config.params, + query_filter: JSON.stringify(filter), + }; + } else { + config.params = { + ...config.params, + domain: activeDomain, + }; + } + } + + return config; +}; diff --git a/openmetadata-ui/src/main/resources/ui/src/hooks/currentUserStore/useCurrentUserStore.test.ts b/openmetadata-ui/src/main/resources/ui/src/hooks/currentUserStore/useCurrentUserStore.test.ts index 55a5604fe9fa..6fbad8742ca1 100644 --- a/openmetadata-ui/src/main/resources/ui/src/hooks/currentUserStore/useCurrentUserStore.test.ts +++ b/openmetadata-ui/src/main/resources/ui/src/hooks/currentUserStore/useCurrentUserStore.test.ts @@ -11,7 +11,6 @@ * limitations under the License. */ import { renderHook, waitFor } from '@testing-library/react'; -import { SupportedLocales } from '../../utils/i18next/LocalUtil.interface'; import { useApplicationStore } from '../useApplicationStore'; import { useCurrentUserPreferences, @@ -23,11 +22,6 @@ jest.mock('../useApplicationStore', () => ({ useApplicationStore: jest.fn(), })); -// Mock the detectBrowserLanguage function -jest.mock('../../utils/i18next/LocalUtil', () => ({ - detectBrowserLanguage: jest.fn(() => 'en-US'), -})); - jest.mock('../../constants/constants', () => ({ PAGE_SIZE_BASE: 15, })); @@ -57,7 +51,6 @@ describe('useCurrentUserStore', () => { const defaultPreferences = { isSidebarCollapsed: false, - language: SupportedLocales.English, selectedEntityTableColumns: {}, globalPageSize: 15, recentlySearched: [], @@ -78,14 +71,12 @@ describe('useCurrentUserStore', () => { result.current.setPreference({ isSidebarCollapsed: true, - language: SupportedLocales.简体中文, }); // Preferences should remain default since no user expect(result.current.preferences).toEqual({ ...defaultPreferences, isSidebarCollapsed: false, - language: SupportedLocales.English, }); }); @@ -104,14 +95,14 @@ describe('useCurrentUserStore', () => { // Set preferences directly through the setPreference method await waitFor(async () => { result.current.setPreference({ - language: SupportedLocales.简体中文, + isSidebarCollapsed: true, }); }); // Direct check without waitFor expect(result.current.preferences).toEqual({ ...defaultPreferences, - language: SupportedLocales.简体中文, + isSidebarCollapsed: true, }); }); @@ -142,7 +133,6 @@ describe('useCurrentUserStore', () => { // Should spread language from defaultPreferences since it's missing expect(result.current.preferences).toEqual({ isSidebarCollapsed: true, - language: SupportedLocales.English, // From defaultPreferences selectedEntityTableColumns: { table1: ['col1', 'col2'] }, globalPageSize: 15, recentlySearched: [], @@ -167,7 +157,6 @@ describe('useCurrentUserStore', () => { preferences: { userWithLanguage: { isSidebarCollapsed: false, - language: SupportedLocales.简体中文, selectedEntityTableColumns: {}, globalPageSize: 15, recentlySearched: [], @@ -183,7 +172,6 @@ describe('useCurrentUserStore', () => { // Should preserve the existing language preference expect(result.current.preferences).toEqual({ isSidebarCollapsed: false, - language: SupportedLocales.简体中文, // User's existing preference preserved selectedEntityTableColumns: {}, globalPageSize: 15, recentlySearched: [], diff --git a/openmetadata-ui/src/main/resources/ui/src/hooks/currentUserStore/useCurrentUserStore.ts b/openmetadata-ui/src/main/resources/ui/src/hooks/currentUserStore/useCurrentUserStore.ts index 5a788490c3d1..c0b326109614 100644 --- a/openmetadata-ui/src/main/resources/ui/src/hooks/currentUserStore/useCurrentUserStore.ts +++ b/openmetadata-ui/src/main/resources/ui/src/hooks/currentUserStore/useCurrentUserStore.ts @@ -15,8 +15,6 @@ import { RecentlySearchedData, RecentlyViewedData } from 'Models'; import { create } from 'zustand'; import { createJSONStorage, persist } from 'zustand/middleware'; import { PAGE_SIZE_BASE } from '../../constants/constants'; -import { detectBrowserLanguage } from '../../utils/i18next/LocalUtil'; -import { SupportedLocales } from '../../utils/i18next/LocalUtil.interface'; import { useApplicationStore } from '../useApplicationStore'; export interface MarketplaceRecentSearchEntry { @@ -26,7 +24,6 @@ export interface MarketplaceRecentSearchEntry { export interface UserPreferences { isSidebarCollapsed: boolean; - language: SupportedLocales; selectedEntityTableColumns: Record; globalPageSize: number; recentlyViewed: RecentlyViewedData[]; @@ -47,7 +44,6 @@ interface Store { const defaultPreferences: UserPreferences = { isSidebarCollapsed: false, - language: detectBrowserLanguage(), selectedEntityTableColumns: {}, globalPageSize: PAGE_SIZE_BASE, recentlyViewed: [], diff --git a/openmetadata-ui/src/main/resources/ui/src/index.tsx b/openmetadata-ui/src/main/resources/ui/src/index.tsx index 09913d94229d..aa68632d0c79 100644 --- a/openmetadata-ui/src/main/resources/ui/src/index.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/index.tsx @@ -13,7 +13,7 @@ import React from 'react'; import { createRoot } from 'react-dom/client'; -import App from './App'; +import AppRoot from './AppRoot'; import './styles/index'; import { getBasePath } from './utils/HistoryUtils'; @@ -25,11 +25,11 @@ const root = createRoot(container); root.render( - + ); -if ('serviceWorker' in navigator && 'indexedDB' in window) { +if ('serviceWorker' in navigator && 'indexedDB' in globalThis) { window.addEventListener('load', () => { const basePath = getBasePath(); const serviceWorkerPath = basePath diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/AddGlossary/AddGlossaryPage.component.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/AddGlossary/AddGlossaryPage.component.tsx index b484e2d08d35..1b24770558b9 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/AddGlossary/AddGlossaryPage.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/AddGlossary/AddGlossaryPage.component.tsx @@ -30,7 +30,7 @@ import { CreateGlossary } from '../../generated/api/data/createGlossary'; import { Operation } from '../../generated/entity/policies/policy'; import { withPageLayout } from '../../hoc/withPageLayout'; import { addGlossaries } from '../../rest/glossaryAPI'; -import { getIsErrorMatch } from '../../utils/CommonUtils'; +import { getIsErrorMatch } from '../../utils/APIUtils'; import { checkPermission } from '../../utils/PermissionsUtils'; import { getGlossaryPath } from '../../utils/RouterUtils'; import { getClassifications, getTaglist } from '../../utils/TagsUtils'; diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/CustomPageSettings/CustomPageSettings.test.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/CustomPageSettings/CustomPageSettings.test.tsx index a7f754061674..1c966dd1527e 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/CustomPageSettings/CustomPageSettings.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/CustomPageSettings/CustomPageSettings.test.tsx @@ -62,10 +62,6 @@ jest.mock('../../components/common/NextPrevious/NextPrevious', () => { return jest.fn().mockImplementation(() =>
NextPrevious
); }); -jest.mock('../../utils/CommonUtils', () => ({ - Transi18next: jest.fn().mockReturnValue(
Transi18next
), -})); - jest.mock('../../components/PageHeader/PageHeader.component', () => { return jest.fn().mockImplementation(({ children, data }) => (
diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/CustomPageSettings/CustomPageSettings.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/CustomPageSettings/CustomPageSettings.tsx index 9b6739190623..e760cf138c0f 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/CustomPageSettings/CustomPageSettings.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/CustomPageSettings/CustomPageSettings.tsx @@ -37,13 +37,15 @@ import { Paging } from '../../generated/type/paging'; import { usePaging } from '../../hooks/paging/usePaging'; import { useApplicationStore } from '../../hooks/useApplicationStore'; import { getAllPersonas } from '../../rest/PersonaAPI'; -import { Transi18next } from '../../utils/CommonUtils'; import { getEntityName } from '../../utils/EntityUtils'; import { getCustomizePagePath, getSettingPageEntityBreadCrumb, } from '../../utils/GlobalSettingsUtils'; -import { translateWithNestedKeys } from '../../utils/i18next/LocalUtil'; +import { + Transi18next, + translateWithNestedKeys, +} from '../../utils/i18next/LocalUtil'; import { getSettingPath } from '../../utils/RouterUtils'; import { showErrorToast } from '../../utils/ToastUtils'; import './custom-page-settings.less'; diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/CustomizablePage/CustomizablePage.test.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/CustomizablePage/CustomizablePage.test.tsx index d0c47476f3ae..042a819f2aa7 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/CustomizablePage/CustomizablePage.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/CustomizablePage/CustomizablePage.test.tsx @@ -11,7 +11,8 @@ * limitations under the License. */ -import { act, render, screen } from '@testing-library/react'; +import { render, screen } from '@testing-library/react'; +import { act } from 'react'; import { useParams } from 'react-router-dom'; import { Page, PageType } from '../../generated/system/ui/page'; import { diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/CustomizablePage/CustomizablePage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/CustomizablePage/CustomizablePage.tsx index 42f84dc9226a..6654e309474b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/CustomizablePage/CustomizablePage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/CustomizablePage/CustomizablePage.tsx @@ -43,7 +43,7 @@ import { updateDocument, } from '../../rest/DocStoreAPI'; import { getPersonaByName } from '../../rest/PersonaAPI'; -import { Transi18next } from '../../utils/CommonUtils'; +import { Transi18next } from '../../utils/i18next/LocalUtil'; import { getSettingPath } from '../../utils/RouterUtils'; import { showErrorToast, showSuccessToast } from '../../utils/ToastUtils'; import { useRequiredParams } from '../../utils/useRequiredParams'; diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/LoginPage/LoginCarousel.test.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/LoginPage/LoginCarousel.test.tsx index b245860f0635..1b84c6e9b18a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/LoginPage/LoginCarousel.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/LoginPage/LoginCarousel.test.tsx @@ -11,7 +11,8 @@ * limitations under the License. */ -import { act, render, screen } from '@testing-library/react'; +import { render, screen } from '@testing-library/react'; +import { act } from 'react'; import { MemoryRouter } from 'react-router-dom'; import loginClassBase from '../../constants/LoginClassBase'; import LoginCarousel from './LoginCarousel'; diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/MyDataPage/my-data.less b/openmetadata-ui/src/main/resources/ui/src/pages/MyDataPage/my-data.less index dd1ff49ff7e9..7bc860d0f1e7 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/MyDataPage/my-data.less +++ b/openmetadata-ui/src/main/resources/ui/src/pages/MyDataPage/my-data.less @@ -70,3 +70,9 @@ scrollbar-color: @grey-300 transparent; } } + +.grid-wrapper { + .grid-container { + margin-top: -220px; + } +} diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/PoliciesPage/AddPolicyPage/AddPolicyPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/PoliciesPage/AddPolicyPage/AddPolicyPage.tsx index dde5c4a1caf8..de344d089090 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/PoliciesPage/AddPolicyPage/AddPolicyPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/PoliciesPage/AddPolicyPage/AddPolicyPage.tsx @@ -31,8 +31,8 @@ import { import { withPageLayout } from '../../../hoc/withPageLayout'; import { FieldProp, FieldTypes } from '../../../interface/FormUtils.interface'; import { addPolicy } from '../../../rest/rolesAPIV1'; +import { getIsErrorMatch } from '../../../utils/APIUtils'; import brandClassBase from '../../../utils/BrandData/BrandClassBase'; -import { getIsErrorMatch } from '../../../utils/CommonUtils'; import { getField } from '../../../utils/formUtils'; import { translateWithNestedKeys } from '../../../utils/i18next/LocalUtil'; import { getPath, getPolicyWithFqnPath } from '../../../utils/RouterUtils'; diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/RolesPage/AddRolePage/AddRolePage.test.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/RolesPage/AddRolePage/AddRolePage.test.tsx index adcf94566158..48fd6bfa7fb4 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/RolesPage/AddRolePage/AddRolePage.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/RolesPage/AddRolePage/AddRolePage.test.tsx @@ -66,9 +66,6 @@ jest.mock('../../../components/common/ResizablePanels/ResizablePanels', () => )) ); -jest.mock('../../../utils/CommonUtils', () => ({ - getIsErrorMatch: jest.fn(), -})); jest.mock('../../../utils/BrandData/BrandClassBase', () => ({ __esModule: true, diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/RolesPage/AddRolePage/AddRolePage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/RolesPage/AddRolePage/AddRolePage.tsx index e26b8a73295d..7252d82c9674 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/RolesPage/AddRolePage/AddRolePage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/RolesPage/AddRolePage/AddRolePage.tsx @@ -28,8 +28,8 @@ import { Policy } from '../../../generated/entity/policies/policy'; import { withPageLayout } from '../../../hoc/withPageLayout'; import { FieldProp, FieldTypes } from '../../../interface/FormUtils.interface'; import { addRole, getPolicies } from '../../../rest/rolesAPIV1'; +import { getIsErrorMatch } from '../../../utils/APIUtils'; import brandClassBase from '../../../utils/BrandData/BrandClassBase'; -import { getIsErrorMatch } from '../../../utils/CommonUtils'; import { getField } from '../../../utils/formUtils'; import { translateWithNestedKeys } from '../../../utils/i18next/LocalUtil'; import { getPath, getRoleWithFqnPath } from '../../../utils/RouterUtils'; @@ -70,7 +70,6 @@ const AddRolePage = () => { const data = { name: trim(name), description, - // TODO the policies should be names instead of ID policies: selectedPolicies.map((policy) => policy), }; diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/SignUp/SignUpPage.test.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/SignUp/SignUpPage.test.tsx index d887a748be17..6189d8352393 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/SignUp/SignUpPage.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/SignUp/SignUpPage.test.tsx @@ -11,9 +11,10 @@ * limitations under the License. */ -import { act, fireEvent, render, screen } from '@testing-library/react'; +import { fireEvent, render, screen } from '@testing-library/react'; +import { act } from 'react'; import { createUser } from '../../rest/userAPI'; -import { getImages } from '../../utils/CommonUtils'; +import { getImages } from '../../utils/UserDataUtils'; import { mockChangedFormData, mockCreateUser } from './mocks/SignupData.mock'; import SignUp from './SignUpPage'; @@ -50,13 +51,12 @@ jest.mock('../../utils/ToastUtils', () => ({ showErrorToast: jest.fn().mockImplementation(() => mockShowErrorToast), })); -jest.mock('../../utils/CommonUtils', () => ({ +jest.mock('../../utils/UserDataUtils', () => ({ getImages: jest .fn() .mockResolvedValue( 'https://lh3.googleusercontent.com/a/ALm5wu0HwEPhAbyRha16cUHrEum-zxTDzj6KZiqYsT5Y=s96-c' ), - Transi18next: jest.fn().mockReturnValue('text'), })); jest.mock('../../utils/AuthProvider.util', () => ({ @@ -84,7 +84,7 @@ describe('SignUp page', () => { const emailInput = screen.getByTestId('email-input'); const selectTeamLabel = screen.getByTestId('select-team-label'); const createButton = screen.getByTestId('create-button'); - const loadingContent = await screen.queryByTestId('loading-content'); + const loadingContent = screen.queryByTestId('loading-content'); const submitButton = screen.getByTestId('create-button'); expect(logo).toBeInTheDocument(); @@ -109,13 +109,9 @@ describe('SignUp page', () => { render(); const form = screen.getByTestId('create-user-form'); - const fullNameInput = screen.getByTestId( - 'full-name-input' - ) as HTMLInputElement; - const userNameInput = screen.getByTestId( - 'username-input' - ) as HTMLInputElement; - const emailInput = screen.getByTestId('email-input') as HTMLInputElement; + const fullNameInput = screen.getByTestId('full-name-input'); + const userNameInput = screen.getByTestId('username-input'); + const emailInput = screen.getByTestId('email-input'); const submitButton = screen.getByTestId('create-button'); expect(form).toBeInTheDocument(); diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/SignUp/SignUpPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/SignUp/SignUpPage.tsx index a81d2a114d2b..da76a19f29a6 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/SignUp/SignUpPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/SignUp/SignUpPage.tsx @@ -19,11 +19,8 @@ import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; import { UserProfile } from '../../components/Auth/AuthProviders/AuthProvider.interface'; import TeamsSelectable from '../../components/Settings/Team/TeamsSelectable/TeamsSelectable'; -import { - REDIRECT_PATHNAME, - ROUTES, - VALIDATION_MESSAGES, -} from '../../constants/constants'; +import { ROUTES, VALIDATION_MESSAGES } from '../../constants/constants'; +import { REDIRECT_PATHNAME } from '../../constants/router.constants'; import { ClientType } from '../../generated/configuration/authenticationConfiguration'; import { EntityReference } from '../../generated/entity/type'; import { useApplicationStore } from '../../hooks/useApplicationStore'; @@ -33,8 +30,9 @@ import { setUrlPathnameExpiryAfterRoute, } from '../../utils/AuthProvider.util'; import brandClassBase from '../../utils/BrandData/BrandClassBase'; -import { getImages, Transi18next } from '../../utils/CommonUtils'; +import { Transi18next } from '../../utils/i18next/LocalUtil'; import { showErrorToast } from '../../utils/ToastUtils'; +import { getImages } from '../../utils/UserDataUtils'; const cookieStorage = new CookieStorage(); diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/UserListPage/UserListPageV1.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/UserListPage/UserListPageV1.tsx index 1d0d3190557a..725ec93f5e30 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/UserListPage/UserListPageV1.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/UserListPage/UserListPageV1.tsx @@ -51,9 +51,9 @@ import { usePaging } from '../../hooks/paging/usePaging'; import { useTableFilters } from '../../hooks/useTableFilters'; import { searchQuery } from '../../rest/searchAPI'; import { getUsers, restoreUser, UsersQueryParams } from '../../rest/userAPI'; -import { Transi18next } from '../../utils/CommonUtils'; import { getEntityName } from '../../utils/EntityUtils'; import { getSettingPageEntityBreadCrumb } from '../../utils/GlobalSettingsUtils'; +import { Transi18next } from '../../utils/i18next/LocalUtil'; import { getSettingPath } from '../../utils/RouterUtils'; import { getTermQuery } from '../../utils/SearchUtils'; import { showErrorToast, showSuccessToast } from '../../utils/ToastUtils'; diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/UserPage/UserPage.component.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/UserPage/UserPage.component.tsx index 430a32ea829e..c58f8e971e07 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/UserPage/UserPage.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/UserPage/UserPage.component.tsx @@ -29,7 +29,7 @@ import { Include } from '../../generated/type/include'; import { useApplicationStore } from '../../hooks/useApplicationStore'; import { useFqn } from '../../hooks/useFqn'; import { getUserByName, updateUserDetail } from '../../rest/userAPI'; -import { Transi18next } from '../../utils/CommonUtils'; +import { Transi18next } from '../../utils/i18next/LocalUtil'; import { getTermQuery } from '../../utils/SearchUtils'; import { showErrorToast, showSuccessToast } from '../../utils/ToastUtils'; diff --git a/openmetadata-ui/src/main/resources/ui/src/rest/miscAPI.ts b/openmetadata-ui/src/main/resources/ui/src/rest/miscAPI.ts index 27faf7d1d549..97d29aef00f1 100644 --- a/openmetadata-ui/src/main/resources/ui/src/rest/miscAPI.ts +++ b/openmetadata-ui/src/main/resources/ui/src/rest/miscAPI.ts @@ -12,8 +12,10 @@ */ import { AxiosResponse } from 'axios'; +import { isEmpty } from 'lodash'; import { Edge } from '../components/Entity/EntityLineage/EntityLineage.interface'; import { ExploreSearchIndex } from '../components/Explore/ExplorePage.interface'; +import { WILD_CARD_CHAR } from '../constants/char.constants'; import { PAGE_SIZE } from '../constants/constants'; import { AsyncDeleteJob } from '../context/AsyncDeleteProvider/AsyncDeleteProvider.interface'; import { SearchIndex } from '../enums/search.enum'; @@ -23,10 +25,58 @@ import { SearchRequest } from '../generated/search/searchRequest'; import { ValidationResponse } from '../generated/system/validationResponse'; import { Paging } from '../generated/type/paging'; import { SearchResponse } from '../interface/search.interface'; -import { getSearchAPIQueryParams } from '../utils/SearchUtils'; -import { escapeESReservedCharacters } from '../utils/StringsUtils'; +import { + escapeESReservedCharacters, + getEncodedFqn, +} from '../utils/StringsUtils'; import APIClient from './index'; +export const getSearchAPIQueryParams = ( + queryString: string, + from: number, + size: number, + filters: string, + sortField: string, + sortOrder: string, + searchIndex: SearchIndex | SearchIndex[], + onlyDeleted = false, + trackTotalHits = false, + wildcard = true +): Record => { + const start = (from - 1) * size; + + const encodedQueryString = queryString + ? getEncodedFqn(escapeESReservedCharacters(queryString)) + : ''; + + const query = + wildcard && encodedQueryString !== WILD_CARD_CHAR + ? `*${encodedQueryString}*` + : encodedQueryString; + + const params: Record = { + q: query + (filters ? ` AND ${filters}` : ''), + from: start, + size, + index: searchIndex, + deleted: onlyDeleted, + }; + + if (!isEmpty(sortField)) { + params.sort_field = sortField; + } + + if (!isEmpty(sortOrder)) { + params.sort_order = sortOrder; + } + + if (trackTotalHits) { + params.track_total_hits = trackTotalHits; + } + + return params; +}; + export const searchData = ( queryString: string, from: number, diff --git a/openmetadata-ui/src/main/resources/ui/src/rest/searchAPI.ts b/openmetadata-ui/src/main/resources/ui/src/rest/searchAPI.ts index 232a99ac2260..a40c31700b4d 100644 --- a/openmetadata-ui/src/main/resources/ui/src/rest/searchAPI.ts +++ b/openmetadata-ui/src/main/resources/ui/src/rest/searchAPI.ts @@ -23,7 +23,7 @@ import { SearchResponse, } from '../interface/search.interface'; import { omitDeep } from '../utils/APIUtils'; -import { getQueryWithSlash } from '../utils/SearchUtils'; +import { getQueryWithSlash } from '../utils/StringsUtils'; import APIClient from './index'; const getSearchIndexParam: ( diff --git a/openmetadata-ui/src/main/resources/ui/src/setupTests.js b/openmetadata-ui/src/main/resources/ui/src/setupTests.js index fa4c15921153..b7ff1e84e5e4 100644 --- a/openmetadata-ui/src/main/resources/ui/src/setupTests.js +++ b/openmetadata-ui/src/main/resources/ui/src/setupTests.js @@ -89,28 +89,6 @@ window.IntersectionObserver = jest.fn().mockImplementation(() => ({ disconnect: jest.fn(), })); -/** - * mock i18next - */ - -jest.mock('i18next', () => ({ - ...jest.requireActual('i18next'), - use: jest.fn(), - init: jest.fn(), - t: jest.fn().mockImplementation((key) => key), -})); - -jest.mock('utils/i18next/LocalUtil', () => ({ - useTranslation: jest.fn().mockReturnValue({ - t: (key) => key, - }), - detectBrowserLanguage: jest.fn().mockReturnValue('en-US'), - t: (key) => key, - translateWithNestedKeys: jest.fn((key, params) => { - return params ? `${key}_${JSON.stringify(params)}` : key; - }), - dir: jest.fn().mockReturnValue('ltr'), -})); /** * mock react-i18next */ @@ -228,3 +206,30 @@ jest.mock('@mui/material', () => { styled, }; }); + +jest.mock('./utils/i18next/LocalUtil', () => { + const React = require('react'); + + return { + Transi18next: jest + .fn() + .mockImplementation(({ i18nKey, renderElement, values }) => { + const valueArr = Object.values(values ?? {}); + + return React.createElement('div', { 'data-testid': i18nKey }, [ + i18nKey, + renderElement, + valueArr, + ]); + }), + __esModule: true, + default: { + t: jest.fn().mockImplementation((key) => key), + on: jest.fn(), + }, + t: jest.fn().mockImplementation((key) => key), + translateWithNestedKeys: jest.fn().mockImplementation((key, params) => { + return params ? `${key}_${JSON.stringify(params)}` : key; + }), + }; +}); diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/APIServiceUtils.ts b/openmetadata-ui/src/main/resources/ui/src/utils/APIServiceUtils.ts index 9e6a2b6b2067..9a0c1581c729 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/APIServiceUtils.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/APIServiceUtils.ts @@ -11,21 +11,15 @@ * limitations under the License. */ import { cloneDeep } from 'lodash'; -import { COMMON_UI_SCHEMA } from '../constants/Services.constant'; +import { COMMON_UI_SCHEMA } from '../constants/ServiceUISchema.constant'; import { APIServiceType } from '../generated/entity/services/apiService'; import restConnection from '../jsons/connectionSchemas/connections/api/restConnection.json'; export const getAPIConfig = (type: APIServiceType) => { let schema = {}; const uiSchema = { ...COMMON_UI_SCHEMA }; - switch (type) { - case APIServiceType.REST: - schema = restConnection; - - break; - - default: - break; + if (type === APIServiceType.REST) { + schema = restConnection; } return cloneDeep({ schema, uiSchema }); diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/APIUtils.ts b/openmetadata-ui/src/main/resources/ui/src/utils/APIUtils.ts index a56d3195cfd6..edd35871042c 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/APIUtils.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/APIUtils.ts @@ -12,7 +12,7 @@ */ import { AxiosError } from 'axios'; -import { isArray, isObject, isString, transform } from 'lodash'; +import { get, isArray, isObject, isString, transform } from 'lodash'; import { SearchIndex } from '../enums/search.enum'; import { DataProduct } from '../generated/entity/domains/dataProduct'; import { Domain } from '../generated/entity/domains/domain'; @@ -152,3 +152,21 @@ export const omitDeep = ( } }); }; + +export const getIsErrorMatch = (error: AxiosError, key: string): boolean => { + let errorMessage = ''; + + if (error) { + errorMessage = get(error, 'response.data.message', ''); + if (!errorMessage) { + // if error text is undefined or null or empty, try responseMessage in data + errorMessage = get(error, 'response.data.responseMessage', ''); + } + if (!errorMessage) { + errorMessage = get(error, 'response.data', '') as string; + errorMessage = typeof errorMessage === 'string' ? errorMessage : ''; + } + } + + return errorMessage.includes(key); +}; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/ApplicationRoutesClassBase.test.ts b/openmetadata-ui/src/main/resources/ui/src/utils/ApplicationRoutesClassBase.test.ts index 927930dc8654..39db2517477e 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/ApplicationRoutesClassBase.test.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/ApplicationRoutesClassBase.test.ts @@ -10,20 +10,78 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { FC } from 'react'; -import AuthenticatedAppRouter from '../components/AppRouter/AuthenticatedAppRouter'; -import { ApplicationRoutesClassBase } from './ApplicationRoutesClassBase'; +import { render, waitFor } from '@testing-library/react'; +import React, { FC } from 'react'; +import { UnAuthenticatedAppRouter } from '../components/AppRouter/UnAuthenticatedAppRouter'; +import { APP_ROUTER_ROUTES } from '../constants/router.constants'; +import applicationRoutesClassBase, { + ApplicationRoutesClassBase, +} from './ApplicationRoutesClassBase'; + +jest.mock('../components/AppRouter/AuthenticatedAppRouter', () => ({ + __esModule: true, + default: function AuthenticatedAppRouter() { + return React.createElement( + 'div', + { + 'data-testid': 'authenticated-app-router', + }, + 'Authenticated' + ); + }, +})); describe('ApplicationRoutesClassBase', () => { - let applicationRoutesClassBase: ApplicationRoutesClassBase; + let instance: ApplicationRoutesClassBase; beforeEach(() => { - applicationRoutesClassBase = new ApplicationRoutesClassBase(); + instance = new ApplicationRoutesClassBase(); + }); + + describe('getRouteElements', () => { + it('should return a lazy-loaded AuthenticatedAppRouter wrapped with Suspense', async () => { + const RouterComponent = instance.getRouteElements(); + + const { getByTestId } = render( + React.createElement(RouterComponent, null) + ); + + await waitFor(() => { + expect(getByTestId('authenticated-app-router')).toBeInTheDocument(); + }); + }); + + it('should return the same component reference as the default export', () => { + const result = instance.getRouteElements(); + const defaultResult = applicationRoutesClassBase.getRouteElements(); + + expect(result).toBe(defaultResult); + }); + }); + + describe('getUnAuthenticatedRouteElements', () => { + it('should return UnAuthenticatedAppRouter', () => { + const result: FC = instance.getUnAuthenticatedRouteElements(); + + expect(result).toBe(UnAuthenticatedAppRouter); + }); }); - it('should return AuthenticatedAppRouter from getRouteElements', () => { - const result: FC = applicationRoutesClassBase.getRouteElements(); + describe('isProtectedRoute', () => { + it('should identify protected routes correctly', () => { + expect(instance.isProtectedRoute('/dashboard')).toBe(true); + expect(instance.isProtectedRoute('/settings')).toBe(true); + expect(instance.isProtectedRoute('/explore')).toBe(true); + }); - expect(result).toBe(AuthenticatedAppRouter); + it('should identify unprotected routes correctly', () => { + expect(instance.isProtectedRoute(APP_ROUTER_ROUTES.SIGNIN)).toBe(false); + expect(instance.isProtectedRoute(APP_ROUTER_ROUTES.SIGNUP)).toBe(false); + expect(instance.isProtectedRoute(APP_ROUTER_ROUTES.HOME)).toBe(false); + expect(instance.isProtectedRoute(APP_ROUTER_ROUTES.FORGOT_PASSWORD)).toBe( + false + ); + expect(instance.isProtectedRoute(APP_ROUTER_ROUTES.CALLBACK)).toBe(false); + }); }); }); diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/ApplicationRoutesClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/ApplicationRoutesClassBase.ts index 1c22cea5d864..cf8250ab69d6 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/ApplicationRoutesClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/ApplicationRoutesClassBase.ts @@ -11,10 +11,14 @@ * limitations under the License. */ -import { FC } from 'react'; -import AuthenticatedAppRouter from '../components/AppRouter/AuthenticatedAppRouter'; +import { FC, lazy } from 'react'; import { UnAuthenticatedAppRouter } from '../components/AppRouter/UnAuthenticatedAppRouter'; -import { ROUTES } from '../constants/constants'; +import withSuspenseFallback from '../components/AppRouter/withSuspenseFallback'; +import { UNPROTECTED_ROUTES } from '../constants/router.constants'; + +const AuthenticatedAppRouter = withSuspenseFallback( + lazy(() => import('../components/AppRouter/AuthenticatedAppRouter')) +); class ApplicationRoutesClassBase { public getRouteElements(): FC { @@ -26,22 +30,7 @@ class ApplicationRoutesClassBase { } public isProtectedRoute(pathname: string): boolean { - return ( - [ - ROUTES.SIGNUP, - ROUTES.SIGNIN, - ROUTES.FORGOT_PASSWORD, - ROUTES.CALLBACK, - ROUTES.SILENT_CALLBACK, - ROUTES.REGISTER, - ROUTES.RESET_PASSWORD, - ROUTES.ACCOUNT_ACTIVATION, - ROUTES.HOME, - ROUTES.AUTH_CALLBACK, - ROUTES.NOT_FOUND, - ROUTES.LOGOUT, - ].indexOf(pathname) === -1 - ); + return !UNPROTECTED_ROUTES.has(pathname); } } diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/AuthProvider.util.ts b/openmetadata-ui/src/main/resources/ui/src/utils/AuthProvider.util.ts index ae6dd95cc1c6..513f43c7dfda 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/AuthProvider.util.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/AuthProvider.util.ts @@ -11,12 +11,7 @@ * limitations under the License. */ -import { - AuthenticationResult, - BrowserCacheLocation, - Configuration, - PopupRequest, -} from '@azure/msal-browser'; +import type { AuthenticationResult, Configuration } from '@azure/msal-browser'; import { CookieStorage } from 'cookie-storage'; import jwtDecode, { JwtPayload } from 'jwt-decode'; import { first, get, isEmpty, isNil } from 'lodash'; @@ -26,8 +21,9 @@ import { OidcUser, UserProfile, } from '../components/Auth/AuthProviders/AuthProvider.interface'; -import { REDIRECT_PATHNAME, ROUTES } from '../constants/constants'; +import { ROUTES } from '../constants/constants'; import { EMAIL_REG_EX } from '../constants/regex.constants'; +import { REDIRECT_PATHNAME } from '../constants/router.constants'; import { AuthenticationConfiguration, ClientType, @@ -188,7 +184,7 @@ export const getAuthConfig = ( postLogoutRedirectUri: '/', }, cache: { - cacheLocation: BrowserCacheLocation.LocalStorage, + cacheLocation: 'localStorage', }, provider, enableSelfSignup, @@ -207,7 +203,7 @@ export const getAuthConfig = ( postLogoutRedirectUri: '/', }, cache: { - cacheLocation: BrowserCacheLocation.LocalStorage, + cacheLocation: 'localStorage', }, provider, clientType, @@ -222,9 +218,9 @@ export const getAuthConfig = ( }; // Add here scopes for id token to be used at MS Identity Platform endpoints. -export const msalLoginRequest: PopupRequest = { +export const msalLoginRequest = { scopes: ['openid', 'profile', 'email', 'offline_access'], -}; +} as const; export const getNameFromEmail = (email: string) => { if (new RegExp(EMAIL_REG_EX).exec(email)) { @@ -279,8 +275,8 @@ export const extractNameFromUserProfile = (user: UserProfile): string => { return user.name.trim(); } - const givenName = get(user, 'given_name', ''); - const familyName = get(user, 'family_name', ''); + const givenName: string = get(user, 'given_name', ''); + const familyName: string = get(user, 'family_name', ''); if (givenName && familyName) { return `${givenName.trim()} ${familyName.trim()}`; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/CommonUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/CommonUtils.tsx index a1031c9711e3..0ffebc873e1e 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/CommonUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/CommonUtils.tsx @@ -34,10 +34,8 @@ import { RecentlyViewedData, } from 'Models'; import { ReactNode } from 'react'; -import { Trans } from 'react-i18next'; import Loader from '../components/common/Loader/Loader'; import { FQN_SEPARATOR_CHAR } from '../constants/char.constants'; -import { imageTypes } from '../constants/constants'; import { BASE_COLORS } from '../constants/DataInsight.constants'; import { FEED_COUNT_INITIAL_DATA } from '../constants/entity.constants'; import { VALIDATE_ESCAPE_START_END_REGEX } from '../constants/regex.constants'; @@ -52,7 +50,7 @@ import { getFeedCount } from '../rest/feedsAPI'; import brandClassBase from './BrandData/BrandClassBase'; import { getEntityFeedLink } from './EntityUtils'; import Fqn from './Fqn'; -import i18n, { t } from './i18next/LocalUtil'; +import i18n, { t, Transi18next } from './i18next/LocalUtil'; import serviceUtilClassBase from './ServiceUtilClassBase'; import { showErrorToast } from './ToastUtils'; @@ -344,18 +342,6 @@ export const requiredField = (label: string, excludeSpace = false) => ( ); -export const getImages = (imageUri: string) => { - const imagesObj: typeof imageTypes = imageTypes; - for (const type in imageTypes) { - imagesObj[type as keyof typeof imageTypes] = imageUri.replace( - 's96-c', - imageTypes[type as keyof typeof imageTypes] - ); - } - - return imagesObj; -}; - export const getServiceLogo = ( serviceType: string, className = '' @@ -647,21 +633,6 @@ export const getTrimmedContent = (content: string, limit: number) => { return refinedContent.join(' '); }; -export const Transi18next = ({ - i18nKey, - values, - renderElement, - ...otherProps -}: { - i18nKey: string; - values?: object; - renderElement: ReactNode; -}): JSX.Element => ( - - {renderElement} - -); - export const getEntityDeleteMessage = (entity: string, dependents: string) => { if (dependents) { return t('message.permanently-delete-metadata-and-dependents', { @@ -703,24 +674,6 @@ export const reducerWithoutAction = (state: S, action: A) => { */ export const getBase64EncodedString = (text: string): string => btoa(text); -export const getIsErrorMatch = (error: AxiosError, key: string): boolean => { - let errorMessage = ''; - - if (error) { - errorMessage = get(error, 'response.data.message', ''); - if (!errorMessage) { - // if error text is undefined or null or empty, try responseMessage in data - errorMessage = get(error, 'response.data.responseMessage', ''); - } - if (!errorMessage) { - errorMessage = get(error, 'response.data', '') as string; - errorMessage = typeof errorMessage === 'string' ? errorMessage : ''; - } - } - - return errorMessage.includes(key); -}; - /** * @param color hex have color code * @param opacity take opacity how much to reduce it diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/CoverImageUploadUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/CoverImageUploadUtils.tsx index d7adcb088186..2888c29274aa 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/CoverImageUploadUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/CoverImageUploadUtils.tsx @@ -18,7 +18,7 @@ import imageClassBase from '../components/BlockEditor/Extensions/image/ImageClas import { CoverImageFileValue } from '../components/common/CoverImageUpload/CoverImageUpload.interface'; import { ERROR_MESSAGE } from '../constants/constants'; import { EntityType } from '../enums/entity.enum'; -import { getIsErrorMatch } from './CommonUtils'; +import { getIsErrorMatch } from './APIUtils'; import { showNotistackError, showNotistackSuccess, diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/CustomizableLandingPageUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/CustomizableLandingPageUtils.tsx index eba886ea6c43..713ebe74530f 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/CustomizableLandingPageUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/CustomizableLandingPageUtils.tsx @@ -12,7 +12,6 @@ */ import Icon from '@ant-design/icons'; -import i18next from 'i18next'; import { capitalize, isUndefined, uniqBy, uniqueId } from 'lodash'; import { DOMAttributes } from 'react'; import { Layout } from 'react-grid-layout'; @@ -22,6 +21,7 @@ import { LandingPageWidgetKeys } from '../enums/CustomizablePage.enum'; import { Document } from '../generated/entity/docStore/document'; import { WidgetConfig } from '../pages/CustomizablePage/CustomizablePage.interface'; import customizeMyDataPageClassBase from './CustomizeMyDataPageClassBase'; +import i18n from './i18next/LocalUtil'; /** * Ensures widget width doesn't exceed the maximum allowed width of 2 @@ -371,11 +371,11 @@ export const getRemoveWidgetHandler = export const getWidgetWidthLabelFromKey = (widgetKey: string): string => { switch (widgetKey) { case 'large': - return i18next.t('label.large'); + return i18n.t('label.large'); case 'medium': - return i18next.t('label.medium'); + return i18n.t('label.medium'); case 'small': - return i18next.t('label.small'); + return i18n.t('label.small'); default: return capitalize(widgetKey); } diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/DashboardServiceUtils.ts b/openmetadata-ui/src/main/resources/ui/src/utils/DashboardServiceUtils.ts index 5c71999498c6..8a1135061852 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/DashboardServiceUtils.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/DashboardServiceUtils.ts @@ -12,7 +12,7 @@ */ import { cloneDeep, isEmpty, isUndefined } from 'lodash'; -import { COMMON_UI_SCHEMA } from '../constants/Services.constant'; +import { COMMON_UI_SCHEMA } from '../constants/ServiceUISchema.constant'; import { DashboardConnection, DashboardServiceType, diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/DataAssetSummaryPanelUtils.test.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/DataAssetSummaryPanelUtils.test.tsx new file mode 100644 index 000000000000..cf4c15964c84 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/utils/DataAssetSummaryPanelUtils.test.tsx @@ -0,0 +1,81 @@ +/* + * Copyright 2026 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 { ExplorePageTabs } from '../enums/Explore.enum'; +import { MOCK_CHART_DATA } from '../mocks/Chart.mock'; +import { MOCK_TABLE, MOCK_TIER_DATA } from '../mocks/TableData.mock'; +import { getEntityOverview } from './DataAssetSummaryPanelUtils'; +import { getTierTags } from './TableUtils'; + +jest.mock('./TableUtils', () => ({ + getTierTags: jest.fn(), + getUsagePercentile: jest.fn().mockImplementation((value) => value + 'th'), +})); + +jest.mock('./CommonUtils', () => ({ + getPartialNameFromTableFQN: jest.fn().mockImplementation((value) => value), + getTableFQNFromColumnFQN: jest.fn().mockImplementation((value) => value), + formatNumberWithComma: jest.fn().mockImplementation((value) => value), +})); + +describe('getEntityOverview', () => { + it('should call getChartOverview and get ChartData if ExplorePageTabs is charts', () => { + const result = JSON.stringify( + getEntityOverview(ExplorePageTabs.CHARTS, { + ...MOCK_CHART_DATA, + dataProducts: [], + }) + ); + + expect(result).toContain('label.owner-plural'); + expect(result).toContain('label.chart'); + expect(result).toContain('label.url-uppercase'); + expect(result).toContain('Are you an ethnic minority in your city?'); + expect(result).toContain( + `http://localhost:8088/superset/explore/?form_data=%7B%22slice_id%22%3A%20127%7D` + ); + expect(result).toContain('label.service'); + expect(result).toContain('sample_superset'); + expect(result).toContain('Other'); + expect(result).toContain('label.service-type'); + expect(result).toContain('Superset'); + }); + + it('should call getChartOverview and get TableData if ExplorePageTabs is table', () => { + const result = JSON.stringify( + getEntityOverview(ExplorePageTabs.TABLES, { + ...MOCK_TABLE, + tags: [MOCK_TIER_DATA], + dataProducts: [], + }) + ); + + expect(result).toContain('label.owner-plural'); + expect(result).toContain('label.type'); + expect(result).toContain('label.service'); + expect(result).toContain('label.database'); + expect(result).toContain('label.schema'); + expect(result).toContain('label.tier'); + expect(result).toContain('label.usage'); + expect(result).toContain('label.query-plural'); + expect(result).toContain('label.column-plural'); + expect(result).toContain('label.row-plural'); + expect(getTierTags).toHaveBeenCalledWith([MOCK_TIER_DATA]); + expect(result).toContain('Regular'); + expect(result).toContain('sample_data'); + expect(result).toContain('ecommerce_db'); + expect(result).toContain('shopify'); + expect(result).toContain('0th'); + expect(result).toContain('4'); + expect(result).toContain('14567'); + }); +}); diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/DataAssetSummaryPanelUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/DataAssetSummaryPanelUtils.tsx new file mode 100644 index 000000000000..f1771765fb2d --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/utils/DataAssetSummaryPanelUtils.tsx @@ -0,0 +1,1417 @@ +/* + * Copyright 2026 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 { isEmpty, isNil, isObject, isUndefined } from 'lodash'; +import { DomainLabel } from '../components/common/DomainLabel/DomainLabel.component'; +import { OwnerLabel } from '../components/common/OwnerLabel/OwnerLabel.component'; +import QueryCount from '../components/common/QueryCount/QueryCount.component'; +import { DataAssetSummaryPanelProps } from '../components/DataAssetSummaryPanelV1/DataAssetSummaryPanelV1.interface'; +import { ProfilerTabPath } from '../components/Database/Profiler/ProfilerDashboard/profilerDashboard.interface'; +import { EntityServiceUnion } from '../components/Explore/ExplorePage.interface'; +import TagsV1 from '../components/Tag/TagsV1/TagsV1.component'; +import { FQN_SEPARATOR_CHAR } from '../constants/char.constants'; +import { NO_DATA } from '../constants/constants'; +import { TAG_START_WITH } from '../constants/Tag.constants'; +import { EntityTabs, EntityType, FqnPart } from '../enums/entity.enum'; +import { ExplorePageTabs } from '../enums/Explore.enum'; +import { ServiceCategory } from '../enums/service.enum'; +import { APICollection } from '../generated/entity/data/apiCollection'; +import { APIEndpoint } from '../generated/entity/data/apiEndpoint'; +import { Chart } from '../generated/entity/data/chart'; +import { Container } from '../generated/entity/data/container'; +import { Dashboard } from '../generated/entity/data/dashboard'; +import { DashboardDataModel } from '../generated/entity/data/dashboardDataModel'; +import { Database } from '../generated/entity/data/database'; +import { DatabaseSchema } from '../generated/entity/data/databaseSchema'; +import { Directory } from '../generated/entity/data/directory'; +import { File } from '../generated/entity/data/file'; +import { Metric } from '../generated/entity/data/metric'; +import { Mlmodel } from '../generated/entity/data/mlmodel'; +import { SearchIndex } from '../generated/entity/data/searchIndex'; +import { Spreadsheet } from '../generated/entity/data/spreadsheet'; +import { + StoredProcedure, + StoredProcedureCodeObject, +} from '../generated/entity/data/storedProcedure'; +import { Table, TableType, TagLabel } from '../generated/entity/data/table'; +import { Topic } from '../generated/entity/data/topic'; +import { Worksheet } from '../generated/entity/data/worksheet'; + +import { Pipeline } from '../generated/entity/data/pipeline'; +import { EntityReference } from '../generated/entity/type'; +import { UsageDetails } from '../generated/type/usageDetails'; +import { + formatNumberWithComma, + getPartialNameFromTableFQN, +} from './CommonUtils'; +import { DRAWER_NAVIGATION_OPTIONS, getEntityName } from './EntityUtils'; +import { BasicEntityOverviewInfo } from './EntityUtils.interface'; +import i18n from './i18next/LocalUtil'; +import { getEntityDetailsPath, getServiceDetailsPath } from './RouterUtils'; +import { bytesToSize, stringToHTML } from './StringsUtils'; +import { getTierTags, getUsagePercentile } from './TableUtils'; + +interface ColumnSearchResult { + dataType?: string; + dataTypeDisplay?: string; + constraint?: string; + table?: { + name?: string; + displayName?: string; + fullyQualifiedName?: string; + }; + service?: { + name?: string; + displayName?: string; + fullyQualifiedName?: string; + type?: string; + }; + database?: { + name?: string; + displayName?: string; + fullyQualifiedName?: string; + }; + databaseSchema?: { + name?: string; + displayName?: string; + fullyQualifiedName?: string; + }; + owners?: EntityReference[]; + domains?: EntityReference[]; +} + +const entityTierRenderer = (tier?: TagLabel) => { + return tier ? ( + + ) : ( + NO_DATA + ); +}; + +const getUsageData = (usageSummary: UsageDetails | undefined) => + isNil(usageSummary?.weeklyStats?.percentileRank) + ? NO_DATA + : getUsagePercentile(usageSummary?.weeklyStats?.percentileRank ?? 0); + +const getTableFieldsFromTableDetails = (tableDetails: Table) => { + const { + fullyQualifiedName, + owners, + tags, + usageSummary, + profile, + columns, + tableType, + service, + database, + databaseSchema, + domains, + } = tableDetails; + const [serviceName, databaseName, schemaName] = getPartialNameFromTableFQN( + fullyQualifiedName ?? '', + [FqnPart.Service, FqnPart.Database, FqnPart.Schema], + FQN_SEPARATOR_CHAR + ).split(FQN_SEPARATOR_CHAR); + + const serviceDisplayName = getEntityName(service) || serviceName; + const databaseDisplayName = getEntityName(database) || databaseName; + const schemaDisplayName = getEntityName(databaseSchema) || schemaName; + + const tier = getTierTags(tags ?? []); + + return { + fullyQualifiedName, + owners, + service: serviceDisplayName, + database: databaseDisplayName, + schema: schemaDisplayName, + tier, + usage: getUsageData(usageSummary), + profile, + columns, + tableType, + domains, + }; +}; + +const getCommonOverview = ( + { + owners, + domains, + }: { + owners?: EntityReference[]; + domains?: EntityReference[]; + }, + showOwner = true +) => { + return [ + ...(showOwner + ? [ + { + name: i18n.t('label.owner-plural'), + value: ( + + ), + visible: [DRAWER_NAVIGATION_OPTIONS.lineage], + }, + ] + : []), + { + name: i18n.t('label.domain-plural'), + value: ( + + ), + visible: [DRAWER_NAVIGATION_OPTIONS.lineage], + }, + ]; +}; + +const getTableOverview = ( + tableDetails: Table, + additionalInfo?: Record +) => { + const { + fullyQualifiedName, + owners, + profile, + columns, + tableType, + service, + database, + schema, + tier, + usage, + domains, + } = getTableFieldsFromTableDetails(tableDetails); + + const overview: BasicEntityOverviewInfo[] = [ + ...getCommonOverview({ owners, domains }), + { + name: i18n.t('label.type'), + value: tableType ?? TableType.Regular, + isLink: false, + visible: [ + DRAWER_NAVIGATION_OPTIONS.lineage, + DRAWER_NAVIGATION_OPTIONS.explore, + ], + }, + { + name: i18n.t('label.service'), + value: service || NO_DATA, + url: getServiceDetailsPath(service, ServiceCategory.DATABASE_SERVICES), + isLink: true, + visible: [DRAWER_NAVIGATION_OPTIONS.lineage], + }, + { + name: i18n.t('label.database'), + value: database || NO_DATA, + url: getEntityDetailsPath( + EntityType.DATABASE, + getPartialNameFromTableFQN( + fullyQualifiedName ?? '', + [FqnPart.Service, FqnPart.Database], + FQN_SEPARATOR_CHAR + ) + ), + isLink: true, + visible: [DRAWER_NAVIGATION_OPTIONS.lineage], + }, + { + name: i18n.t('label.schema'), + value: schema || NO_DATA, + url: getEntityDetailsPath( + EntityType.DATABASE_SCHEMA, + getPartialNameFromTableFQN( + fullyQualifiedName ?? '', + [FqnPart.Service, FqnPart.Database, FqnPart.Schema], + FQN_SEPARATOR_CHAR + ) + ), + isLink: true, + visible: [DRAWER_NAVIGATION_OPTIONS.lineage], + }, + { + name: i18n.t('label.tier'), + value: entityTierRenderer(tier), + isLink: false, + visible: [DRAWER_NAVIGATION_OPTIONS.lineage], + }, + { + name: i18n.t('label.usage'), + value: usage || NO_DATA, + isLink: false, + visible: [DRAWER_NAVIGATION_OPTIONS.lineage], + }, + { + name: i18n.t('label.query-plural'), + value: , + isLink: false, + visible: [ + DRAWER_NAVIGATION_OPTIONS.lineage, + DRAWER_NAVIGATION_OPTIONS.explore, + ], + }, + { + name: i18n.t('label.column-plural'), + value: columns ? columns.length : NO_DATA, + isLink: false, + visible: [ + DRAWER_NAVIGATION_OPTIONS.lineage, + DRAWER_NAVIGATION_OPTIONS.explore, + ], + }, + { + name: i18n.t('label.row-plural'), + value: + !isUndefined(profile) && profile?.rowCount + ? formatNumberWithComma(profile.rowCount) + : NO_DATA, + isLink: false, + visible: [DRAWER_NAVIGATION_OPTIONS.lineage], + }, + { + name: i18n.t('label.incident-plural'), + value: additionalInfo?.incidentCount ?? 0, + isLink: true, + linkProps: { + pathname: getEntityDetailsPath( + EntityType.TABLE, + fullyQualifiedName ?? '', + EntityTabs.PROFILER, + ProfilerTabPath.INCIDENTS + ), + }, + visible: [ + DRAWER_NAVIGATION_OPTIONS.lineage, + DRAWER_NAVIGATION_OPTIONS.explore, + ], + }, + ]; + + return overview; +}; + +const getTopicOverview = (topicDetails: Topic) => { + const { + domains, + partitions, + replicationFactor, + retentionSize, + cleanupPolicies, + maximumMessageSize, + messageSchema, + } = topicDetails; + + const overview: BasicEntityOverviewInfo[] = [ + ...getCommonOverview({ domains, owners: topicDetails.owners }), + { + name: i18n.t('label.partition-plural'), + value: partitions ?? NO_DATA, + isLink: false, + visible: [ + DRAWER_NAVIGATION_OPTIONS.lineage, + DRAWER_NAVIGATION_OPTIONS.explore, + ], + }, + { + name: i18n.t('label.replication-factor'), + value: replicationFactor, + isLink: false, + visible: [ + DRAWER_NAVIGATION_OPTIONS.lineage, + DRAWER_NAVIGATION_OPTIONS.explore, + ], + }, + { + name: i18n.t('label.retention-size'), + value: bytesToSize(retentionSize ?? 0), + isLink: false, + visible: [ + DRAWER_NAVIGATION_OPTIONS.lineage, + DRAWER_NAVIGATION_OPTIONS.explore, + ], + }, + { + name: i18n.t('label.clean-up-policy-plural'), + value: cleanupPolicies ? cleanupPolicies.join(', ') : NO_DATA, + isLink: false, + visible: [ + DRAWER_NAVIGATION_OPTIONS.lineage, + DRAWER_NAVIGATION_OPTIONS.explore, + ], + }, + { + name: i18n.t('label.max-message-size'), + value: bytesToSize(maximumMessageSize ?? 0), + isLink: false, + visible: [ + DRAWER_NAVIGATION_OPTIONS.lineage, + DRAWER_NAVIGATION_OPTIONS.explore, + ], + }, + { + name: i18n.t('label.schema-type'), + value: messageSchema?.schemaType ?? NO_DATA, + isLink: false, + visible: [ + DRAWER_NAVIGATION_OPTIONS.lineage, + DRAWER_NAVIGATION_OPTIONS.explore, + ], + }, + ]; + + return overview; +}; + +const getPipelineOverview = (pipelineDetails: Pipeline) => { + const { owners, tags, sourceUrl, service, displayName, domains } = + pipelineDetails; + const tier = getTierTags(tags ?? []); + const serviceDisplayName = getEntityName(service); + + const overview: BasicEntityOverviewInfo[] = [ + ...getCommonOverview({ owners, domains }), + { + name: `${i18n.t('label.pipeline')} ${i18n.t('label.url-uppercase')}`, + dataTestId: 'pipeline-url-label', + value: stringToHTML(displayName ?? '') || NO_DATA, + url: sourceUrl, + isLink: true, + isExternal: true, + visible: [ + DRAWER_NAVIGATION_OPTIONS.lineage, + DRAWER_NAVIGATION_OPTIONS.explore, + ], + }, + { + name: i18n.t('label.service'), + value: serviceDisplayName || NO_DATA, + url: getServiceDetailsPath( + service?.name ?? '', + ServiceCategory.PIPELINE_SERVICES + ), + isLink: true, + isExternal: false, + visible: [DRAWER_NAVIGATION_OPTIONS.lineage], + }, + { + name: i18n.t('label.tier'), + value: entityTierRenderer(tier), + isLink: false, + visible: [DRAWER_NAVIGATION_OPTIONS.lineage], + }, + ]; + + return overview; +}; + +const getDashboardOverview = (dashboardDetails: Dashboard) => { + const { owners, tags, sourceUrl, service, displayName, project, domains } = + dashboardDetails; + const tier = getTierTags(tags ?? []); + const serviceDisplayName = getEntityName(service); + + const overview: BasicEntityOverviewInfo[] = [ + ...getCommonOverview({ owners, domains }), + { + name: `${i18n.t('label.dashboard')} ${i18n.t('label.url-uppercase')}`, + value: stringToHTML(displayName ?? '') || NO_DATA, + url: sourceUrl, + isLink: true, + isExternal: true, + visible: [ + DRAWER_NAVIGATION_OPTIONS.lineage, + DRAWER_NAVIGATION_OPTIONS.explore, + ], + }, + { + name: i18n.t('label.service'), + value: serviceDisplayName || NO_DATA, + url: getServiceDetailsPath( + service?.name ?? '', + ServiceCategory.DASHBOARD_SERVICES + ), + isExternal: false, + isLink: true, + visible: [DRAWER_NAVIGATION_OPTIONS.lineage], + }, + { + name: i18n.t('label.tier'), + value: entityTierRenderer(tier), + isLink: false, + isExternal: false, + visible: [DRAWER_NAVIGATION_OPTIONS.lineage], + }, + { + name: i18n.t('label.project'), + value: project ?? NO_DATA, + isLink: false, + visible: [ + DRAWER_NAVIGATION_OPTIONS.explore, + DRAWER_NAVIGATION_OPTIONS.lineage, + ], + }, + ]; + + return overview; +}; + +export const getSearchIndexOverview = (searchIndexDetails: SearchIndex) => { + const { owners, tags, service, domains } = searchIndexDetails; + const tier = getTierTags(tags ?? []); + + const overview: BasicEntityOverviewInfo[] = [ + ...getCommonOverview({ owners, domains }), + { + name: i18n.t('label.tier'), + value: entityTierRenderer(tier), + isLink: false, + isExternal: false, + visible: [DRAWER_NAVIGATION_OPTIONS.lineage], + }, + { + name: i18n.t('label.service'), + value: service?.fullyQualifiedName ?? NO_DATA, + url: getServiceDetailsPath( + service?.name ?? '', + ServiceCategory.SEARCH_SERVICES + ), + isExternal: false, + isLink: true, + visible: [DRAWER_NAVIGATION_OPTIONS.lineage], + }, + ]; + + return overview; +}; + +const getMlModelOverview = (mlModelDetails: Mlmodel) => { + const { algorithm, target, server, dashboard, owners, domains } = + mlModelDetails; + + const overview: BasicEntityOverviewInfo[] = [ + ...getCommonOverview({ owners, domains }), + { + name: i18n.t('label.algorithm'), + value: algorithm || NO_DATA, + url: '', + isLink: false, + visible: [ + DRAWER_NAVIGATION_OPTIONS.lineage, + DRAWER_NAVIGATION_OPTIONS.explore, + ], + }, + { + name: i18n.t('label.target'), + value: target ?? NO_DATA, + url: '', + isLink: false, + visible: [ + DRAWER_NAVIGATION_OPTIONS.lineage, + DRAWER_NAVIGATION_OPTIONS.explore, + ], + }, + { + name: i18n.t('label.server'), + value: server ?? NO_DATA, + url: server, + isLink: Boolean(server), + isExternal: true, + visible: [ + DRAWER_NAVIGATION_OPTIONS.lineage, + DRAWER_NAVIGATION_OPTIONS.explore, + ], + }, + { + name: i18n.t('label.dashboard'), + value: getEntityName(dashboard) || NO_DATA, + url: getEntityDetailsPath( + EntityType.DASHBOARD, + dashboard?.fullyQualifiedName ?? '' + ), + isLink: true, + isExternal: false, + visible: [ + DRAWER_NAVIGATION_OPTIONS.lineage, + DRAWER_NAVIGATION_OPTIONS.explore, + ], + }, + ]; + + return overview; +}; + +const getContainerOverview = (containerDetails: Container) => { + const { numberOfObjects, serviceType, dataModel, owners, domains } = + containerDetails; + + const visible = [ + DRAWER_NAVIGATION_OPTIONS.lineage, + DRAWER_NAVIGATION_OPTIONS.explore, + ]; + + const overview: BasicEntityOverviewInfo[] = [ + ...getCommonOverview({ owners, domains }), + { + name: i18n.t('label.object-plural'), + value: numberOfObjects, + isLink: false, + visible, + }, + { + name: i18n.t('label.service-type'), + value: serviceType, + isLink: false, + visible, + }, + { + name: i18n.t('label.column-plural'), + value: + !isUndefined(dataModel) && dataModel.columns + ? dataModel.columns.length + : NO_DATA, + isLink: false, + visible, + }, + ]; + + return overview; +}; + +const getChartOverview = (chartDetails: Chart) => { + const { + owners, + sourceUrl, + chartType, + service, + serviceType, + displayName, + domains, + } = chartDetails; + const serviceDisplayName = getEntityName(service); + + const overview: BasicEntityOverviewInfo[] = [ + ...getCommonOverview({ owners, domains }), + { + name: `${i18n.t('label.chart')} ${i18n.t('label.url-uppercase')}`, + value: stringToHTML(displayName ?? '') || NO_DATA, + url: sourceUrl, + isLink: true, + isExternal: true, + visible: [ + DRAWER_NAVIGATION_OPTIONS.lineage, + DRAWER_NAVIGATION_OPTIONS.explore, + ], + }, + { + name: i18n.t('label.service'), + value: serviceDisplayName || NO_DATA, + url: getServiceDetailsPath( + service?.name ?? '', + ServiceCategory.DASHBOARD_SERVICES + ), + isExternal: false, + isLink: true, + visible: [ + DRAWER_NAVIGATION_OPTIONS.lineage, + DRAWER_NAVIGATION_OPTIONS.explore, + ], + }, + { + name: i18n.t('label.chart-type'), + value: chartType ?? NO_DATA, + isLink: false, + visible: [ + DRAWER_NAVIGATION_OPTIONS.explore, + DRAWER_NAVIGATION_OPTIONS.lineage, + ], + }, + { + name: i18n.t('label.service-type'), + value: serviceType ?? NO_DATA, + isLink: false, + visible: [ + DRAWER_NAVIGATION_OPTIONS.explore, + DRAWER_NAVIGATION_OPTIONS.lineage, + ], + }, + ]; + + return overview; +}; + +const getDataModelOverview = (dataModelDetails: DashboardDataModel) => { + const { + owners, + tags, + service, + domains, + displayName, + dataModelType, + fullyQualifiedName, + } = dataModelDetails; + const tier = getTierTags(tags ?? []); + + const overview: BasicEntityOverviewInfo[] = [ + ...getCommonOverview({ owners, domains }), + { + name: `${i18n.t('label.data-model')} ${i18n.t('label.url-uppercase')}`, + value: stringToHTML(displayName ?? '') || NO_DATA, + url: getEntityDetailsPath( + EntityType.DASHBOARD_DATA_MODEL, + fullyQualifiedName ?? '' + ), + isLink: true, + visible: [ + DRAWER_NAVIGATION_OPTIONS.lineage, + DRAWER_NAVIGATION_OPTIONS.explore, + ], + }, + { + name: i18n.t('label.service'), + value: service?.fullyQualifiedName ?? NO_DATA, + url: getServiceDetailsPath( + service?.name ?? '', + ServiceCategory.DASHBOARD_SERVICES + ), + isExternal: false, + isLink: true, + visible: [ + DRAWER_NAVIGATION_OPTIONS.lineage, + DRAWER_NAVIGATION_OPTIONS.explore, + ], + }, + + { + name: i18n.t('label.tier'), + value: entityTierRenderer(tier), + isLink: false, + isExternal: false, + visible: [ + DRAWER_NAVIGATION_OPTIONS.lineage, + DRAWER_NAVIGATION_OPTIONS.explore, + ], + }, + { + name: i18n.t('label.data-model-type'), + value: dataModelType, + isLink: false, + isExternal: false, + visible: [ + DRAWER_NAVIGATION_OPTIONS.lineage, + DRAWER_NAVIGATION_OPTIONS.explore, + ], + }, + ]; + + return overview; +}; + +const getStoredProcedureOverview = ( + storedProcedureDetails: StoredProcedure +) => { + const { fullyQualifiedName, owners, tags, domains, storedProcedureCode } = + storedProcedureDetails; + const [service, database, schema] = getPartialNameFromTableFQN( + fullyQualifiedName ?? '', + [FqnPart.Service, FqnPart.Database, FqnPart.Schema], + FQN_SEPARATOR_CHAR + ).split(FQN_SEPARATOR_CHAR); + + const tier = getTierTags(tags ?? []); + + const overview: BasicEntityOverviewInfo[] = [ + ...getCommonOverview({ owners, domains }), + { + name: i18n.t('label.service'), + value: service || NO_DATA, + url: getServiceDetailsPath(service, ServiceCategory.DATABASE_SERVICES), + isLink: true, + visible: [DRAWER_NAVIGATION_OPTIONS.lineage], + }, + { + name: i18n.t('label.database'), + value: database || NO_DATA, + url: getEntityDetailsPath( + EntityType.DATABASE, + getPartialNameFromTableFQN( + fullyQualifiedName ?? '', + [FqnPart.Service, FqnPart.Database], + FQN_SEPARATOR_CHAR + ) + ), + isLink: true, + visible: [DRAWER_NAVIGATION_OPTIONS.lineage], + }, + { + name: i18n.t('label.schema'), + value: schema || NO_DATA, + url: getEntityDetailsPath( + EntityType.DATABASE_SCHEMA, + getPartialNameFromTableFQN( + fullyQualifiedName ?? '', + [FqnPart.Service, FqnPart.Database, FqnPart.Schema], + FQN_SEPARATOR_CHAR + ) + ), + isLink: true, + visible: [ + DRAWER_NAVIGATION_OPTIONS.lineage, + DRAWER_NAVIGATION_OPTIONS.explore, + ], + }, + { + name: i18n.t('label.tier'), + value: entityTierRenderer(tier), + isLink: false, + visible: [ + DRAWER_NAVIGATION_OPTIONS.lineage, + DRAWER_NAVIGATION_OPTIONS.explore, + ], + }, + ...(isObject(storedProcedureCode) + ? [ + { + name: i18n.t('label.language'), + value: + (storedProcedureCode as StoredProcedureCodeObject).language ?? + NO_DATA, + isLink: false, + visible: [ + DRAWER_NAVIGATION_OPTIONS.lineage, + DRAWER_NAVIGATION_OPTIONS.explore, + ], + }, + ] + : []), + ]; + + return overview; +}; + +const getDatabaseOverview = (databaseDetails: Database) => { + const { owners, service, domains, tags, usageSummary } = databaseDetails; + + const tier = getTierTags(tags ?? []); + + const overview: BasicEntityOverviewInfo[] = [ + { + name: i18n.t('label.owner-plural'), + value: , + visible: [DRAWER_NAVIGATION_OPTIONS.explore], + }, + ...getCommonOverview({ domains }, false), + { + name: i18n.t('label.tier'), + value: entityTierRenderer(tier), + isLink: false, + visible: [DRAWER_NAVIGATION_OPTIONS.explore], + }, + { + name: i18n.t('label.service'), + value: service?.fullyQualifiedName || NO_DATA, + url: getServiceDetailsPath( + service?.fullyQualifiedName ?? '', + ServiceCategory.DATABASE_SERVICES + ), + isLink: true, + visible: [DRAWER_NAVIGATION_OPTIONS.explore], + }, + + { + name: i18n.t('label.usage'), + value: getUsageData(usageSummary), + isLink: false, + visible: [DRAWER_NAVIGATION_OPTIONS.explore], + }, + ]; + + return overview; +}; + +const getDatabaseSchemaOverview = (databaseSchemaDetails: DatabaseSchema) => { + const { owners, service, tags, domains, usageSummary, database } = + databaseSchemaDetails; + + const tier = getTierTags(tags ?? []); + + const overview: BasicEntityOverviewInfo[] = [ + { + name: i18n.t('label.owner-plural'), + value: , + visible: [DRAWER_NAVIGATION_OPTIONS.explore], + }, + ...getCommonOverview({ domains }, false), + { + name: i18n.t('label.tier'), + value: entityTierRenderer(tier), + isLink: false, + visible: [DRAWER_NAVIGATION_OPTIONS.explore], + }, + { + name: i18n.t('label.service'), + value: service?.fullyQualifiedName ?? NO_DATA, + url: getServiceDetailsPath( + service?.fullyQualifiedName ?? '', + ServiceCategory.DATABASE_SERVICES + ), + isLink: true, + visible: [DRAWER_NAVIGATION_OPTIONS.explore], + }, + { + name: i18n.t('label.database'), + value: database?.fullyQualifiedName ?? NO_DATA, + url: getEntityDetailsPath( + EntityType.DATABASE, + database?.fullyQualifiedName ?? '' + ), + isLink: true, + visible: [DRAWER_NAVIGATION_OPTIONS.explore], + }, + { + name: i18n.t('label.usage'), + value: getUsageData(usageSummary), + isLink: false, + visible: [DRAWER_NAVIGATION_OPTIONS.explore], + }, + ]; + + return overview; +}; + +const getEntityServiceOverview = (serviceDetails: EntityServiceUnion) => { + const { owners, domains, tags, serviceType } = serviceDetails; + + const tier = getTierTags(tags ?? []); + + const overview: BasicEntityOverviewInfo[] = [ + { + name: i18n.t('label.owner-plural'), + value: , + visible: [DRAWER_NAVIGATION_OPTIONS.explore], + }, + ...getCommonOverview({ domains }, false), + { + name: i18n.t('label.tier'), + value: entityTierRenderer(tier), + isLink: false, + visible: [DRAWER_NAVIGATION_OPTIONS.explore], + }, + { + name: i18n.t('label.service-type'), + value: serviceType, + isLink: false, + visible: [DRAWER_NAVIGATION_OPTIONS.explore], + }, + ]; + + return overview; +}; + +const getApiCollectionOverview = (apiCollection: APICollection) => { + if (isNil(apiCollection) || isEmpty(apiCollection)) { + return []; + } + + const { service, domains } = apiCollection; + + const overview: BasicEntityOverviewInfo[] = [ + ...getCommonOverview({ domains }, false), + { + name: i18n.t('label.endpoint-url'), + value: apiCollection.endpointURL || NO_DATA, + url: apiCollection.endpointURL, + isLink: true, + isExternal: true, + visible: [DRAWER_NAVIGATION_OPTIONS.explore], + }, + { + name: i18n.t('label.service'), + value: service?.fullyQualifiedName ?? NO_DATA, + url: getServiceDetailsPath( + service?.fullyQualifiedName ?? '', + ServiceCategory.API_SERVICES + ), + isLink: true, + visible: [DRAWER_NAVIGATION_OPTIONS.explore], + }, + ]; + + return overview; +}; +const getApiEndpointOverview = (apiEndpoint: APIEndpoint) => { + if (isNil(apiEndpoint) || isEmpty(apiEndpoint)) { + return []; + } + const { service, apiCollection, domains } = apiEndpoint; + + const overview: BasicEntityOverviewInfo[] = [ + ...getCommonOverview({ domains }, false), + { + name: i18n.t('label.endpoint-url'), + value: apiEndpoint.endpointURL || NO_DATA, + url: apiEndpoint.endpointURL, + isLink: true, + isExternal: true, + visible: [ + DRAWER_NAVIGATION_OPTIONS.explore, + DRAWER_NAVIGATION_OPTIONS.lineage, + ], + }, + { + name: i18n.t('label.api-collection'), + value: apiEndpoint.apiCollection?.fullyQualifiedName ?? '', + url: getEntityDetailsPath( + EntityType.API_COLLECTION, + apiCollection?.fullyQualifiedName ?? '' + ), + isLink: true, + visible: [ + DRAWER_NAVIGATION_OPTIONS.explore, + DRAWER_NAVIGATION_OPTIONS.lineage, + ], + }, + { + name: i18n.t('label.service'), + value: service?.fullyQualifiedName ?? '', + url: getServiceDetailsPath( + service?.fullyQualifiedName ?? '', + ServiceCategory.API_SERVICES + ), + isLink: true, + visible: [ + DRAWER_NAVIGATION_OPTIONS.explore, + DRAWER_NAVIGATION_OPTIONS.lineage, + ], + }, + { + name: i18n.t('label.request-method'), + value: apiEndpoint.requestMethod || NO_DATA, + isLink: false, + visible: [ + DRAWER_NAVIGATION_OPTIONS.explore, + DRAWER_NAVIGATION_OPTIONS.lineage, + ], + }, + ]; + + return overview; +}; +const getMetricOverview = (metric: Metric) => { + if (isNil(metric) || isEmpty(metric)) { + return []; + } + + const overview: BasicEntityOverviewInfo[] = [ + ...getCommonOverview({ domains: metric.domains }, false), + { + name: i18n.t('label.metric-type'), + value: metric.metricType || NO_DATA, + isLink: false, + visible: [ + DRAWER_NAVIGATION_OPTIONS.explore, + DRAWER_NAVIGATION_OPTIONS.lineage, + ], + }, + { + name: i18n.t('label.unit-of-measurement'), + value: metric.unitOfMeasurement || NO_DATA, + isLink: false, + visible: [ + DRAWER_NAVIGATION_OPTIONS.explore, + DRAWER_NAVIGATION_OPTIONS.lineage, + ], + }, + { + name: i18n.t('label.granularity'), + value: metric.granularity || NO_DATA, + isLink: false, + visible: [ + DRAWER_NAVIGATION_OPTIONS.explore, + DRAWER_NAVIGATION_OPTIONS.lineage, + ], + }, + ]; + + return overview; +}; + +const getDirectoryOverview = (directoryDetails: Directory) => { + const { + numberOfSubDirectories, + numberOfFiles, + serviceType, + owners, + domains, + } = directoryDetails; + + const visible = [ + DRAWER_NAVIGATION_OPTIONS.lineage, + DRAWER_NAVIGATION_OPTIONS.explore, + ]; + + const overview: BasicEntityOverviewInfo[] = [ + ...getCommonOverview({ owners, domains }), + { + name: i18n.t('label.directory-plural'), + value: numberOfSubDirectories ?? NO_DATA, + isLink: false, + visible, + }, + { + name: i18n.t('label.file-plural'), + value: numberOfFiles ?? NO_DATA, + isLink: false, + visible, + }, + { + name: i18n.t('label.service-type'), + value: serviceType, + isLink: false, + visible, + }, + ]; + + return overview; +}; + +const getFileOverview = (fileDetails: File) => { + const { fileExtension, fileType, fileVersion, serviceType, owners, domains } = + fileDetails; + + const visible = [ + DRAWER_NAVIGATION_OPTIONS.lineage, + DRAWER_NAVIGATION_OPTIONS.explore, + ]; + + const overview: BasicEntityOverviewInfo[] = [ + ...getCommonOverview({ owners, domains }), + { + name: i18n.t('label.file-extension'), + value: fileExtension ?? NO_DATA, + isLink: false, + visible, + }, + { + name: i18n.t('label.file-type'), + value: fileType ?? NO_DATA, + isLink: false, + visible, + }, + { + name: i18n.t('label.file-version'), + value: fileVersion ?? NO_DATA, + isLink: false, + visible, + }, + { + name: i18n.t('label.service-type'), + value: serviceType, + isLink: false, + visible, + }, + ]; + + return overview; +}; + +const getSpreadsheetOverview = (spreadsheetDetails: Spreadsheet) => { + const { fileVersion, serviceType, owners, domains } = spreadsheetDetails; + + const visible = [ + DRAWER_NAVIGATION_OPTIONS.lineage, + DRAWER_NAVIGATION_OPTIONS.explore, + ]; + + const overview: BasicEntityOverviewInfo[] = [ + ...getCommonOverview({ owners, domains }), + { + name: i18n.t('label.file-version'), + value: fileVersion ?? NO_DATA, + isLink: false, + visible, + }, + { + name: i18n.t('label.service-type'), + value: serviceType, + isLink: false, + visible, + }, + ]; + + return overview; +}; + +const getWorksheetOverview = (worksheetDetails: Worksheet) => { + const { columnCount, rowCount, serviceType, owners, domains } = + worksheetDetails; + + const visible = [ + DRAWER_NAVIGATION_OPTIONS.lineage, + DRAWER_NAVIGATION_OPTIONS.explore, + ]; + + const overview: BasicEntityOverviewInfo[] = [ + ...getCommonOverview({ owners, domains }), + { + name: i18n.t('label.column-plural'), + value: columnCount ?? NO_DATA, + isLink: false, + visible, + }, + { + name: i18n.t('label.row-plural'), + value: rowCount ?? NO_DATA, + isLink: false, + visible, + }, + { + name: i18n.t('label.service-type'), + value: serviceType, + isLink: false, + visible, + }, + ]; + + return overview; +}; + +const getColumnOverview = ( + columnDetails: ColumnSearchResult +): BasicEntityOverviewInfo[] => { + const { + dataType, + dataTypeDisplay, + constraint, + table, + service, + database, + databaseSchema, + owners, + domains, + } = columnDetails; + + const overview: BasicEntityOverviewInfo[] = [ + ...getCommonOverview({ owners, domains }), + { + name: i18n.t('label.data-type'), + value: dataTypeDisplay || dataType || '--', + isLink: false, + visible: [ + DRAWER_NAVIGATION_OPTIONS.lineage, + DRAWER_NAVIGATION_OPTIONS.explore, + ], + }, + { + name: i18n.t('label.table'), + value: table?.displayName || table?.name || '--', + url: table?.fullyQualifiedName + ? getEntityDetailsPath(EntityType.TABLE, table.fullyQualifiedName) + : undefined, + isLink: !!table?.fullyQualifiedName, + visible: [ + DRAWER_NAVIGATION_OPTIONS.lineage, + DRAWER_NAVIGATION_OPTIONS.explore, + ], + }, + { + name: i18n.t('label.service'), + value: service?.displayName || service?.name || '--', + url: service?.fullyQualifiedName + ? getServiceDetailsPath(service.fullyQualifiedName, service.type || '') + : undefined, + isLink: !!service?.fullyQualifiedName, + visible: [ + DRAWER_NAVIGATION_OPTIONS.lineage, + DRAWER_NAVIGATION_OPTIONS.explore, + ], + }, + { + name: i18n.t('label.database'), + value: database?.displayName || database?.name || '--', + url: database?.fullyQualifiedName + ? getEntityDetailsPath(EntityType.DATABASE, database.fullyQualifiedName) + : undefined, + isLink: !!database?.fullyQualifiedName, + visible: [ + DRAWER_NAVIGATION_OPTIONS.lineage, + DRAWER_NAVIGATION_OPTIONS.explore, + ], + }, + { + name: i18n.t('label.schema'), + value: databaseSchema?.displayName || databaseSchema?.name || '--', + url: databaseSchema?.fullyQualifiedName + ? getEntityDetailsPath( + EntityType.DATABASE_SCHEMA, + databaseSchema.fullyQualifiedName + ) + : undefined, + isLink: !!databaseSchema?.fullyQualifiedName, + visible: [ + DRAWER_NAVIGATION_OPTIONS.lineage, + DRAWER_NAVIGATION_OPTIONS.explore, + ], + }, + ]; + + if (constraint) { + overview.push({ + name: i18n.t('label.constraint'), + value: constraint, + isLink: false, + visible: [ + DRAWER_NAVIGATION_OPTIONS.lineage, + DRAWER_NAVIGATION_OPTIONS.explore, + ], + }); + } + + return overview; +}; + +export const getEntityOverview = ( + type: string, + entityDetail: DataAssetSummaryPanelProps['dataAsset'], + additionalInfo?: Record +): Array => { + switch (type) { + case ExplorePageTabs.TABLES: + case EntityType.TABLE: { + return getTableOverview(entityDetail as Table, additionalInfo); + } + + case ExplorePageTabs.COLUMNS: + case EntityType.TABLE_COLUMN: { + return getColumnOverview(entityDetail as unknown as ColumnSearchResult); + } + + case ExplorePageTabs.TOPICS: + case EntityType.TOPIC: { + return getTopicOverview(entityDetail as Topic); + } + + case ExplorePageTabs.PIPELINES: + case EntityType.PIPELINE: { + return getPipelineOverview(entityDetail as Pipeline); + } + + case ExplorePageTabs.DASHBOARDS: + case EntityType.DASHBOARD: { + return getDashboardOverview(entityDetail as Dashboard); + } + + case ExplorePageTabs.SEARCH_INDEX: + case EntityType.SEARCH_INDEX: { + return getSearchIndexOverview(entityDetail as SearchIndex); + } + + case ExplorePageTabs.MLMODELS: + case EntityType.MLMODEL: { + return getMlModelOverview(entityDetail as Mlmodel); + } + case ExplorePageTabs.CONTAINERS: + case EntityType.CONTAINER: { + return getContainerOverview(entityDetail as Container); + } + case ExplorePageTabs.CHARTS: + case EntityType.CHART: { + return getChartOverview(entityDetail as Chart); + } + + case ExplorePageTabs.DASHBOARD_DATA_MODEL: + case EntityType.DASHBOARD_DATA_MODEL: { + return getDataModelOverview(entityDetail as DashboardDataModel); + } + + case ExplorePageTabs.STORED_PROCEDURE: + case EntityType.STORED_PROCEDURE: { + return getStoredProcedureOverview(entityDetail as StoredProcedure); + } + + case ExplorePageTabs.DATABASE: + case EntityType.DATABASE: { + return getDatabaseOverview(entityDetail as Database); + } + + case ExplorePageTabs.DATABASE_SCHEMA: + case EntityType.DATABASE_SCHEMA: { + return getDatabaseSchemaOverview(entityDetail as DatabaseSchema); + } + + case ExplorePageTabs.API_COLLECTION: + case EntityType.API_COLLECTION: { + return getApiCollectionOverview(entityDetail as APICollection); + } + + case ExplorePageTabs.API_ENDPOINT: + case EntityType.API_ENDPOINT: { + return getApiEndpointOverview(entityDetail as APIEndpoint); + } + + case ExplorePageTabs.METRIC: + case EntityType.METRIC: { + return getMetricOverview(entityDetail as Metric); + } + + case ExplorePageTabs.DIRECTORIES: + case EntityType.DIRECTORY: { + return getDirectoryOverview(entityDetail as Directory); + } + + case ExplorePageTabs.FILES: + case EntityType.FILE: { + return getFileOverview(entityDetail as unknown as File); + } + + case ExplorePageTabs.SPREADSHEETS: + case EntityType.SPREADSHEET: { + return getSpreadsheetOverview(entityDetail as Spreadsheet); + } + + case ExplorePageTabs.WORKSHEETS: + case EntityType.WORKSHEET: { + return getWorksheetOverview(entityDetail as Worksheet); + } + + case ExplorePageTabs.DATABASE_SERVICE: + case ExplorePageTabs.MESSAGING_SERVICE: + case ExplorePageTabs.DASHBOARD_SERVICE: + case ExplorePageTabs.ML_MODEL_SERVICE: + case ExplorePageTabs.PIPELINE_SERVICE: + case ExplorePageTabs.SEARCH_INDEX_SERVICE: + case ExplorePageTabs.API_SERVICE: + case EntityType.DATABASE_SERVICE: + case EntityType.MESSAGING_SERVICE: + case EntityType.DASHBOARD_SERVICE: + case EntityType.MLMODEL_SERVICE: + case EntityType.PIPELINE_SERVICE: + case EntityType.SEARCH_SERVICE: + case EntityType.API_SERVICE: { + return getEntityServiceOverview(entityDetail as EntityServiceUnion); + } + + default: + return []; + } +}; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/DataAssetsHeader.utils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/DataAssetsHeader.utils.tsx index ecdb3ad4c2cc..61df51113828 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/DataAssetsHeader.utils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/DataAssetsHeader.utils.tsx @@ -15,7 +15,6 @@ import Icon from '@ant-design/icons'; import { Divider, Tooltip, Typography } from 'antd'; import classNames from 'classnames'; -import { t } from 'i18next'; import { isArray, isEmpty, isObject, isUndefined } from 'lodash'; import React, { ReactNode } from 'react'; import { ReactComponent as IconExternalLink } from '../assets/svg/external-links.svg'; @@ -70,10 +69,13 @@ import { getBreadcrumbForTable, getEntityBreadcrumbs, } from './EntityUtils'; +import i18n from './i18next/LocalUtil'; import { getEntityDetailsPath } from './RouterUtils'; import { bytesToSize } from './StringsUtils'; import { getUsagePercentile } from './TableUtils'; +const { t } = i18n; + export const ExtraInfoLabel = ({ label, value, diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/DataContract/DataContractUtils.test.ts b/openmetadata-ui/src/main/resources/ui/src/utils/DataContract/DataContractUtils.test.ts index 99ccde2b7bb2..79009802716d 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/DataContract/DataContractUtils.test.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/DataContract/DataContractUtils.test.ts @@ -43,15 +43,6 @@ jest.mock('js-yaml', () => ({ dump: jest.fn((data) => JSON.stringify(data)), })); -jest.mock('../i18next/LocalUtil', () => ({ - __esModule: true, - default: { - t: jest.fn((key: string) => key), - }, - t: jest.fn((key: string) => key), - detectBrowserLanguage: jest.fn(() => 'en-US'), -})); - // Import after mocks are set up import { DataContractProcessedResultCharts } from '../../components/DataContract/ContractExecutionChart/ContractExecutionChart.interface'; import { DataContract } from '../../generated/entity/data/dataContract'; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/DatabaseServiceUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/DatabaseServiceUtils.tsx index f5a06466f765..0f0722346bb2 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/DatabaseServiceUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/DatabaseServiceUtils.tsx @@ -18,7 +18,7 @@ import { ReactComponent as ImportIcon } from '../assets/svg/ic-import.svg'; import { ManageButtonItemLabel } from '../components/common/ManageButtonContentItem/ManageButtonContentItem.component'; import { useEntityExportModalProvider } from '../components/Entity/EntityExportModalProvider/EntityExportModalProvider.component'; import { ExportTypes } from '../constants/Export.constants'; -import { COMMON_UI_SCHEMA } from '../constants/Services.constant'; +import { COMMON_UI_SCHEMA } from '../constants/ServiceUISchema.constant'; import { OperationPermission } from '../context/PermissionProvider/PermissionProvider.interface'; import { EntityType } from '../enums/entity.enum'; import { DatabaseServiceType } from '../generated/entity/services/databaseService'; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/DomainUtils.test.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/DomainUtils.test.tsx index c13c9de444e3..643136ae8ea4 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/DomainUtils.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/DomainUtils.test.tsx @@ -10,18 +10,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { InternalAxiosRequestConfig } from 'axios'; -import { DEFAULT_DOMAIN_VALUE } from '../constants/constants'; import { EntityType } from '../enums/entity.enum'; -import { SearchIndex } from '../enums/search.enum'; import { Domain, DomainType } from '../generated/entity/domains/domain'; -import { useDomainStore } from '../hooks/useDomainStore'; import { getQueryFilterToIncludeDomain, isDomainExist, - withDomainFilter, } from '../utils/DomainUtils'; -import { getPathNameFromWindowLocation } from './RouterUtils'; jest.mock('../hooks/useDomainStore'); jest.mock('./RouterUtils'); @@ -260,460 +254,3 @@ describe('isDomainExist', () => { }); }); }); - -describe('withDomainFilter', () => { - const mockGetState = jest.fn(); - const mockGetPathName = getPathNameFromWindowLocation as jest.Mock; - - beforeEach(() => { - jest.clearAllMocks(); - (useDomainStore as unknown as jest.Mock).mockImplementation(() => ({ - getState: mockGetState, - })); - (useDomainStore.getState as jest.Mock) = mockGetState; - }); - - const createMockConfig = ( - method: string = 'get', - url?: string, - params?: Record - ): InternalAxiosRequestConfig => - ({ - method, - url, - params, - headers: {}, - } as InternalAxiosRequestConfig); - - describe('should not intercept requests', () => { - it('should return config unchanged when path starts with /domain', () => { - mockGetPathName.mockReturnValue('/domain/test'); - mockGetState.mockReturnValue({ activeDomain: 'testDomain' }); - - const config = createMockConfig(); - const result = withDomainFilter(config); - - expect(result).toBe(config); - expect(result.params).toBeUndefined(); - }); - - it('should return config unchanged when path starts with /auth/logout', () => { - mockGetPathName.mockReturnValue('/auth/logout'); - mockGetState.mockReturnValue({ activeDomain: 'testDomain' }); - - const config = createMockConfig(); - const result = withDomainFilter(config); - - expect(result).toBe(config); - expect(result.params).toBeUndefined(); - }); - - it('should return config unchanged when path starts with /auth/refresh', () => { - mockGetPathName.mockReturnValue('/auth/refresh'); - mockGetState.mockReturnValue({ activeDomain: 'testDomain' }); - - const config = createMockConfig(); - const result = withDomainFilter(config); - - expect(result).toBe(config); - expect(result.params).toBeUndefined(); - }); - - it('should return config unchanged when method is not GET', () => { - mockGetPathName.mockReturnValue('/api/test'); - mockGetState.mockReturnValue({ activeDomain: 'testDomain' }); - - const config = createMockConfig('post'); - const result = withDomainFilter(config); - - expect(result).toBe(config); - expect(result.params).toBeUndefined(); - }); - - it('should return config unchanged when activeDomain is DEFAULT_DOMAIN_VALUE', () => { - mockGetPathName.mockReturnValue('/api/test'); - mockGetState.mockReturnValue({ activeDomain: DEFAULT_DOMAIN_VALUE }); - - const config = createMockConfig(); - const result = withDomainFilter(config); - - expect(result).toBe(config); - expect(result.params).toBeUndefined(); - }); - }); - - describe('regular GET requests', () => { - it('should add domain parameter for regular GET requests with active domain', () => { - mockGetPathName.mockReturnValue('/api/tables'); - mockGetState.mockReturnValue({ activeDomain: 'engineering' }); - - const config = createMockConfig('get', '/api/tables'); - const result = withDomainFilter(config); - - expect(result.params).toEqual({ - domain: 'engineering', - }); - }); - - it('should preserve existing params when adding domain parameter', () => { - mockGetPathName.mockReturnValue('/api/tables'); - mockGetState.mockReturnValue({ activeDomain: 'engineering' }); - - const config = createMockConfig('get', '/api/tables', { - limit: 10, - offset: 0, - }); - const result = withDomainFilter(config); - - expect(result.params).toEqual({ - limit: 10, - offset: 0, - domain: 'engineering', - }); - }); - }); - - describe('search query requests', () => { - it('should add should filter with term and prefix for /search/query with active domain', () => { - mockGetPathName.mockReturnValue('/api/search'); - mockGetState.mockReturnValue({ activeDomain: 'engineering' }); - - const config = createMockConfig('get', '/search/query', { - index: SearchIndex.TABLE, - }); - const result = withDomainFilter(config); - - expect(result.params).toHaveProperty('query_filter'); - - const filter = JSON.parse(result.params?.query_filter as string); - - expect(filter).toEqual({ - query: { - bool: { - must: [ - { - bool: { - should: [ - { - term: { - 'domains.fullyQualifiedName': 'engineering', - }, - }, - { - prefix: { - 'domains.fullyQualifiedName': 'engineering.', - }, - }, - ], - }, - }, - ], - }, - }, - }); - }); - - it('should return config unchanged for TAG index searches', () => { - mockGetPathName.mockReturnValue('/api/search'); - mockGetState.mockReturnValue({ activeDomain: 'engineering' }); - - const config = createMockConfig('get', '/search/query', { - index: SearchIndex.TAG, - }); - const result = withDomainFilter(config); - - expect(result).toBe(config); - expect(result.params?.query_filter).toBeUndefined(); - }); - - it('should use fullyQualifiedName field for DOMAIN index searches', () => { - mockGetPathName.mockReturnValue('/api/search'); - mockGetState.mockReturnValue({ activeDomain: 'engineering' }); - - const config = createMockConfig('get', '/search/query', { - index: SearchIndex.DOMAIN, - }); - const result = withDomainFilter(config); - const queryFilter = JSON.parse(result.params?.query_filter as string); - const shouldClauses = - queryFilter.query.bool.must[queryFilter.query.bool.must.length - 1].bool - .should; - - expect(shouldClauses).toEqual([ - { term: { fullyQualifiedName: 'engineering' } }, - { prefix: { fullyQualifiedName: 'engineering.' } }, - ]); - }); - - it('should preserve existing query_filter and add should filter', () => { - mockGetPathName.mockReturnValue('/api/search'); - mockGetState.mockReturnValue({ activeDomain: 'engineering' }); - - const existingFilter = { - query: { - bool: { - must: [ - { - term: { - entityType: 'table', - }, - }, - ], - }, - }, - }; - - const config = createMockConfig('get', '/search/query', { - index: SearchIndex.TABLE, - query_filter: JSON.stringify(existingFilter), - }); - const result = withDomainFilter(config); - - const filter = JSON.parse(result.params?.query_filter as string); - - expect(filter.query.bool.must).toHaveLength(2); - expect(filter.query.bool.must[0]).toEqual({ - term: { - entityType: 'table', - }, - }); - expect(filter.query.bool.must[1]).toEqual({ - bool: { - should: [ - { - term: { - 'domains.fullyQualifiedName': 'engineering', - }, - }, - { - prefix: { - 'domains.fullyQualifiedName': 'engineering.', - }, - }, - ], - }, - }); - }); - - it('should handle invalid JSON in query_filter gracefully', () => { - mockGetPathName.mockReturnValue('/api/search'); - mockGetState.mockReturnValue({ activeDomain: 'engineering' }); - - const config = createMockConfig('get', '/search/query', { - index: SearchIndex.TABLE, - query_filter: 'invalid-json', - }); - const result = withDomainFilter(config); - - const filter = JSON.parse(result.params?.query_filter as string); - - expect(filter).toEqual({ - query: { - bool: { - must: [ - { - bool: { - should: [ - { - term: { - 'domains.fullyQualifiedName': 'engineering', - }, - }, - { - prefix: { - 'domains.fullyQualifiedName': 'engineering.', - }, - }, - ], - }, - }, - ], - }, - }, - }); - }); - - it('should handle query_filter with empty must array', () => { - mockGetPathName.mockReturnValue('/api/search'); - mockGetState.mockReturnValue({ activeDomain: 'engineering' }); - - const existingFilter = { - query: { - bool: {}, - }, - }; - - const config = createMockConfig('get', '/search/query', { - index: SearchIndex.TABLE, - query_filter: JSON.stringify(existingFilter), - }); - const result = withDomainFilter(config); - - const filter = JSON.parse(result.params?.query_filter as string); - - expect(filter.query.bool.must).toHaveLength(1); - expect(filter.query.bool.must[0]).toEqual({ - bool: { - should: [ - { - term: { - 'domains.fullyQualifiedName': 'engineering', - }, - }, - { - prefix: { - 'domains.fullyQualifiedName': 'engineering.', - }, - }, - ], - }, - }); - }); - - it('should handle empty object query_filter gracefully', () => { - mockGetPathName.mockReturnValue('/api/search'); - mockGetState.mockReturnValue({ activeDomain: 'engineering' }); - - const config = createMockConfig('get', '/search/query', { - index: SearchIndex.TABLE, - query_filter: '{}', - }); - const result = withDomainFilter(config); - - const filter = JSON.parse(result.params?.query_filter as string); - - expect(filter).toEqual({ - query: { - bool: { - must: [ - { - bool: { - should: [ - { - term: { - 'domains.fullyQualifiedName': 'engineering', - }, - }, - { - prefix: { - 'domains.fullyQualifiedName': 'engineering.', - }, - }, - ], - }, - }, - ], - }, - }, - }); - }); - - it('should preserve existing params when adding query_filter', () => { - mockGetPathName.mockReturnValue('/api/search'); - mockGetState.mockReturnValue({ activeDomain: 'engineering' }); - - const config = createMockConfig('get', '/search/query', { - index: SearchIndex.TABLE, - limit: 10, - offset: 0, - }); - const result = withDomainFilter(config); - - expect(result.params).toHaveProperty('index', SearchIndex.TABLE); - expect(result.params).toHaveProperty('limit', 10); - expect(result.params).toHaveProperty('offset', 0); - expect(result.params).toHaveProperty('query_filter'); - }); - - it('should preserve non-bool top-level clauses when adding domain filter', () => { - mockGetPathName.mockReturnValue('/api/search'); - mockGetState.mockReturnValue({ activeDomain: 'engineering' }); - - const existingFilter = JSON.stringify({ - query: { - term: { 'some.field': 'someValue' }, - bool: { must: [{ term: { 'other.field': 'otherValue' } }] }, - }, - }); - - const config = createMockConfig('get', '/search/query', { - index: SearchIndex.TABLE, - query_filter: existingFilter, - }); - const result = withDomainFilter(config); - - const parsed = JSON.parse(result.params?.query_filter as string); - - expect(parsed.query.bool.must).toContainEqual({ - term: { 'some.field': 'someValue' }, - }); - expect(parsed.query.bool.must).toContainEqual({ - term: { 'other.field': 'otherValue' }, - }); - expect(parsed.query.bool.must).toContainEqual({ - bool: { - should: [ - { - term: { - 'domains.fullyQualifiedName': 'engineering', - }, - }, - { - prefix: { - 'domains.fullyQualifiedName': 'engineering.', - }, - }, - ], - }, - }); - expect(parsed.query.bool.must).toHaveLength(3); - }); - }); - - describe('nested domain paths', () => { - it('should handle nested domain paths correctly', () => { - mockGetPathName.mockReturnValue('/api/tables'); - mockGetState.mockReturnValue({ - activeDomain: 'engineering.backend.services', - }); - - const config = createMockConfig('get', '/api/tables'); - const result = withDomainFilter(config); - - expect(result.params).toEqual({ - domain: 'engineering.backend.services', - }); - }); - - it('should add should filter with nested domain for search queries', () => { - mockGetPathName.mockReturnValue('/api/search'); - mockGetState.mockReturnValue({ - activeDomain: 'engineering.backend.services', - }); - - const config = createMockConfig('get', '/search/query', { - index: SearchIndex.TABLE, - }); - const result = withDomainFilter(config); - - const filter = JSON.parse(result.params?.query_filter as string); - - expect(filter.query.bool.must[0]).toEqual({ - bool: { - should: [ - { - term: { - 'domains.fullyQualifiedName': 'engineering.backend.services', - }, - }, - { - prefix: { - 'domains.fullyQualifiedName': 'engineering.backend.services.', - }, - }, - ], - }, - }); - }); - }); -}); diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/DomainUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/DomainUtils.tsx index 388c9fbc25dd..2934d81423d2 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/DomainUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/DomainUtils.tsx @@ -13,7 +13,6 @@ import { Tooltip, TooltipTrigger } from '@openmetadata/ui-core-components'; import { InfoCircle } from '@untitledui/icons'; import { Divider, Space, Tooltip as AntDTooltip, Typography } from 'antd'; -import { InternalAxiosRequestConfig } from 'axios'; import classNames from 'classnames'; import { get, isEmpty, isUndefined, noop } from 'lodash'; import { Fragment, ReactNode } from 'react'; @@ -36,20 +35,14 @@ import SubDomainsTable from '../components/Domain/SubDomainsTable/SubDomainsTabl import EntitySummaryPanel from '../components/Explore/EntitySummaryPanel/EntitySummaryPanel.component'; import AssetsTabs from '../components/Glossary/GlossaryTerms/tabs/AssetsTabs.component'; import { AssetsOfEntity } from '../components/Glossary/GlossaryTerms/tabs/AssetsTabs.interface'; -import { - DEFAULT_DOMAIN_VALUE, - DE_ACTIVE_COLOR, - NO_DATA_PLACEHOLDER, -} from '../constants/constants'; +import { DE_ACTIVE_COLOR, NO_DATA_PLACEHOLDER } from '../constants/constants'; import { DOMAIN_TYPE_DATA } from '../constants/Domain.constants'; import { DetailPageWidgetKeys } from '../enums/CustomizeDetailPage.enum'; import { EntityTabs, EntityType } from '../enums/entity.enum'; -import { SearchIndex } from '../enums/search.enum'; import { Domain } from '../generated/entity/domains/domain'; import { Operation } from '../generated/entity/policies/policy'; import { EntityReference } from '../generated/entity/type'; import { PageType } from '../generated/system/ui/page'; -import { useDomainStore } from '../hooks/useDomainStore'; import { WidgetConfig } from '../pages/CustomizablePage/CustomizablePage.interface'; import { QueryFieldInterface, @@ -64,99 +57,7 @@ import { getPrioritizedEditPermission, getPrioritizedViewPermission, } from './PermissionsUtils'; -import { getDomainPath, getPathNameFromWindowLocation } from './RouterUtils'; - -export const withDomainFilter = ( - config: InternalAxiosRequestConfig -): InternalAxiosRequestConfig => { - const isGetRequest = config.method === 'get'; - const activeDomain = useDomainStore.getState().activeDomain; - const hasActiveDomain = activeDomain !== DEFAULT_DOMAIN_VALUE; - const currentPath = getPathNameFromWindowLocation(); - const shouldNotIntercept = [ - '/domain', - '/auth/logout', - '/auth/refresh', - ].reduce((prev, curr) => { - return prev || currentPath.startsWith(curr); - }, false); - - if (shouldNotIntercept) { - return config; - } - - if (isGetRequest && hasActiveDomain) { - if (config.url?.includes('/search/query')) { - if (config.params?.index === SearchIndex.TAG) { - return config; - } - - const domainFilterField = - config.params?.index === SearchIndex.DOMAIN - ? 'fullyQualifiedName' - : 'domains.fullyQualifiedName'; - let filter: QueryFilterInterface = { query: { bool: {} } }; - if (config.params?.query_filter) { - try { - const parsed = JSON.parse(config.params.query_filter as string); - filter = parsed?.query ? parsed : { query: { bool: {} } }; - } catch { - filter = { query: { bool: {} } }; - } - } - - let mustArray: QueryFieldInterface[] = []; - const existingMust = filter.query?.bool?.must; - if (Array.isArray(existingMust)) { - mustArray = [...existingMust]; - } else if (existingMust) { - mustArray = [existingMust]; - } - - const { bool: existingBool, ...nonBoolClauses } = filter.query ?? {}; - for (const [key, value] of Object.entries(nonBoolClauses)) { - mustArray.push({ [key]: value } as QueryFieldInterface); - } - - filter.query = { - bool: { - ...existingBool, - must: [ - ...mustArray, - { - bool: { - should: [ - { - term: { - [domainFilterField]: activeDomain, - }, - }, - { - prefix: { - [domainFilterField]: `${activeDomain}.`, - }, - }, - ], - }, - } as QueryFieldInterface, - ], - }, - }; - - config.params = { - ...config.params, - query_filter: JSON.stringify(filter), - }; - } else { - config.params = { - ...config.params, - domain: activeDomain, - }; - } - } - - return config; -}; +import { getDomainPath } from './RouterUtils'; export const getOwner = ( hasPermission: boolean, diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/DriveServiceUtils.test.ts b/openmetadata-ui/src/main/resources/ui/src/utils/DriveServiceUtils.test.ts index 7fd96a15bc82..c22ecf825695 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/DriveServiceUtils.test.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/DriveServiceUtils.test.ts @@ -12,7 +12,7 @@ */ import { cloneDeep } from 'lodash'; -import { COMMON_UI_SCHEMA } from '../constants/Services.constant'; +import { COMMON_UI_SCHEMA } from '../constants/ServiceUISchema.constant'; import { DriveServiceType } from '../generated/entity/services/driveService'; import customDriveConnection from '../jsons/connectionSchemas/connections/drive/customDriveConnection.json'; import googleDriveConnection from '../jsons/connectionSchemas/connections/drive/googleDriveConnection.json'; @@ -22,7 +22,7 @@ jest.mock('lodash', () => ({ cloneDeep: jest.fn(), })); -jest.mock('../constants/Services.constant', () => ({ +jest.mock('../constants/ServiceUISchema.constant', () => ({ COMMON_UI_SCHEMA: { connection: { 'ui:field': 'collapsible', diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/DriveServiceUtils.ts b/openmetadata-ui/src/main/resources/ui/src/utils/DriveServiceUtils.ts index dcdc1b20def6..d66f9e87c768 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/DriveServiceUtils.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/DriveServiceUtils.ts @@ -12,7 +12,7 @@ */ import { cloneDeep } from 'lodash'; -import { COMMON_UI_SCHEMA } from '../constants/Services.constant'; +import { COMMON_UI_SCHEMA } from '../constants/ServiceUISchema.constant'; import { DriveServiceType } from '../generated/entity/services/driveService'; import customDriveConnection from '../jsons/connectionSchemas/connections/drive/customDriveConnection.json'; import googleDriveConnection from '../jsons/connectionSchemas/connections/drive/googleDriveConnection.json'; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/EntityUtils.test.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/EntityUtils.test.tsx index 599f427825e8..99f91c8f4432 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/EntityUtils.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/EntityUtils.test.tsx @@ -14,11 +14,8 @@ import { render } from '@testing-library/react'; import { startCase } from 'lodash'; import { DEFAULT_DOMAIN_VALUE } from '../constants/constants'; import { EntityTabs, EntityType } from '../enums/entity.enum'; -import { ExplorePageTabs } from '../enums/Explore.enum'; import { ServiceCategory } from '../enums/service.enum'; import { TestSuite } from '../generated/tests/testCase'; -import { MOCK_CHART_DATA } from '../mocks/Chart.mock'; -import { MOCK_TABLE, MOCK_TIER_DATA } from '../mocks/TableData.mock'; import { columnSorter, getBreadcrumbForTestSuite, @@ -26,7 +23,6 @@ import { getDomainDisplayName, getEntityBreadcrumbs, getEntityLinkFromType, - getEntityOverview, hasCustomPropertiesTab, hasLineageTab, hasSchemaTab, @@ -55,7 +51,6 @@ import { getSettingPath, } from './RouterUtils'; import { getServiceRouteFromServiceType } from './ServiceUtils'; -import { getTierTags } from './TableUtils'; jest.mock('../constants/constants', () => ({ DEFAULT_DOMAIN_VALUE: 'All Domains', @@ -218,59 +213,6 @@ describe('EntityUtils unit tests', () => { }); }); - describe('getEntityOverview', () => { - it('should call getChartOverview and get ChartData if ExplorePageTabs is charts', () => { - const result = JSON.stringify( - getEntityOverview(ExplorePageTabs.CHARTS, { - ...MOCK_CHART_DATA, - dataProducts: [], - }) - ); - - expect(result).toContain('label.owner-plural'); - expect(result).toContain('label.chart'); - expect(result).toContain('label.url-uppercase'); - expect(result).toContain('Are you an ethnic minority in your city?'); - expect(result).toContain( - `http://localhost:8088/superset/explore/?form_data=%7B%22slice_id%22%3A%20127%7D` - ); - expect(result).toContain('label.service'); - expect(result).toContain('sample_superset'); - expect(result).toContain('Other'); - expect(result).toContain('label.service-type'); - expect(result).toContain('Superset'); - }); - - it('should call getChartOverview and get TableData if ExplorePageTabs is table', () => { - const result = JSON.stringify( - getEntityOverview(ExplorePageTabs.TABLES, { - ...MOCK_TABLE, - tags: [MOCK_TIER_DATA], - dataProducts: [], - }) - ); - - expect(result).toContain('label.owner-plural'); - expect(result).toContain('label.type'); - expect(result).toContain('label.service'); - expect(result).toContain('label.database'); - expect(result).toContain('label.schema'); - expect(result).toContain('label.tier'); - expect(result).toContain('label.usage'); - expect(result).toContain('label.query-plural'); - expect(result).toContain('label.column-plural'); - expect(result).toContain('label.row-plural'); - expect(getTierTags).toHaveBeenCalledWith([MOCK_TIER_DATA]); - expect(result).toContain('Regular'); - expect(result).toContain('sample_data'); - expect(result).toContain('ecommerce_db'); - expect(result).toContain('shopify'); - expect(result).toContain('0th'); - expect(result).toContain('4'); - expect(result).toContain('14567'); - }); - }); - describe('getColumnSorter', () => { type TestType = { name: string }; @@ -635,10 +577,10 @@ describe('EntityUtils unit tests', () => { expect(result).toBe('Engineering'); }); - it('should return translated "All Domains" when activeDomain is DEFAULT_DOMAIN_VALUE', () => { + it('should return translated "label.all-domain-plural" when activeDomain is DEFAULT_DOMAIN_VALUE', () => { const result = getDomainDisplayName(undefined, DEFAULT_DOMAIN_VALUE); - expect(result).toBe('All Domains'); + expect(result).toBe('label.all-domain-plural'); }); it('should return custom domain name when activeDomain is a custom value', () => { diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/EntityUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/EntityUtils.tsx index 8f41b86d50a3..069ed1b32baf 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/EntityUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/EntityUtils.tsx @@ -12,45 +12,27 @@ */ import { Popover, Space, Typography } from 'antd'; -import i18next, { t } from 'i18next'; -import { - isEmpty, - isNil, - isObject, - isUndefined, - lowerCase, - startCase, -} from 'lodash'; +import { isEmpty, isUndefined, lowerCase, startCase } from 'lodash'; import { EntityDetailUnion } from 'Models'; import { Fragment } from 'react'; import { Link } from 'react-router-dom'; import { Node } from 'reactflow'; -import { DomainLabel } from '../components/common/DomainLabel/DomainLabel.component'; -import { OwnerLabel } from '../components/common/OwnerLabel/OwnerLabel.component'; -import QueryCount from '../components/common/QueryCount/QueryCount.component'; import { TitleLink } from '../components/common/TitleBreadcrumb/TitleBreadcrumb.interface'; import { DataAssetsWithoutServiceField } from '../components/DataAssets/DataAssetsHeader/DataAssetsHeader.interface'; -import { DataAssetSummaryPanelProps } from '../components/DataAssetSummaryPanelV1/DataAssetSummaryPanelV1.interface'; -import { ProfilerTabPath } from '../components/Database/Profiler/ProfilerDashboard/profilerDashboard.interface'; import { QueryVoteType } from '../components/Database/TableQueries/TableQueries.interface'; import { CUSTOM_PROPERTIES_TABS_SET, LINEAGE_TABS_SET, SCHEMA_TABS_SET, } from '../components/Entity/EntityRightPanel/EntityRightPanelVerticalNav.constants'; -import { - EntityServiceUnion, - EntityWithServices, -} from '../components/Explore/ExplorePage.interface'; +import { EntityWithServices } from '../components/Explore/ExplorePage.interface'; import { SearchedDataProps, SourceType, } from '../components/SearchedData/SearchedData.interface'; -import TagsV1 from '../components/Tag/TagsV1/TagsV1.component'; import { FQN_SEPARATOR_CHAR } from '../constants/char.constants'; import { DEFAULT_DOMAIN_VALUE, - NO_DATA, PLACEHOLDER_ROUTE_ENTITY_TYPE, PLACEHOLDER_ROUTE_FQN, ROUTES, @@ -59,14 +41,12 @@ import { GlobalSettingOptions, GlobalSettingsMenuCategory, } from '../constants/GlobalSettings.constants'; -import { TAG_START_WITH } from '../constants/Tag.constants'; import { EntityLineageNodeType, EntityTabs, EntityType, FqnPart, } from '../enums/entity.enum'; -import { ExplorePageTabs } from '../enums/Explore.enum'; import { ServiceCategory, ServiceCategoryPlural } from '../enums/service.enum'; import { Kpi } from '../generated/dataInsight/kpi/kpi'; import { Classification } from '../generated/entity/classification/classification'; @@ -75,32 +55,23 @@ import { APICollection } from '../generated/entity/data/apiCollection'; import { APIEndpoint } from '../generated/entity/data/apiEndpoint'; import { Chart } from '../generated/entity/data/chart'; import { Container } from '../generated/entity/data/container'; -import { Dashboard } from '../generated/entity/data/dashboard'; import { DashboardDataModel } from '../generated/entity/data/dashboardDataModel'; import { Database } from '../generated/entity/data/database'; import { DatabaseSchema } from '../generated/entity/data/databaseSchema'; import { Directory } from '../generated/entity/data/directory'; import { File } from '../generated/entity/data/file'; import { GlossaryTerm } from '../generated/entity/data/glossaryTerm'; -import { Metric } from '../generated/entity/data/metric'; -import { Mlmodel } from '../generated/entity/data/mlmodel'; -import { Pipeline } from '../generated/entity/data/pipeline'; import { SearchIndex as SearchIndexAsset, - SearchIndex as SearchIndexEntity, SearchIndexField, } from '../generated/entity/data/searchIndex'; import { Spreadsheet } from '../generated/entity/data/spreadsheet'; -import { - StoredProcedure, - StoredProcedureCodeObject, -} from '../generated/entity/data/storedProcedure'; +import { StoredProcedure } from '../generated/entity/data/storedProcedure'; import { Column, ColumnJoins, JoinedWith, Table, - TableType, } from '../generated/entity/data/table'; import { Topic } from '../generated/entity/data/topic'; import { Worksheet } from '../generated/entity/data/worksheet'; @@ -113,7 +84,6 @@ import { import { TestCase, TestSuite } from '../generated/tests/testCase'; import { EntityReference } from '../generated/type/entityUsage'; import { TagLabel } from '../generated/type/tagLabel'; -import { UsageDetails } from '../generated/type/usageDetails'; import { Votes } from '../generated/type/votes'; import { DataInsightTabs } from '../interface/data-insight.interface'; import { @@ -122,14 +92,13 @@ import { } from '../interface/search.interface'; import { DataQualityPageTabs } from '../pages/DataQuality/DataQualityPage.interface'; import { - formatNumberWithComma, getPartialNameFromTableFQN, getTableFQNFromColumnFQN, } from './CommonUtils'; import { getDataInsightPathWithFqn } from './DataInsightUtils'; import EntityLink from './EntityLink'; -import { BasicEntityOverviewInfo } from './EntityUtils.interface'; import Fqn from './Fqn'; +import i18n from './i18next/LocalUtil'; import { getApplicationDetailsPath, getBotsPagePath, @@ -155,15 +124,12 @@ import { getTestCaseDetailPagePath, } from './RouterUtils'; import { getServiceRouteFromServiceType } from './ServiceUtils'; -import { bytesToSize, getEncodedFqn, stringToHTML } from './StringsUtils'; -import { - getDataTypeString, - getTagsWithoutTier, - getTierTags, - getUsagePercentile, -} from './TableUtils'; +import { getEncodedFqn } from './StringsUtils'; +import { getDataTypeString, getTagsWithoutTier } from './TableUtils'; import { getTableTags } from './TagsUtils'; +const { t } = i18n; + export enum DRAWER_NAVIGATION_OPTIONS { explore = 'Explore', lineage = 'Lineage', @@ -239,1370 +205,6 @@ export const getEntityTags = ( } }; -const entityTierRenderer = (tier?: TagLabel) => { - return tier ? ( - - ) : ( - NO_DATA - ); -}; - -const getUsageData = (usageSummary: UsageDetails | undefined) => - !isNil(usageSummary?.weeklyStats?.percentileRank) - ? getUsagePercentile(usageSummary?.weeklyStats?.percentileRank ?? 0) - : NO_DATA; - -const getTableFieldsFromTableDetails = (tableDetails: Table) => { - const { - fullyQualifiedName, - owners, - tags, - usageSummary, - profile, - columns, - tableType, - service, - database, - databaseSchema, - domains, - } = tableDetails; - const [serviceName, databaseName, schemaName] = getPartialNameFromTableFQN( - fullyQualifiedName ?? '', - [FqnPart.Service, FqnPart.Database, FqnPart.Schema], - FQN_SEPARATOR_CHAR - ).split(FQN_SEPARATOR_CHAR); - - const serviceDisplayName = getEntityName(service) || serviceName; - const databaseDisplayName = getEntityName(database) || databaseName; - const schemaDisplayName = getEntityName(databaseSchema) || schemaName; - - const tier = getTierTags(tags ?? []); - - return { - fullyQualifiedName, - owners, - service: serviceDisplayName, - database: databaseDisplayName, - schema: schemaDisplayName, - tier, - usage: getUsageData(usageSummary), - profile, - columns, - tableType, - domains, - }; -}; - -const getCommonOverview = ( - { - owners, - domains, - }: { - owners?: EntityReference[]; - domains?: EntityReference[]; - }, - showOwner = true -) => { - return [ - ...(showOwner - ? [ - { - name: i18next.t('label.owner-plural'), - value: ( - - ), - visible: [DRAWER_NAVIGATION_OPTIONS.lineage], - }, - ] - : []), - { - name: i18next.t('label.domain-plural'), - value: ( - - ), - visible: [DRAWER_NAVIGATION_OPTIONS.lineage], - }, - ]; -}; - -interface ColumnSearchResult { - dataType?: string; - dataTypeDisplay?: string; - constraint?: string; - table?: { - name?: string; - displayName?: string; - fullyQualifiedName?: string; - }; - service?: { - name?: string; - displayName?: string; - fullyQualifiedName?: string; - type?: string; - }; - database?: { - name?: string; - displayName?: string; - fullyQualifiedName?: string; - }; - databaseSchema?: { - name?: string; - displayName?: string; - fullyQualifiedName?: string; - }; - owners?: EntityReference[]; - domains?: EntityReference[]; -} - -const getColumnOverview = ( - columnDetails: ColumnSearchResult -): BasicEntityOverviewInfo[] => { - const { - dataType, - dataTypeDisplay, - constraint, - table, - service, - database, - databaseSchema, - owners, - domains, - } = columnDetails; - - const overview: BasicEntityOverviewInfo[] = [ - ...getCommonOverview({ owners, domains }), - { - name: i18next.t('label.data-type'), - value: dataTypeDisplay || dataType || '--', - isLink: false, - visible: [ - DRAWER_NAVIGATION_OPTIONS.lineage, - DRAWER_NAVIGATION_OPTIONS.explore, - ], - }, - { - name: i18next.t('label.table'), - value: table?.displayName || table?.name || '--', - url: table?.fullyQualifiedName - ? getEntityDetailsPath(EntityType.TABLE, table.fullyQualifiedName) - : undefined, - isLink: !!table?.fullyQualifiedName, - visible: [ - DRAWER_NAVIGATION_OPTIONS.lineage, - DRAWER_NAVIGATION_OPTIONS.explore, - ], - }, - { - name: i18next.t('label.service'), - value: service?.displayName || service?.name || '--', - url: service?.fullyQualifiedName - ? getServiceDetailsPath(service.fullyQualifiedName, service.type || '') - : undefined, - isLink: !!service?.fullyQualifiedName, - visible: [ - DRAWER_NAVIGATION_OPTIONS.lineage, - DRAWER_NAVIGATION_OPTIONS.explore, - ], - }, - { - name: i18next.t('label.database'), - value: database?.displayName || database?.name || '--', - url: database?.fullyQualifiedName - ? getEntityDetailsPath(EntityType.DATABASE, database.fullyQualifiedName) - : undefined, - isLink: !!database?.fullyQualifiedName, - visible: [ - DRAWER_NAVIGATION_OPTIONS.lineage, - DRAWER_NAVIGATION_OPTIONS.explore, - ], - }, - { - name: i18next.t('label.schema'), - value: databaseSchema?.displayName || databaseSchema?.name || '--', - url: databaseSchema?.fullyQualifiedName - ? getEntityDetailsPath( - EntityType.DATABASE_SCHEMA, - databaseSchema.fullyQualifiedName - ) - : undefined, - isLink: !!databaseSchema?.fullyQualifiedName, - visible: [ - DRAWER_NAVIGATION_OPTIONS.lineage, - DRAWER_NAVIGATION_OPTIONS.explore, - ], - }, - ]; - - if (constraint) { - overview.push({ - name: i18next.t('label.constraint'), - value: constraint, - isLink: false, - visible: [ - DRAWER_NAVIGATION_OPTIONS.lineage, - DRAWER_NAVIGATION_OPTIONS.explore, - ], - }); - } - - return overview; -}; - -const getTableOverview = ( - tableDetails: Table, - additionalInfo?: Record -) => { - const { - fullyQualifiedName, - owners, - profile, - columns, - tableType, - service, - database, - schema, - tier, - usage, - domains, - } = getTableFieldsFromTableDetails(tableDetails); - - const overview: BasicEntityOverviewInfo[] = [ - ...getCommonOverview({ owners, domains }), - { - name: i18next.t('label.type'), - value: tableType ?? TableType.Regular, - isLink: false, - visible: [ - DRAWER_NAVIGATION_OPTIONS.lineage, - DRAWER_NAVIGATION_OPTIONS.explore, - ], - }, - { - name: i18next.t('label.service'), - value: service || NO_DATA, - url: getServiceDetailsPath(service, ServiceCategory.DATABASE_SERVICES), - isLink: true, - visible: [DRAWER_NAVIGATION_OPTIONS.lineage], - }, - { - name: i18next.t('label.database'), - value: database || NO_DATA, - url: getEntityDetailsPath( - EntityType.DATABASE, - getPartialNameFromTableFQN( - fullyQualifiedName ?? '', - [FqnPart.Service, FqnPart.Database], - FQN_SEPARATOR_CHAR - ) - ), - isLink: true, - visible: [DRAWER_NAVIGATION_OPTIONS.lineage], - }, - { - name: i18next.t('label.schema'), - value: schema || NO_DATA, - url: getEntityDetailsPath( - EntityType.DATABASE_SCHEMA, - getPartialNameFromTableFQN( - fullyQualifiedName ?? '', - [FqnPart.Service, FqnPart.Database, FqnPart.Schema], - FQN_SEPARATOR_CHAR - ) - ), - isLink: true, - visible: [DRAWER_NAVIGATION_OPTIONS.lineage], - }, - { - name: i18next.t('label.tier'), - value: entityTierRenderer(tier), - isLink: false, - visible: [DRAWER_NAVIGATION_OPTIONS.lineage], - }, - { - name: i18next.t('label.usage'), - value: usage || NO_DATA, - isLink: false, - visible: [DRAWER_NAVIGATION_OPTIONS.lineage], - }, - { - name: i18next.t('label.query-plural'), - value: , - isLink: false, - visible: [ - DRAWER_NAVIGATION_OPTIONS.lineage, - DRAWER_NAVIGATION_OPTIONS.explore, - ], - }, - { - name: i18next.t('label.column-plural'), - value: columns ? columns.length : NO_DATA, - isLink: false, - visible: [ - DRAWER_NAVIGATION_OPTIONS.lineage, - DRAWER_NAVIGATION_OPTIONS.explore, - ], - }, - { - name: i18next.t('label.row-plural'), - value: - !isUndefined(profile) && profile?.rowCount - ? formatNumberWithComma(profile.rowCount) - : NO_DATA, - isLink: false, - visible: [DRAWER_NAVIGATION_OPTIONS.lineage], - }, - { - name: i18next.t('label.incident-plural'), - value: additionalInfo?.incidentCount ?? 0, - isLink: true, - linkProps: { - pathname: getEntityDetailsPath( - EntityType.TABLE, - fullyQualifiedName ?? '', - EntityTabs.PROFILER, - ProfilerTabPath.INCIDENTS - ), - }, - visible: [ - DRAWER_NAVIGATION_OPTIONS.lineage, - DRAWER_NAVIGATION_OPTIONS.explore, - ], - }, - ]; - - return overview; -}; - -const getTopicOverview = (topicDetails: Topic) => { - const { - domains, - partitions, - replicationFactor, - retentionSize, - cleanupPolicies, - maximumMessageSize, - messageSchema, - } = topicDetails; - - const overview: BasicEntityOverviewInfo[] = [ - ...getCommonOverview({ domains, owners: topicDetails.owners }), - { - name: i18next.t('label.partition-plural'), - value: partitions ?? NO_DATA, - isLink: false, - visible: [ - DRAWER_NAVIGATION_OPTIONS.lineage, - DRAWER_NAVIGATION_OPTIONS.explore, - ], - }, - { - name: i18next.t('label.replication-factor'), - value: replicationFactor, - isLink: false, - visible: [ - DRAWER_NAVIGATION_OPTIONS.lineage, - DRAWER_NAVIGATION_OPTIONS.explore, - ], - }, - { - name: i18next.t('label.retention-size'), - value: bytesToSize(retentionSize ?? 0), - isLink: false, - visible: [ - DRAWER_NAVIGATION_OPTIONS.lineage, - DRAWER_NAVIGATION_OPTIONS.explore, - ], - }, - { - name: i18next.t('label.clean-up-policy-plural'), - value: cleanupPolicies ? cleanupPolicies.join(', ') : NO_DATA, - isLink: false, - visible: [ - DRAWER_NAVIGATION_OPTIONS.lineage, - DRAWER_NAVIGATION_OPTIONS.explore, - ], - }, - { - name: i18next.t('label.max-message-size'), - value: bytesToSize(maximumMessageSize ?? 0), - isLink: false, - visible: [ - DRAWER_NAVIGATION_OPTIONS.lineage, - DRAWER_NAVIGATION_OPTIONS.explore, - ], - }, - { - name: i18next.t('label.schema-type'), - value: messageSchema?.schemaType ?? NO_DATA, - isLink: false, - visible: [ - DRAWER_NAVIGATION_OPTIONS.lineage, - DRAWER_NAVIGATION_OPTIONS.explore, - ], - }, - ]; - - return overview; -}; - -const getPipelineOverview = (pipelineDetails: Pipeline) => { - const { owners, tags, sourceUrl, service, displayName, domains } = - pipelineDetails; - const tier = getTierTags(tags ?? []); - const serviceDisplayName = getEntityName(service); - - const overview: BasicEntityOverviewInfo[] = [ - ...getCommonOverview({ owners, domains }), - { - name: `${i18next.t('label.pipeline')} ${i18next.t( - 'label.url-uppercase' - )}`, - dataTestId: 'pipeline-url-label', - value: stringToHTML(displayName ?? '') || NO_DATA, - url: sourceUrl, - isLink: true, - isExternal: true, - visible: [ - DRAWER_NAVIGATION_OPTIONS.lineage, - DRAWER_NAVIGATION_OPTIONS.explore, - ], - }, - { - name: i18next.t('label.service'), - value: serviceDisplayName || NO_DATA, - url: getServiceDetailsPath( - service?.name ?? '', - ServiceCategory.PIPELINE_SERVICES - ), - isLink: true, - isExternal: false, - visible: [DRAWER_NAVIGATION_OPTIONS.lineage], - }, - { - name: i18next.t('label.tier'), - value: entityTierRenderer(tier), - isLink: false, - visible: [DRAWER_NAVIGATION_OPTIONS.lineage], - }, - ]; - - return overview; -}; - -const getDashboardOverview = (dashboardDetails: Dashboard) => { - const { owners, tags, sourceUrl, service, displayName, project, domains } = - dashboardDetails; - const tier = getTierTags(tags ?? []); - const serviceDisplayName = getEntityName(service); - - const overview: BasicEntityOverviewInfo[] = [ - ...getCommonOverview({ owners, domains }), - { - name: `${i18next.t('label.dashboard')} ${i18next.t( - 'label.url-uppercase' - )}`, - value: stringToHTML(displayName ?? '') || NO_DATA, - url: sourceUrl, - isLink: true, - isExternal: true, - visible: [ - DRAWER_NAVIGATION_OPTIONS.lineage, - DRAWER_NAVIGATION_OPTIONS.explore, - ], - }, - { - name: i18next.t('label.service'), - value: serviceDisplayName || NO_DATA, - url: getServiceDetailsPath( - service?.name ?? '', - ServiceCategory.DASHBOARD_SERVICES - ), - isExternal: false, - isLink: true, - visible: [DRAWER_NAVIGATION_OPTIONS.lineage], - }, - { - name: i18next.t('label.tier'), - value: entityTierRenderer(tier), - isLink: false, - isExternal: false, - visible: [DRAWER_NAVIGATION_OPTIONS.lineage], - }, - { - name: i18next.t('label.project'), - value: project ?? NO_DATA, - isLink: false, - visible: [ - DRAWER_NAVIGATION_OPTIONS.explore, - DRAWER_NAVIGATION_OPTIONS.lineage, - ], - }, - ]; - - return overview; -}; - -export const getSearchIndexOverview = ( - searchIndexDetails: SearchIndexEntity -) => { - const { owners, tags, service, domains } = searchIndexDetails; - const tier = getTierTags(tags ?? []); - - const overview: BasicEntityOverviewInfo[] = [ - ...getCommonOverview({ owners, domains }), - { - name: i18next.t('label.tier'), - value: entityTierRenderer(tier), - isLink: false, - isExternal: false, - visible: [DRAWER_NAVIGATION_OPTIONS.lineage], - }, - { - name: i18next.t('label.service'), - value: service?.fullyQualifiedName ?? NO_DATA, - url: getServiceDetailsPath( - service?.name ?? '', - ServiceCategory.SEARCH_SERVICES - ), - isExternal: false, - isLink: true, - visible: [DRAWER_NAVIGATION_OPTIONS.lineage], - }, - ]; - - return overview; -}; - -const getMlModelOverview = (mlModelDetails: Mlmodel) => { - const { algorithm, target, server, dashboard, owners, domains } = - mlModelDetails; - - const overview: BasicEntityOverviewInfo[] = [ - ...getCommonOverview({ owners, domains }), - { - name: i18next.t('label.algorithm'), - value: algorithm || NO_DATA, - url: '', - isLink: false, - visible: [ - DRAWER_NAVIGATION_OPTIONS.lineage, - DRAWER_NAVIGATION_OPTIONS.explore, - ], - }, - { - name: i18next.t('label.target'), - value: target ?? NO_DATA, - url: '', - isLink: false, - visible: [ - DRAWER_NAVIGATION_OPTIONS.lineage, - DRAWER_NAVIGATION_OPTIONS.explore, - ], - }, - { - name: i18next.t('label.server'), - value: server ?? NO_DATA, - url: server, - isLink: Boolean(server), - isExternal: true, - visible: [ - DRAWER_NAVIGATION_OPTIONS.lineage, - DRAWER_NAVIGATION_OPTIONS.explore, - ], - }, - { - name: i18next.t('label.dashboard'), - value: getEntityName(dashboard) || NO_DATA, - url: getEntityDetailsPath( - EntityType.DASHBOARD, - dashboard?.fullyQualifiedName ?? '' - ), - isLink: true, - isExternal: false, - visible: [ - DRAWER_NAVIGATION_OPTIONS.lineage, - DRAWER_NAVIGATION_OPTIONS.explore, - ], - }, - ]; - - return overview; -}; - -const getContainerOverview = (containerDetails: Container) => { - const { numberOfObjects, serviceType, dataModel, owners, domains } = - containerDetails; - - const visible = [ - DRAWER_NAVIGATION_OPTIONS.lineage, - DRAWER_NAVIGATION_OPTIONS.explore, - ]; - - const overview: BasicEntityOverviewInfo[] = [ - ...getCommonOverview({ owners, domains }), - { - name: i18next.t('label.object-plural'), - value: numberOfObjects, - isLink: false, - visible, - }, - { - name: i18next.t('label.service-type'), - value: serviceType, - isLink: false, - visible, - }, - { - name: i18next.t('label.column-plural'), - value: - !isUndefined(dataModel) && dataModel.columns - ? dataModel.columns.length - : NO_DATA, - isLink: false, - visible, - }, - ]; - - return overview; -}; - -const getChartOverview = (chartDetails: Chart) => { - const { - owners, - sourceUrl, - chartType, - service, - serviceType, - displayName, - domains, - } = chartDetails; - const serviceDisplayName = getEntityName(service); - - const overview: BasicEntityOverviewInfo[] = [ - ...getCommonOverview({ owners, domains }), - { - name: `${i18next.t('label.chart')} ${i18next.t('label.url-uppercase')}`, - value: stringToHTML(displayName ?? '') || NO_DATA, - url: sourceUrl, - isLink: true, - isExternal: true, - visible: [ - DRAWER_NAVIGATION_OPTIONS.lineage, - DRAWER_NAVIGATION_OPTIONS.explore, - ], - }, - { - name: i18next.t('label.service'), - value: serviceDisplayName || NO_DATA, - url: getServiceDetailsPath( - service?.name ?? '', - ServiceCategory.DASHBOARD_SERVICES - ), - isExternal: false, - isLink: true, - visible: [ - DRAWER_NAVIGATION_OPTIONS.lineage, - DRAWER_NAVIGATION_OPTIONS.explore, - ], - }, - { - name: i18next.t('label.chart-type'), - value: chartType ?? NO_DATA, - isLink: false, - visible: [ - DRAWER_NAVIGATION_OPTIONS.explore, - DRAWER_NAVIGATION_OPTIONS.lineage, - ], - }, - { - name: i18next.t('label.service-type'), - value: serviceType ?? NO_DATA, - isLink: false, - visible: [ - DRAWER_NAVIGATION_OPTIONS.explore, - DRAWER_NAVIGATION_OPTIONS.lineage, - ], - }, - ]; - - return overview; -}; - -const getDataModelOverview = (dataModelDetails: DashboardDataModel) => { - const { - owners, - tags, - service, - domains, - displayName, - dataModelType, - fullyQualifiedName, - } = dataModelDetails; - const tier = getTierTags(tags ?? []); - - const overview: BasicEntityOverviewInfo[] = [ - ...getCommonOverview({ owners, domains }), - { - name: `${i18next.t('label.data-model')} ${i18next.t( - 'label.url-uppercase' - )}`, - value: stringToHTML(displayName ?? '') || NO_DATA, - url: getEntityDetailsPath( - EntityType.DASHBOARD_DATA_MODEL, - fullyQualifiedName ?? '' - ), - isLink: true, - visible: [ - DRAWER_NAVIGATION_OPTIONS.lineage, - DRAWER_NAVIGATION_OPTIONS.explore, - ], - }, - { - name: i18next.t('label.service'), - value: service?.fullyQualifiedName ?? NO_DATA, - url: getServiceDetailsPath( - service?.name ?? '', - ServiceCategory.DASHBOARD_SERVICES - ), - isExternal: false, - isLink: true, - visible: [ - DRAWER_NAVIGATION_OPTIONS.lineage, - DRAWER_NAVIGATION_OPTIONS.explore, - ], - }, - - { - name: i18next.t('label.tier'), - value: entityTierRenderer(tier), - isLink: false, - isExternal: false, - visible: [ - DRAWER_NAVIGATION_OPTIONS.lineage, - DRAWER_NAVIGATION_OPTIONS.explore, - ], - }, - { - name: i18next.t('label.data-model-type'), - value: dataModelType, - isLink: false, - isExternal: false, - visible: [ - DRAWER_NAVIGATION_OPTIONS.lineage, - DRAWER_NAVIGATION_OPTIONS.explore, - ], - }, - ]; - - return overview; -}; - -const getStoredProcedureOverview = ( - storedProcedureDetails: StoredProcedure -) => { - const { fullyQualifiedName, owners, tags, domains, storedProcedureCode } = - storedProcedureDetails; - const [service, database, schema] = getPartialNameFromTableFQN( - fullyQualifiedName ?? '', - [FqnPart.Service, FqnPart.Database, FqnPart.Schema], - FQN_SEPARATOR_CHAR - ).split(FQN_SEPARATOR_CHAR); - - const tier = getTierTags(tags ?? []); - - const overview: BasicEntityOverviewInfo[] = [ - ...getCommonOverview({ owners, domains }), - { - name: i18next.t('label.service'), - value: service || NO_DATA, - url: getServiceDetailsPath(service, ServiceCategory.DATABASE_SERVICES), - isLink: true, - visible: [DRAWER_NAVIGATION_OPTIONS.lineage], - }, - { - name: i18next.t('label.database'), - value: database || NO_DATA, - url: getEntityDetailsPath( - EntityType.DATABASE, - getPartialNameFromTableFQN( - fullyQualifiedName ?? '', - [FqnPart.Service, FqnPart.Database], - FQN_SEPARATOR_CHAR - ) - ), - isLink: true, - visible: [DRAWER_NAVIGATION_OPTIONS.lineage], - }, - { - name: i18next.t('label.schema'), - value: schema || NO_DATA, - url: getEntityDetailsPath( - EntityType.DATABASE_SCHEMA, - getPartialNameFromTableFQN( - fullyQualifiedName ?? '', - [FqnPart.Service, FqnPart.Database, FqnPart.Schema], - FQN_SEPARATOR_CHAR - ) - ), - isLink: true, - visible: [ - DRAWER_NAVIGATION_OPTIONS.lineage, - DRAWER_NAVIGATION_OPTIONS.explore, - ], - }, - { - name: i18next.t('label.tier'), - value: entityTierRenderer(tier), - isLink: false, - visible: [ - DRAWER_NAVIGATION_OPTIONS.lineage, - DRAWER_NAVIGATION_OPTIONS.explore, - ], - }, - ...(isObject(storedProcedureCode) - ? [ - { - name: i18next.t('label.language'), - value: - (storedProcedureCode as StoredProcedureCodeObject).language ?? - NO_DATA, - isLink: false, - visible: [ - DRAWER_NAVIGATION_OPTIONS.lineage, - DRAWER_NAVIGATION_OPTIONS.explore, - ], - }, - ] - : []), - ]; - - return overview; -}; - -const getDatabaseOverview = (databaseDetails: Database) => { - const { owners, service, domains, tags, usageSummary } = databaseDetails; - - const tier = getTierTags(tags ?? []); - - const overview: BasicEntityOverviewInfo[] = [ - { - name: i18next.t('label.owner-plural'), - value: , - visible: [DRAWER_NAVIGATION_OPTIONS.explore], - }, - ...getCommonOverview({ domains }, false), - { - name: i18next.t('label.tier'), - value: entityTierRenderer(tier), - isLink: false, - visible: [DRAWER_NAVIGATION_OPTIONS.explore], - }, - { - name: i18next.t('label.service'), - value: service?.fullyQualifiedName || NO_DATA, - url: getServiceDetailsPath( - service?.fullyQualifiedName ?? '', - ServiceCategory.DATABASE_SERVICES - ), - isLink: true, - visible: [DRAWER_NAVIGATION_OPTIONS.explore], - }, - - { - name: i18next.t('label.usage'), - value: getUsageData(usageSummary), - isLink: false, - visible: [DRAWER_NAVIGATION_OPTIONS.explore], - }, - ]; - - return overview; -}; - -const getDatabaseSchemaOverview = (databaseSchemaDetails: DatabaseSchema) => { - const { owners, service, tags, domains, usageSummary, database } = - databaseSchemaDetails; - - const tier = getTierTags(tags ?? []); - - const overview: BasicEntityOverviewInfo[] = [ - { - name: i18next.t('label.owner-plural'), - value: , - visible: [DRAWER_NAVIGATION_OPTIONS.explore], - }, - ...getCommonOverview({ domains }, false), - { - name: i18next.t('label.tier'), - value: entityTierRenderer(tier), - isLink: false, - visible: [DRAWER_NAVIGATION_OPTIONS.explore], - }, - { - name: i18next.t('label.service'), - value: service?.fullyQualifiedName ?? NO_DATA, - url: getServiceDetailsPath( - service?.fullyQualifiedName ?? '', - ServiceCategory.DATABASE_SERVICES - ), - isLink: true, - visible: [DRAWER_NAVIGATION_OPTIONS.explore], - }, - { - name: i18next.t('label.database'), - value: database?.fullyQualifiedName ?? NO_DATA, - url: getEntityDetailsPath( - EntityType.DATABASE, - database?.fullyQualifiedName ?? '' - ), - isLink: true, - visible: [DRAWER_NAVIGATION_OPTIONS.explore], - }, - { - name: i18next.t('label.usage'), - value: getUsageData(usageSummary), - isLink: false, - visible: [DRAWER_NAVIGATION_OPTIONS.explore], - }, - ]; - - return overview; -}; - -const getEntityServiceOverview = (serviceDetails: EntityServiceUnion) => { - const { owners, domains, tags, serviceType } = serviceDetails; - - const tier = getTierTags(tags ?? []); - - const overview: BasicEntityOverviewInfo[] = [ - { - name: i18next.t('label.owner-plural'), - value: , - visible: [DRAWER_NAVIGATION_OPTIONS.explore], - }, - ...getCommonOverview({ domains }, false), - { - name: i18next.t('label.tier'), - value: entityTierRenderer(tier), - isLink: false, - visible: [DRAWER_NAVIGATION_OPTIONS.explore], - }, - { - name: i18next.t('label.service-type'), - value: serviceType, - isLink: false, - visible: [DRAWER_NAVIGATION_OPTIONS.explore], - }, - ]; - - return overview; -}; - -const getApiCollectionOverview = (apiCollection: APICollection) => { - if (isNil(apiCollection) || isEmpty(apiCollection)) { - return []; - } - - const { service, domains } = apiCollection; - - const overview: BasicEntityOverviewInfo[] = [ - ...getCommonOverview({ domains }, false), - { - name: i18next.t('label.endpoint-url'), - value: apiCollection.endpointURL || NO_DATA, - url: apiCollection.endpointURL, - isLink: true, - isExternal: true, - visible: [DRAWER_NAVIGATION_OPTIONS.explore], - }, - { - name: i18next.t('label.service'), - value: service?.fullyQualifiedName ?? NO_DATA, - url: getServiceDetailsPath( - service?.fullyQualifiedName ?? '', - ServiceCategory.API_SERVICES - ), - isLink: true, - visible: [DRAWER_NAVIGATION_OPTIONS.explore], - }, - ]; - - return overview; -}; -const getApiEndpointOverview = (apiEndpoint: APIEndpoint) => { - if (isNil(apiEndpoint) || isEmpty(apiEndpoint)) { - return []; - } - const { service, apiCollection, domains } = apiEndpoint; - - const overview: BasicEntityOverviewInfo[] = [ - ...getCommonOverview({ domains }, false), - { - name: i18next.t('label.endpoint-url'), - value: apiEndpoint.endpointURL || NO_DATA, - url: apiEndpoint.endpointURL, - isLink: true, - isExternal: true, - visible: [ - DRAWER_NAVIGATION_OPTIONS.explore, - DRAWER_NAVIGATION_OPTIONS.lineage, - ], - }, - { - name: i18next.t('label.api-collection'), - value: apiEndpoint.apiCollection?.fullyQualifiedName ?? '', - url: getEntityDetailsPath( - EntityType.API_COLLECTION, - apiCollection?.fullyQualifiedName ?? '' - ), - isLink: true, - visible: [ - DRAWER_NAVIGATION_OPTIONS.explore, - DRAWER_NAVIGATION_OPTIONS.lineage, - ], - }, - { - name: i18next.t('label.service'), - value: service?.fullyQualifiedName ?? '', - url: getServiceDetailsPath( - service?.fullyQualifiedName ?? '', - ServiceCategory.API_SERVICES - ), - isLink: true, - visible: [ - DRAWER_NAVIGATION_OPTIONS.explore, - DRAWER_NAVIGATION_OPTIONS.lineage, - ], - }, - { - name: i18next.t('label.request-method'), - value: apiEndpoint.requestMethod || NO_DATA, - isLink: false, - visible: [ - DRAWER_NAVIGATION_OPTIONS.explore, - DRAWER_NAVIGATION_OPTIONS.lineage, - ], - }, - ]; - - return overview; -}; -const getMetricOverview = (metric: Metric) => { - if (isNil(metric) || isEmpty(metric)) { - return []; - } - - const overview: BasicEntityOverviewInfo[] = [ - ...getCommonOverview({ domains: metric.domains }, false), - { - name: i18next.t('label.metric-type'), - value: metric.metricType || NO_DATA, - isLink: false, - visible: [ - DRAWER_NAVIGATION_OPTIONS.explore, - DRAWER_NAVIGATION_OPTIONS.lineage, - ], - }, - { - name: i18next.t('label.unit-of-measurement'), - value: metric.unitOfMeasurement || NO_DATA, - isLink: false, - visible: [ - DRAWER_NAVIGATION_OPTIONS.explore, - DRAWER_NAVIGATION_OPTIONS.lineage, - ], - }, - { - name: i18next.t('label.granularity'), - value: metric.granularity || NO_DATA, - isLink: false, - visible: [ - DRAWER_NAVIGATION_OPTIONS.explore, - DRAWER_NAVIGATION_OPTIONS.lineage, - ], - }, - ]; - - return overview; -}; - -const getDirectoryOverview = (directoryDetails: Directory) => { - const { - numberOfSubDirectories, - numberOfFiles, - serviceType, - owners, - domains, - } = directoryDetails; - - const visible = [ - DRAWER_NAVIGATION_OPTIONS.lineage, - DRAWER_NAVIGATION_OPTIONS.explore, - ]; - - const overview: BasicEntityOverviewInfo[] = [ - ...getCommonOverview({ owners, domains }), - { - name: i18next.t('label.directory-plural'), - value: numberOfSubDirectories ?? NO_DATA, - isLink: false, - visible, - }, - { - name: i18next.t('label.file-plural'), - value: numberOfFiles ?? NO_DATA, - isLink: false, - visible, - }, - { - name: i18next.t('label.service-type'), - value: serviceType, - isLink: false, - visible, - }, - ]; - - return overview; -}; - -const getFileOverview = (fileDetails: File) => { - const { fileExtension, fileType, fileVersion, serviceType, owners, domains } = - fileDetails; - - const visible = [ - DRAWER_NAVIGATION_OPTIONS.lineage, - DRAWER_NAVIGATION_OPTIONS.explore, - ]; - - const overview: BasicEntityOverviewInfo[] = [ - ...getCommonOverview({ owners, domains }), - { - name: i18next.t('label.file-extension'), - value: fileExtension ?? NO_DATA, - isLink: false, - visible, - }, - { - name: i18next.t('label.file-type'), - value: fileType ?? NO_DATA, - isLink: false, - visible, - }, - { - name: i18next.t('label.file-version'), - value: fileVersion ?? NO_DATA, - isLink: false, - visible, - }, - { - name: i18next.t('label.service-type'), - value: serviceType, - isLink: false, - visible, - }, - ]; - - return overview; -}; - -const getSpreadsheetOverview = (spreadsheetDetails: Spreadsheet) => { - const { fileVersion, serviceType, owners, domains } = spreadsheetDetails; - - const visible = [ - DRAWER_NAVIGATION_OPTIONS.lineage, - DRAWER_NAVIGATION_OPTIONS.explore, - ]; - - const overview: BasicEntityOverviewInfo[] = [ - ...getCommonOverview({ owners, domains }), - { - name: i18next.t('label.file-version'), - value: fileVersion ?? NO_DATA, - isLink: false, - visible, - }, - { - name: i18next.t('label.service-type'), - value: serviceType, - isLink: false, - visible, - }, - ]; - - return overview; -}; - -const getWorksheetOverview = (worksheetDetails: Worksheet) => { - const { columnCount, rowCount, serviceType, owners, domains } = - worksheetDetails; - - const visible = [ - DRAWER_NAVIGATION_OPTIONS.lineage, - DRAWER_NAVIGATION_OPTIONS.explore, - ]; - - const overview: BasicEntityOverviewInfo[] = [ - ...getCommonOverview({ owners, domains }), - { - name: i18next.t('label.column-plural'), - value: columnCount ?? NO_DATA, - isLink: false, - visible, - }, - { - name: i18next.t('label.row-plural'), - value: rowCount ?? NO_DATA, - isLink: false, - visible, - }, - { - name: i18next.t('label.service-type'), - value: serviceType, - isLink: false, - visible, - }, - ]; - - return overview; -}; - -export const getEntityOverview = ( - type: string, - entityDetail: DataAssetSummaryPanelProps['dataAsset'], - additionalInfo?: Record -): Array => { - switch (type) { - case ExplorePageTabs.TABLES: - case EntityType.TABLE: { - return getTableOverview(entityDetail as Table, additionalInfo); - } - - case ExplorePageTabs.COLUMNS: - case EntityType.TABLE_COLUMN: { - return getColumnOverview(entityDetail as unknown as ColumnSearchResult); - } - - case ExplorePageTabs.TOPICS: - case EntityType.TOPIC: { - return getTopicOverview(entityDetail as Topic); - } - - case ExplorePageTabs.PIPELINES: - case EntityType.PIPELINE: { - return getPipelineOverview(entityDetail as Pipeline); - } - - case ExplorePageTabs.DASHBOARDS: - case EntityType.DASHBOARD: { - return getDashboardOverview(entityDetail as Dashboard); - } - - case ExplorePageTabs.SEARCH_INDEX: - case EntityType.SEARCH_INDEX: { - return getSearchIndexOverview(entityDetail as SearchIndexEntity); - } - - case ExplorePageTabs.MLMODELS: - case EntityType.MLMODEL: { - return getMlModelOverview(entityDetail as Mlmodel); - } - case ExplorePageTabs.CONTAINERS: - case EntityType.CONTAINER: { - return getContainerOverview(entityDetail as Container); - } - case ExplorePageTabs.CHARTS: - case EntityType.CHART: { - return getChartOverview(entityDetail as Chart); - } - - case ExplorePageTabs.DASHBOARD_DATA_MODEL: - case EntityType.DASHBOARD_DATA_MODEL: { - return getDataModelOverview(entityDetail as DashboardDataModel); - } - - case ExplorePageTabs.STORED_PROCEDURE: - case EntityType.STORED_PROCEDURE: { - return getStoredProcedureOverview(entityDetail as StoredProcedure); - } - - case ExplorePageTabs.DATABASE: - case EntityType.DATABASE: { - return getDatabaseOverview(entityDetail as Database); - } - - case ExplorePageTabs.DATABASE_SCHEMA: - case EntityType.DATABASE_SCHEMA: { - return getDatabaseSchemaOverview(entityDetail as DatabaseSchema); - } - - case ExplorePageTabs.API_COLLECTION: - case EntityType.API_COLLECTION: { - return getApiCollectionOverview(entityDetail as APICollection); - } - - case ExplorePageTabs.API_ENDPOINT: - case EntityType.API_ENDPOINT: { - return getApiEndpointOverview(entityDetail as APIEndpoint); - } - - case ExplorePageTabs.METRIC: - case EntityType.METRIC: { - return getMetricOverview(entityDetail as Metric); - } - - case ExplorePageTabs.DIRECTORIES: - case EntityType.DIRECTORY: { - return getDirectoryOverview(entityDetail as Directory); - } - - case ExplorePageTabs.FILES: - case EntityType.FILE: { - return getFileOverview(entityDetail as File); - } - - case ExplorePageTabs.SPREADSHEETS: - case EntityType.SPREADSHEET: { - return getSpreadsheetOverview(entityDetail as Spreadsheet); - } - - case ExplorePageTabs.WORKSHEETS: - case EntityType.WORKSHEET: { - return getWorksheetOverview(entityDetail as Worksheet); - } - - case ExplorePageTabs.DATABASE_SERVICE: - case ExplorePageTabs.MESSAGING_SERVICE: - case ExplorePageTabs.DASHBOARD_SERVICE: - case ExplorePageTabs.ML_MODEL_SERVICE: - case ExplorePageTabs.PIPELINE_SERVICE: - case ExplorePageTabs.SEARCH_INDEX_SERVICE: - case ExplorePageTabs.API_SERVICE: - case EntityType.DATABASE_SERVICE: - case EntityType.MESSAGING_SERVICE: - case EntityType.DASHBOARD_SERVICE: - case EntityType.MLMODEL_SERVICE: - case EntityType.PIPELINE_SERVICE: - case EntityType.SEARCH_SERVICE: - case EntityType.API_SERVICE: { - return getEntityServiceOverview(entityDetail as EntityServiceUnion); - } - - default: - return []; - } -}; - export const ENTITY_LINK_SEPARATOR = '::'; export const getEntityFeedLink = ( @@ -1687,7 +289,7 @@ export const checkIfJoinsAvailable = ( return ( joins && Boolean(joins.length) && - Boolean(joins.find((join) => join.columnName === columnName)) + Boolean(joins.some((join) => join.columnName === columnName)) ); }; @@ -2145,7 +747,7 @@ export function getBreadcrumbForEntityWithParent< export const getBreadcrumbForTestCase = (entity: TestCase): TitleLink[] => [ { - name: i18next.t('label.data-quality'), + name: i18n.t('label.data-quality'), url: `${ROUTES.DATA_QUALITY}/${DataQualityPageTabs.TEST_CASES}`, }, { @@ -2158,7 +760,7 @@ export const getBreadcrumbForTestCase = (entity: TestCase): TitleLink[] => [ state: { breadcrumbData: [ { - name: i18next.t('label.data-quality'), + name: i18n.t('label.data-quality'), url: `${ROUTES.DATA_QUALITY}/${DataQualityPageTabs.TEST_CASES}`, }, ], @@ -2197,7 +799,7 @@ export const getBreadcrumbForTestSuite = (entity: TestSuite) => { export const getBreadCrumbForKpi = (entity: Kpi) => { return [ { - name: i18next.t('label.kpi-uppercase'), + name: i18n.t('label.kpi-uppercase'), url: getDataInsightPathWithFqn(DataInsightTabs.KPIS), }, { @@ -2525,7 +1127,7 @@ export const getEntityBreadcrumbs = ( case EntityType.DOMAIN: return [ { - name: i18next.t('label.domain-plural'), + name: i18n.t('label.domain-plural'), url: getDomainPath(), }, ]; @@ -2602,7 +1204,7 @@ export const getEntityBreadcrumbs = ( case EntityType.APPLICATION: { return [ { - name: i18next.t('label.application-plural'), + name: i18n.t('label.application-plural'), url: getSettingPath(GlobalSettingsMenuCategory.APPLICATIONS), }, { @@ -2615,7 +1217,7 @@ export const getEntityBreadcrumbs = ( case EntityType.PERSONA: { return [ { - name: i18next.t('label.persona-plural'), + name: i18n.t('label.persona-plural'), url: getSettingPath( GlobalSettingsMenuCategory.MEMBERS, GlobalSettingOptions.PERSONA @@ -2631,7 +1233,7 @@ export const getEntityBreadcrumbs = ( case EntityType.ROLE: { return [ { - name: i18next.t('label.role-plural'), + name: i18n.t('label.role-plural'), url: getSettingPath( GlobalSettingsMenuCategory.ACCESS, GlobalSettingOptions.ROLES @@ -2647,7 +1249,7 @@ export const getEntityBreadcrumbs = ( case EntityType.POLICY: { return [ { - name: i18next.t('label.policy-plural'), + name: i18n.t('label.policy-plural'), url: getSettingPath( GlobalSettingsMenuCategory.ACCESS, GlobalSettingOptions.POLICIES diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/FeedUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/FeedUtils.tsx index a2c50628c7ba..40e34a29e593 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/FeedUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/FeedUtils.tsx @@ -62,7 +62,6 @@ import { getPartialNameFromFQN, getPartialNameFromTableFQN, getRandomColor, - Transi18next, } from './CommonUtils'; import { getRelativeCalendar } from './date-time/DateTimeUtils'; import EntityLink from './EntityLink'; @@ -73,7 +72,7 @@ import { getEntityName, } from './EntityUtils'; import Fqn from './Fqn'; -import { t } from './i18next/LocalUtil'; +import { t, Transi18next } from './i18next/LocalUtil'; import { getImageWithResolutionAndFallback, ImageQuality, diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/FileDetailsUtils.test.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/FileDetailsUtils.test.tsx index 8a202c0a9dab..5d6ddb29fa86 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/FileDetailsUtils.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/FileDetailsUtils.test.tsx @@ -48,11 +48,6 @@ jest.mock('../components/DataAssets/CommonWidgets/CommonWidgets', () => ({ )), })); -jest.mock('../utils/i18next/LocalUtil', () => ({ - t: (key: string) => key, - detectBrowserLanguage: jest.fn().mockReturnValue('en-US'), -})); - jest.mock('../components/DataContract/ContractTab/ContractTab.tsx', () => { return jest.fn().mockImplementation(() =>

DataContractComponent

); }); diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/Fqn.ts b/openmetadata-ui/src/main/resources/ui/src/utils/Fqn.ts index 2b9924e7a073..2fe0430f7676 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/Fqn.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/Fqn.ts @@ -13,10 +13,10 @@ import antlr4 from 'antlr4'; import { ParseTreeWalker } from 'antlr4/src/antlr4/tree'; -import i18next from 'i18next'; import SplitListener from '../antlr/SplitListener'; import FqnLexer from '../generated/antlr/FqnLexer'; import FqnParser from '../generated/antlr/FqnParser'; +import i18n from './i18next/LocalUtil'; export default class Fqn { // Equivalent of Java's FullyQualifiedName#split @@ -45,8 +45,8 @@ export default class Fqn { // Equivalent of Java's FullyQualifiedName#quoteName static quoteName(name: string) { const matcher = /^(")([^"]+)(")$|^(.*)$/.exec(name); - if (!matcher || matcher[0].length !== name.length) { - throw new Error(`${i18next.t('label.invalid-name')} ${name}`); + if (matcher?.[0].length !== name.length) { + throw new Error(`${i18n.t('label.invalid-name')} ${name}`); } // Name matches quoted string "sss". @@ -64,6 +64,6 @@ export default class Fqn { return unquotedName.includes('.') ? '"' + name + '"' : unquotedName; } - throw new Error(`${i18next.t('label.invalid-name')} ${name}`); + throw new Error(`${i18n.t('label.invalid-name')} ${name}`); } } diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/GlobalSettingsUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/GlobalSettingsUtils.tsx index 90e3d8bf8c7f..07ea3e8934be 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/GlobalSettingsUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/GlobalSettingsUtils.tsx @@ -11,7 +11,6 @@ * limitations under the License. */ -import i18next from 'i18next'; import { PLACEHOLDER_ROUTE_FQN, ROUTES } from '../constants/constants'; import { GlobalSettingOptions, @@ -19,6 +18,7 @@ import { } from '../constants/GlobalSettings.constants'; import { EntityType } from '../enums/entity.enum'; import globalSettingsClassBase from './GlobalSettingsClassBase'; +import i18n from './i18next/LocalUtil'; import { getSettingPath } from './RouterUtils'; import { getEncodedFqn } from './StringsUtils'; @@ -113,7 +113,7 @@ export const getSettingPageEntityBreadCrumb = ( return [ { - name: i18next.t('label.setting-plural'), + name: i18n.t('label.setting-plural'), url: ROUTES.SETTINGS, }, { diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/HistoryUtils.ts b/openmetadata-ui/src/main/resources/ui/src/utils/HistoryUtils.ts index 53130b85b4d7..174ae3ce877b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/HistoryUtils.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/HistoryUtils.ts @@ -11,7 +11,7 @@ * limitations under the License. */ export const getBasePath = () => { - return window.BASE_PATH !== '${basePath}' - ? window.BASE_PATH?.slice(0, -1) ?? '' - : ''; + return globalThis.BASE_PATH === '${basePath}' + ? '' + : globalThis.BASE_PATH?.slice(0, -1) ?? ''; }; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/IngestionUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/IngestionUtils.tsx index 4a269eb07ec4..9ada1f5f2789 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/IngestionUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/IngestionUtils.tsx @@ -47,8 +47,7 @@ import { } from '../generated/entity/services/ingestionPipelines/ingestionPipeline'; import { SearchSourceAlias } from '../interface/search.interface'; import { DataObj, ServicesType } from '../interface/service.interface'; -import { Transi18next } from './CommonUtils'; -import i18n from './i18next/LocalUtil'; +import i18n, { Transi18next } from './i18next/LocalUtil'; import { getSchemaByWorkflowType } from './IngestionWorkflowUtils'; import { getServiceDetailsPath, diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/LocationUtils.ts b/openmetadata-ui/src/main/resources/ui/src/utils/LocationUtils.ts new file mode 100644 index 000000000000..eac49336ce74 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/utils/LocationUtils.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2026 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 { getBasePath } from './HistoryUtils'; + +export const getPathNameFromWindowLocation = () => { + return globalThis.location.pathname.replace(getBasePath() ?? '', ''); +}; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/MessagingServiceUtils.ts b/openmetadata-ui/src/main/resources/ui/src/utils/MessagingServiceUtils.ts index c435fa95110a..dced426db1f3 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/MessagingServiceUtils.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/MessagingServiceUtils.ts @@ -12,7 +12,7 @@ */ import { cloneDeep, isUndefined } from 'lodash'; -import { COMMON_UI_SCHEMA } from '../constants/Services.constant'; +import { COMMON_UI_SCHEMA } from '../constants/ServiceUISchema.constant'; import { MessagingConnection, MessagingServiceType, @@ -31,7 +31,7 @@ export const getBrokers = (config: MessagingConnection['config']) => { retVal = config.bootstrapServers; } - return !isUndefined(retVal) ? retVal : '--'; + return isUndefined(retVal) ? '--' : retVal; }; export const getMessagingConfig = (type: MessagingServiceType) => { diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/MetadataServiceUtils.ts b/openmetadata-ui/src/main/resources/ui/src/utils/MetadataServiceUtils.ts index e20dd14cdc03..d1aa2a63a098 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/MetadataServiceUtils.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/MetadataServiceUtils.ts @@ -12,7 +12,7 @@ */ import { cloneDeep } from 'lodash'; -import { COMMON_UI_SCHEMA } from '../constants/Services.constant'; +import { COMMON_UI_SCHEMA } from '../constants/ServiceUISchema.constant'; import { MetadataServiceType } from '../generated/entity/services/metadataService'; import alationSinkConnection from '../jsons/connectionSchemas/connections/metadata/alationSinkConnection.json'; import amundsenConnection from '../jsons/connectionSchemas/connections/metadata/amundsenConnection.json'; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/MlmodelServiceUtils.ts b/openmetadata-ui/src/main/resources/ui/src/utils/MlmodelServiceUtils.ts index 3b3f1e9ea748..7a9f59498b66 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/MlmodelServiceUtils.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/MlmodelServiceUtils.ts @@ -12,7 +12,7 @@ */ import { cloneDeep } from 'lodash'; -import { COMMON_UI_SCHEMA } from '../constants/Services.constant'; +import { COMMON_UI_SCHEMA } from '../constants/ServiceUISchema.constant'; import { MlModelServiceType } from '../generated/entity/services/mlmodelService'; import customMlModelConnection from '../jsons/connectionSchemas/connections/mlmodel/customMlModelConnection.json'; import mlflowConnection from '../jsons/connectionSchemas/connections/mlmodel/mlflowConnection.json'; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/OktaCustomStorage.ts b/openmetadata-ui/src/main/resources/ui/src/utils/OktaCustomStorage.ts index ffe838858ffc..f63481b108ad 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/OktaCustomStorage.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/OktaCustomStorage.ts @@ -11,7 +11,7 @@ * limitations under the License. */ -import { StorageProvider } from '@okta/okta-auth-js'; +import type { StorageProvider } from '@okta/okta-auth-js'; import { swTokenStorage } from './SwTokenStorage'; import { isServiceWorkerAvailable } from './SwTokenStorageUtils'; @@ -19,8 +19,8 @@ const OKTA_TOKENS_KEY = 'okta_tokens'; export class OktaCustomStorage implements StorageProvider { private memoryCache: Record = {}; - private isServiceWorkerAvailable: boolean; - private initPromise: Promise; + private readonly isServiceWorkerAvailable: boolean; + private readonly initPromise: Promise; constructor() { this.isServiceWorkerAvailable = isServiceWorkerAvailable(); @@ -66,9 +66,9 @@ export class OktaCustomStorage implements StorageProvider { return this.memoryCache[key] || null; } - async setItem(key: string, value: string): Promise { + setItem(key: string, value: string) { this.memoryCache[key] = value; - await this.persistToStorage(); + this.persistToStorage(); } removeItem(key: string): void { diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/PipelineServiceUtils.ts b/openmetadata-ui/src/main/resources/ui/src/utils/PipelineServiceUtils.ts index cfa7e7c42b21..5b36af83834a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/PipelineServiceUtils.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/PipelineServiceUtils.ts @@ -12,7 +12,7 @@ */ import { cloneDeep } from 'lodash'; -import { COMMON_UI_SCHEMA } from '../constants/Services.constant'; +import { COMMON_UI_SCHEMA } from '../constants/ServiceUISchema.constant'; import { PipelineServiceType } from '../generated/entity/services/pipelineService'; import airbyteConnection from '../jsons/connectionSchemas/connections/pipeline/airbyteConnection.json'; import airflowConnection from '../jsons/connectionSchemas/connections/pipeline/airflowConnection.json'; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/RouterUtils.ts b/openmetadata-ui/src/main/resources/ui/src/utils/RouterUtils.ts index 8f276dbc3a08..426666848f6b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/RouterUtils.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/RouterUtils.ts @@ -51,7 +51,6 @@ import { useMarketplaceStore } from '../hooks/useMarketplaceStore'; import { DataQualityPageTabs } from '../pages/DataQuality/DataQualityPage.interface'; import { TestCasePageTabs } from '../pages/IncidentManager/IncidentManager.interface'; import { getPartialNameFromFQN } from './CommonUtils'; -import { getBasePath } from './HistoryUtils'; import { getServiceRouteFromServiceType } from './ServiceUtils'; import { getEncodedFqn } from './StringsUtils'; @@ -706,9 +705,6 @@ export const getNotificationAlertDetailsPath = (fqn: string, tab?: string) => { return path; }; -export const getPathNameFromWindowLocation = () => { - return window.location.pathname.replace(getBasePath() ?? '', ''); -}; export const getTagsDetailsPath = (entityFQN: string) => { let path = ROUTES.TAG_DETAILS; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/SchedularUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/SchedularUtils.tsx index 59da299ce37c..61508f3e9d6d 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/SchedularUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/SchedularUtils.tsx @@ -13,7 +13,6 @@ import { Select } from 'antd'; import cronstrue from 'cronstrue/i18n'; -import { t } from 'i18next'; import { isUndefined, toNumber, toString } from 'lodash'; import { RuleObject } from 'rc-field-form/es/interface'; import { @@ -36,6 +35,7 @@ import { } from '../constants/Schedular.constants'; import { CronTypes } from '../enums/Schedular.enum'; import { FieldTypes, FormItemLayout } from '../interface/FormUtils.interface'; +import i18n from './i18next/LocalUtil'; export const getScheduleOptionsFromSchedules = ( scheduleOptions: string[] @@ -303,29 +303,35 @@ export const cronValidator = async (_: RuleObject, value: string) => { // Check if the cron expression has exactly 5 fields (standard Unix cron) if (cronParts.length !== 5) { - return Promise.reject(new Error(t('message.cron-invalid-field-count'))); + return Promise.reject( + new Error(i18n.t('message.cron-invalid-field-count')) + ); } // Validate that each field follows standard Unix cron format const [minute, hour, dayOfMonth, month, dayOfWeek] = cronParts; if (!MINUTE_PATTERN.test(minute)) { - return Promise.reject(new Error(t('message.cron-invalid-minute-field'))); + return Promise.reject( + new Error(i18n.t('message.cron-invalid-minute-field')) + ); } if (!HOUR_PATTERN.test(hour)) { - return Promise.reject(new Error(t('message.cron-invalid-hour-field'))); + return Promise.reject(new Error(i18n.t('message.cron-invalid-hour-field'))); } if (!DAY_OF_MONTH_PATTERN.test(dayOfMonth)) { return Promise.reject( - new Error(t('message.cron-invalid-day-of-month-field')) + new Error(i18n.t('message.cron-invalid-day-of-month-field')) ); } if (!MONTH_PATTERN.test(month)) { - return Promise.reject(new Error(t('message.cron-invalid-month-field'))); + return Promise.reject( + new Error(i18n.t('message.cron-invalid-month-field')) + ); } if (!DAY_OF_WEEK_PATTERN.test(dayOfWeek)) { return Promise.reject( - new Error(t('message.cron-invalid-day-of-week-field')) + new Error(i18n.t('message.cron-invalid-day-of-week-field')) ); } @@ -339,14 +345,14 @@ export const cronValidator = async (_: RuleObject, value: string) => { if (isFrequencyInMinutes || isFrequencyInSeconds) { return Promise.reject( - new Error(t('message.cron-less-than-hour-message')) + new Error(i18n.t('message.cron-less-than-hour-message')) ); } return Promise.resolve(); } catch { // If cronstrue fails to parse, it's an invalid cron expression - return Promise.reject(new Error(t('message.cron-invalid-expression'))); + return Promise.reject(new Error(i18n.t('message.cron-invalid-expression'))); } }; @@ -355,7 +361,7 @@ export const getRaiseOnErrorFormField = ( ) => { return { name: 'raiseOnError', - label: t('label.raise-on-error'), + label: i18n.t('label.raise-on-error'), type: FieldTypes.SWITCH, required: false, formItemProps: { diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/SearchServiceUtils.ts b/openmetadata-ui/src/main/resources/ui/src/utils/SearchServiceUtils.ts index 0cc1a24d4987..c10d3bbc018d 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/SearchServiceUtils.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/SearchServiceUtils.ts @@ -12,7 +12,7 @@ */ import { cloneDeep } from 'lodash'; -import { COMMON_UI_SCHEMA } from '../constants/Services.constant'; +import { COMMON_UI_SCHEMA } from '../constants/ServiceUISchema.constant'; import { SearchServiceType } from '../generated/entity/services/searchService'; import customSearchConnection from '../jsons/connectionSchemas/connections/search/customSearchConnection.json'; import elasticSearchConnection from '../jsons/connectionSchemas/connections/search/elasticSearchConnection.json'; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/SearchUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/SearchUtils.tsx index a19fe3045ecd..21161bec89bd 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/SearchUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/SearchUtils.tsx @@ -13,8 +13,6 @@ import { SearchOutlined } from '@ant-design/icons'; import { Button, Typography } from 'antd'; -import i18next from 'i18next'; -import { isEmpty } from 'lodash'; import { Bucket } from 'Models'; import { Link } from 'react-router-dom'; import { ReactComponent as GlossaryTermIcon } from '../assets/svg/book.svg'; @@ -33,7 +31,6 @@ import { ReactComponent as IconMlModal } from '../assets/svg/mlmodal.svg'; import { ReactComponent as IconPipeline } from '../assets/svg/pipeline-grey.svg'; import { ReactComponent as IconTag } from '../assets/svg/tag-grey.svg'; import { ReactComponent as IconTopic } from '../assets/svg/topic-grey.svg'; -import { WILD_CARD_CHAR } from '../constants/char.constants'; import { Option, SearchSuggestions, @@ -42,179 +39,129 @@ import { EntityType, FqnPart } from '../enums/entity.enum'; import { SearchIndex } from '../enums/search.enum'; import { SearchSourceAlias } from '../interface/search.interface'; import { getPartialNameFromTableFQN } from './CommonUtils'; +import i18n from './i18next/LocalUtil'; import { ElasticsearchQuery } from './QueryBuilderUtils'; import searchClassBase from './SearchClassBase'; import serviceUtilClassBase from './ServiceUtilClassBase'; -import { escapeESReservedCharacters, getEncodedFqn } from './StringsUtils'; - -export const getSearchAPIQueryParams = ( - queryString: string, - from: number, - size: number, - filters: string, - sortField: string, - sortOrder: string, - searchIndex: SearchIndex | SearchIndex[], - onlyDeleted = false, - trackTotalHits = false, - wildcard = true -): Record => { - const start = (from - 1) * size; - - const encodedQueryString = queryString - ? getEncodedFqn(escapeESReservedCharacters(queryString)) - : ''; - - const query = - wildcard && encodedQueryString !== WILD_CARD_CHAR - ? `*${encodedQueryString}*` - : encodedQueryString; - - const params: Record = { - q: query + (filters ? ` AND ${filters}` : ''), - from: start, - size, - index: searchIndex, - deleted: onlyDeleted, - }; - - if (!isEmpty(sortField)) { - params.sort_field = sortField; - } - - if (!isEmpty(sortOrder)) { - params.sort_order = sortOrder; - } - - if (trackTotalHits) { - params.track_total_hits = trackTotalHits; - } - - return params; -}; - -// will add back slash "\" before quote in string if present -export const getQueryWithSlash = (query: string): string => - query.replace(/["']/g, '\\$&'); export const getGroupLabel = (index: string) => { let label = ''; let GroupIcon; switch (index) { case SearchIndex.TOPIC: - label = i18next.t('label.topic-plural'); + label = i18n.t('label.topic-plural'); GroupIcon = IconTopic; break; case SearchIndex.DATABASE: - label = i18next.t('label.database-plural'); + label = i18n.t('label.database-plural'); GroupIcon = IconDatabase; break; case SearchIndex.DATABASE_SCHEMA: - label = i18next.t('label.database-schema-plural'); + label = i18n.t('label.database-schema-plural'); GroupIcon = IconDatabaseSchema; break; case SearchIndex.DASHBOARD: - label = i18next.t('label.dashboard-plural'); + label = i18n.t('label.dashboard-plural'); GroupIcon = IconDashboard; break; case SearchIndex.PIPELINE: - label = i18next.t('label.pipeline-plural'); + label = i18n.t('label.pipeline-plural'); GroupIcon = IconPipeline; break; case SearchIndex.MLMODEL: - label = i18next.t('label.ml-model-plural'); + label = i18n.t('label.ml-model-plural'); GroupIcon = IconMlModal; break; case SearchIndex.GLOSSARY_TERM: - label = i18next.t('label.glossary-term-plural'); + label = i18n.t('label.glossary-term-plural'); GroupIcon = GlossaryTermIcon; break; case SearchIndex.TAG: - label = i18next.t('label.tag-plural'); + label = i18n.t('label.tag-plural'); GroupIcon = IconTag; break; case SearchIndex.CONTAINER: - label = i18next.t('label.container-plural'); + label = i18n.t('label.container-plural'); GroupIcon = IconContainer; break; case SearchIndex.STORED_PROCEDURE: - label = i18next.t('label.stored-procedure-plural'); + label = i18n.t('label.stored-procedure-plural'); GroupIcon = IconStoredProcedure; break; case SearchIndex.DASHBOARD_DATA_MODEL: - label = i18next.t('label.data-model-plural'); + label = i18n.t('label.data-model-plural'); GroupIcon = IconDashboard; break; case SearchIndex.SEARCH_INDEX: - label = i18next.t('label.search-index-plural'); + label = i18n.t('label.search-index-plural'); GroupIcon = SearchOutlined; break; case SearchIndex.DATA_PRODUCT: - label = i18next.t('label.data-product-plural'); + label = i18n.t('label.data-product-plural'); GroupIcon = DataProductIcon; break; case SearchIndex.CHART: - label = i18next.t('label.chart-plural'); + label = i18n.t('label.chart-plural'); GroupIcon = IconChart; break; case SearchIndex.API_COLLECTION: - label = i18next.t('label.api-collection-plural'); + label = i18n.t('label.api-collection-plural'); GroupIcon = IconApiCollection; break; case SearchIndex.API_ENDPOINT: - label = i18next.t('label.api-endpoint-plural'); + label = i18n.t('label.api-endpoint-plural'); GroupIcon = IconApiEndpoint; break; case SearchIndex.METRIC: - label = i18next.t('label.metric-plural'); + label = i18n.t('label.metric-plural'); GroupIcon = MetricIcon; break; case SearchIndex.DIRECTORY: - label = i18next.t('label.directory-plural'); + label = i18n.t('label.directory-plural'); GroupIcon = MetricIcon; break; case SearchIndex.FILE: - label = i18next.t('label.file-plural'); + label = i18n.t('label.file-plural'); GroupIcon = MetricIcon; break; case SearchIndex.SPREADSHEET: - label = i18next.t('label.spreadsheet-plural'); + label = i18n.t('label.spreadsheet-plural'); GroupIcon = MetricIcon; break; case SearchIndex.WORKSHEET: - label = i18next.t('label.worksheet-plural'); + label = i18n.t('label.worksheet-plural'); GroupIcon = MetricIcon; break; case SearchIndex.COLUMN: - label = i18next.t('label.column-plural'); + label = i18n.t('label.column-plural'); GroupIcon = ColumnIcon; break; @@ -273,7 +220,7 @@ export const getSuggestionElement = ( @@ -446,16 +393,14 @@ export const getTermQuery = ( wildcardMustNotQueries?: Record; } ) => { - const termQueries = Object.entries(terms) - .map(([field, value]) => { - const nestedPath = getNestedPath(field); - if (Array.isArray(value)) { - return value.map((v) => wrapTermQuery(field, v, nestedPath)); - } + const termQueries = Object.entries(terms).flatMap(([field, value]) => { + const nestedPath = getNestedPath(field); + if (Array.isArray(value)) { + return value.map((v) => wrapTermQuery(field, v, nestedPath)); + } - return wrapTermQuery(field, value, nestedPath); - }) - .flat(); + return wrapTermQuery(field, value, nestedPath); + }); const wildcardQueries = options?.wildcardTerms ? Object.entries(options.wildcardTerms).map(([field, value]) => ({ @@ -464,16 +409,14 @@ export const getTermQuery = ( : []; const mustNotQueries = options?.mustNotTerms - ? Object.entries(options.mustNotTerms) - .map(([field, value]) => { - const nestedPath = getNestedPath(field); - if (Array.isArray(value)) { - return value.map((v) => wrapTermQuery(field, v, nestedPath)); - } - - return wrapTermQuery(field, value, nestedPath); - }) - .flat() + ? Object.entries(options.mustNotTerms).flatMap(([field, value]) => { + const nestedPath = getNestedPath(field); + if (Array.isArray(value)) { + return value.map((v) => wrapTermQuery(field, v, nestedPath)); + } + + return wrapTermQuery(field, value, nestedPath); + }) : []; const matchQueries = options?.matchTerms @@ -519,15 +462,15 @@ export const getTermQuery = ( // Handle wildcardMustNotQueries const wildcardMustNotQueries = options?.wildcardMustNotQueries - ? Object.entries(options.wildcardMustNotQueries) - .map(([field, value]) => { + ? Object.entries(options.wildcardMustNotQueries).flatMap( + ([field, value]) => { if (Array.isArray(value)) { return value.map((v) => ({ wildcard: { [field]: v } })); } return { wildcard: { [field]: value } }; - }) - .flat() + } + ) : []; const allMustNotQueries = [...mustNotQueries, ...wildcardMustNotQueries]; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/SecurityServiceUtils.ts b/openmetadata-ui/src/main/resources/ui/src/utils/SecurityServiceUtils.ts index b1694cd07925..38df67e0a24c 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/SecurityServiceUtils.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/SecurityServiceUtils.ts @@ -12,22 +12,16 @@ */ import { cloneDeep } from 'lodash'; -import { COMMON_UI_SCHEMA } from '../constants/Services.constant'; +import { COMMON_UI_SCHEMA } from '../constants/ServiceUISchema.constant'; import { Type } from '../generated/entity/services/securityService'; +import rangerConnection from '../jsons/connectionSchemas/connections/security/rangerConnection.json'; export const getSecurityConfig = (type: Type) => { let schema = {}; const uiSchema = { ...COMMON_UI_SCHEMA }; - switch (type) { - case Type.Ranger: { - // eslint-disable-next-line @typescript-eslint/no-require-imports - schema = require('../jsons/connectionSchemas/connections/security/rangerConnection.json'); - - break; - } - default: - break; + if (type === Type.Ranger) { + schema = rangerConnection; } return cloneDeep({ schema, uiSchema }); diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/ServiceIconUtils.ts b/openmetadata-ui/src/main/resources/ui/src/utils/ServiceIconUtils.ts new file mode 100644 index 000000000000..2b28006e2168 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/utils/ServiceIconUtils.ts @@ -0,0 +1,273 @@ +/* + * Copyright 2022 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 athena from '../assets/img/service-icon-athena.png'; +import azuresql from '../assets/img/service-icon-azuresql.png'; +import bigtable from '../assets/img/service-icon-bigtable.png'; +import burstiq from '../assets/img/service-icon-burstiq.png'; +import cassandra from '../assets/img/service-icon-cassandra.png'; +import clickhouse from '../assets/img/service-icon-clickhouse.png'; +import cockroach from '../assets/img/service-icon-cockroach.png'; +import couchbase from '../assets/img/service-icon-couchbase.svg'; +import databrick from '../assets/img/service-icon-databrick.png'; +import datalake from '../assets/img/service-icon-datalake.png'; +import deltalake from '../assets/img/service-icon-delta-lake.png'; +import doris from '../assets/img/service-icon-doris.png'; +import druid from '../assets/img/service-icon-druid.png'; +import dynamodb from '../assets/img/service-icon-dynamodb.png'; +import exasol from '../assets/img/service-icon-exasol.png'; +import glue from '../assets/img/service-icon-glue.png'; +import greenplum from '../assets/img/service-icon-greenplum.png'; +import hive from '../assets/img/service-icon-hive.png'; +import ibmdb2 from '../assets/img/service-icon-ibmdb2.png'; +import impala from '../assets/img/service-icon-impala.png'; +import iomete from '../assets/img/service-icon-iomete.png'; +import mariadb from '../assets/img/service-icon-mariadb.png'; +import mongodb from '../assets/img/service-icon-mongodb.png'; +import mssql from '../assets/img/service-icon-mssql.png'; +import oracle from '../assets/img/service-icon-oracle.png'; +import pinot from '../assets/img/service-icon-pinot.png'; +import postgres from '../assets/img/service-icon-post.png'; +import presto from '../assets/img/service-icon-presto.png'; +import bigquery from '../assets/img/service-icon-query.png'; +import redshift from '../assets/img/service-icon-redshift.png'; +import salesforce from '../assets/img/service-icon-salesforce.png'; +import saperp from '../assets/img/service-icon-sap-erp.png'; +import saphana from '../assets/img/service-icon-sap-hana.png'; +import sas from '../assets/img/service-icon-sas.svg'; +import singlestore from '../assets/img/service-icon-singlestore.png'; +import snowflake from '../assets/img/service-icon-snowflakes.png'; +import mysql from '../assets/img/service-icon-sql.png'; +import sqlite from '../assets/img/service-icon-sqlite.png'; +import starrocks from '../assets/img/service-icon-starrocks.png'; +import timescale from '../assets/img/service-icon-timescale.png'; +import trino from '../assets/img/service-icon-trino.png'; +import unitycatalog from '../assets/img/service-icon-unitycatalog.svg'; +import vertica from '../assets/img/service-icon-vertica.png'; +import teradata from '../assets/svg/teradata.svg'; + +// Messaging services +import kafka from '../assets/img/service-icon-kafka.png'; +import kinesis from '../assets/img/service-icon-kinesis.png'; +import redpanda from '../assets/img/service-icon-redpanda.png'; +import pubsub from '../assets/svg/service-icon-pubsub.svg'; + +// Dashboard services +import domo from '../assets/img/service-icon-domo.png'; +import grafana from '../assets/img/service-icon-grafana.png'; +import lightdash from '../assets/img/service-icon-lightdash.png'; +import looker from '../assets/img/service-icon-looker.png'; +import metabase from '../assets/img/service-icon-metabase.png'; +import microstrategy from '../assets/img/service-icon-microstrategy.svg'; +import mode from '../assets/img/service-icon-mode.png'; +import powerbi from '../assets/img/service-icon-power-bi.png'; +import qliksense from '../assets/img/service-icon-qlik-sense.png'; +import quicksight from '../assets/img/service-icon-quicksight.png'; +import redash from '../assets/img/service-icon-redash.png'; +import sigma from '../assets/img/service-icon-sigma.png'; +import superset from '../assets/img/service-icon-superset.png'; +import tableau from '../assets/img/service-icon-tableau.png'; +import hex from '../assets/svg/service-icon-hex.svg'; + +// Pipeline services +import airbyte from '../assets/img/Airbyte.png'; +import airflow from '../assets/img/service-icon-airflow.png'; +import dagster from '../assets/img/service-icon-dagster.png'; +import dbt from '../assets/img/service-icon-dbt.png'; +import fivetran from '../assets/img/service-icon-fivetran.png'; +import flink from '../assets/img/service-icon-flink.png'; +import nifi from '../assets/img/service-icon-nifi.png'; +import openlineage from '../assets/img/service-icon-openlineage.svg'; +import spark from '../assets/img/service-icon-spark.png'; +import spline from '../assets/img/service-icon-spline.png'; + +// ML Model services +import sagemaker from '../assets/img/service-icon-sagemaker.png'; +import scikit from '../assets/img/service-icon-scikit.png'; +import mlflow from '../assets/svg/service-icon-mlflow.svg'; + +// Storage services +import amazons3 from '../assets/img/service-icon-amazon-s3.svg'; +import gcs from '../assets/img/service-icon-gcs.png'; + +// Search services +import elasticsearch from '../assets/svg/elasticsearch.svg'; +import opensearch from '../assets/svg/open-search.svg'; + +// Metadata services +import alationsink from '../assets/img/service-icon-alation-sink.png'; +import amundsen from '../assets/img/service-icon-amundsen.png'; +import atlas from '../assets/img/service-icon-atlas.svg'; + +// Drive services +import googledrive from '../assets/svg/service-icon-google-drive.svg'; +import sftp from '../assets/svg/service-icon-sftp.svg'; + +// Default icons +import synapse from '../assets/img/service-icon-synapse.png'; +import dashboarddefault from '../assets/svg/dashboard.svg'; +import defaultservice from '../assets/svg/default-service-icon.svg'; +import databasedefault from '../assets/svg/ic-custom-database.svg'; +import customdrivedefault from '../assets/svg/ic-custom-drive.svg'; +import mlmodeldefault from '../assets/svg/ic-custom-model.svg'; +import searchdefault from '../assets/svg/ic-custom-search.svg'; +import storagedefault from '../assets/svg/ic-custom-storage.svg'; +import drivedefault from '../assets/svg/ic-drive-service.svg'; +import restservice from '../assets/svg/ic-service-rest-api.svg'; +import logo from '../assets/svg/logo-monogram.svg'; +import pipelinedefault from '../assets/svg/pipeline.svg'; +import securitydefault from '../assets/svg/security-safe.svg'; +import topicdefault from '../assets/svg/topic.svg'; + +const SERVICE_ICON_LOADERS: Record = { + // Database services + mysql: mysql, + sqlite: sqlite, + mssql: mssql, + redshift: redshift, + bigquery: bigquery, + bigtable: bigtable, + hive: hive, + impala: impala, + postgres: postgres, + oracle: oracle, + snowflake: snowflake, + athena: athena, + presto: presto, + trino: trino, + glue: glue, + mariadb: mariadb, + vertica: vertica, + azuresql: azuresql, + clickhouse: clickhouse, + databricks: databrick, + unitycatalog: unitycatalog, + db2: ibmdb2, + doris: doris, + starrocks: starrocks, + druid: druid, + dynamodb: dynamodb, + singlestore: singlestore, + salesforce: salesforce, + saphana: saphana, + saperp: saperp, + deltalake: deltalake, + pinotdb: pinot, + datalake: datalake, + exasol: exasol, + mongodb: mongodb, + cassandra: cassandra, + couchbase: couchbase, + greenplum: greenplum, + teradata: teradata, + cockroach: cockroach, + timescale: timescale, + burstiq: burstiq, + sas: sas, + iomete: iomete, + domodatabase: domo, + customdatabase: databasedefault, + + // Messaging services + kafka: kafka, + pubsub: pubsub, + redpanda: redpanda, + kinesis: kinesis, + custommessaging: topicdefault, + + // Dashboard services + superset: superset, + looker: looker, + tableau: tableau, + redash: redash, + metabase: metabase, + powerbi: powerbi, + sigma: sigma, + mode: mode, + domodashboard: domo, + quicksight: quicksight, + qliksense: qliksense, + lightdash: lightdash, + microstrategy: microstrategy, + grafana: grafana, + hex: hex, + customdashboard: dashboarddefault, + + // Pipeline services + airflow: airflow, + airbyte: airbyte, + dagster: dagster, + dbtcloud: dbt, + fivetran: fivetran, + nifi: nifi, + spark: spark, + spline: spline, + flink: flink, + openlineage: openlineage, + domopipeline: domo, + kafkaconnect: kafka, + databrickspipeline: databrick, + gluepipeline: glue, + custompipeline: pipelinedefault, + + // ML Model services + mlflow: mlflow, + scikit: scikit, + sagemaker: sagemaker, + custommlmodel: mlmodeldefault, + + // Storage services + s3: amazons3, + gcs: gcs, + + // Search services + elasticsearch: elasticsearch, + opensearch: opensearch, + + // Metadata services + amundsen: amundsen, + atlas: atlas, + alationsink: alationsink, + openmetadata: logo, + + // Drive services + googledrive: googledrive, + sftp: sftp, + customdrive: customdrivedefault, + + // API services + rest: restservice, + + // Default icons + defaultservice: defaultservice, + databasedefault: databasedefault, + topicdefault: topicdefault, + dashboarddefault: dashboarddefault, + pipelinedefault: pipelinedefault, + mlmodeldefault: mlmodeldefault, + storagedefault: storagedefault, + drivedefault: drivedefault, + customdrivedefault: customdrivedefault, + searchdefault: searchdefault, + securitydefault: securitydefault, + restservice: restservice, + logo: logo, + synapse: synapse, +}; + +export const getServiceIcon = (iconKey: string): string => { + const normalizedKey = iconKey.toLowerCase().replaceAll(/[_-]/g, ''); + const icon = SERVICE_ICON_LOADERS[normalizedKey]; + + return icon; +}; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/ServiceInsightsTabUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/ServiceInsightsTabUtils.tsx index f3945a27c9a0..4e239e13d188 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/ServiceInsightsTabUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/ServiceInsightsTabUtils.tsx @@ -42,10 +42,10 @@ import { } from '../generated/entity/services/ingestionPipelines/ingestionPipeline'; import { DataInsightCustomChartResult } from '../rest/DataInsightAPI'; import i18n from '../utils/i18next/LocalUtil'; -import { Transi18next } from './CommonUtils'; import documentationLinksClassBase from './DocumentationLinksClassBase'; import { getEntityNameLabel } from './EntityUtils'; import Fqn from './Fqn'; +import { Transi18next } from './i18next/LocalUtil'; import { getEntityIcon } from './TableUtils'; const { t } = i18n; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/ServiceUtilClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/ServiceUtilClassBase.ts index a6c3cab1f8ca..22228d14234f 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/ServiceUtilClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/ServiceUtilClassBase.ts @@ -12,7 +12,7 @@ */ import { ObjectFieldTemplatePropertyType } from '@rjsf/utils'; -import { get, isEmpty, toLower } from 'lodash'; +import { get, isEmpty } from 'lodash'; import { ServiceTypes } from 'Models'; import GlossaryIcon from '../assets/svg/book.svg'; import ChartIcon from '../assets/svg/chart.svg'; @@ -25,104 +25,6 @@ import AgentsStatusWidget from '../components/ServiceInsights/AgentsStatusWidget import PlatformInsightsWidget from '../components/ServiceInsights/PlatformInsightsWidget/PlatformInsightsWidget'; import TotalDataAssetsWidget from '../components/ServiceInsights/TotalDataAssetsWidget/TotalDataAssetsWidget'; import MetadataAgentsWidget from '../components/Settings/Services/Ingestion/MetadataAgentsWidget/MetadataAgentsWidget'; -import { - AIRBYTE, - AIRFLOW, - ALATIONSINK, - AMAZON_S3, - AMUNDSEN, - ATHENA, - ATLAS, - AZURESQL, - BIGQUERY, - BIGTABLE, - BURSTIQ, - CASSANDRA, - CLICKHOUSE, - COCKROACH, - COUCHBASE, - CUSTOM_DRIVE_DEFAULT, - CUSTOM_SEARCH_DEFAULT, - CUSTOM_STORAGE_DEFAULT, - DAGSTER, - DASHBOARD_DEFAULT, - DATABASE_DEFAULT, - DATABRICK, - DATALAKE, - DBT, - DEFAULT_SERVICE, - DELTALAKE, - DOMO, - DORIS, - DRUID, - DYNAMODB, - ELASTIC_SEARCH, - EXASOL, - FIVETRAN, - FLINK, - GCS, - GLUE, - GOOGLE_DRIVE, - GRAFANA, - GREENPLUM, - HEX, - HIVE, - IBMDB2, - IMPALA, - IOMETE, - KAFKA, - KINESIS, - LIGHT_DASH, - LOGO, - LOOKER, - MARIADB, - METABASE, - MICROSTRATEGY, - MLFLOW, - ML_MODEL_DEFAULT, - MODE, - MONGODB, - MSSQL, - MYSQL, - NIFI, - OPENLINEAGE, - OPEN_SEARCH, - ORACLE, - PINOT, - PIPELINE_DEFAULT, - POSTGRES, - POWERBI, - PRESTO, - QLIK_SENSE, - QUICKSIGHT, - REDASH, - REDPANDA, - REDSHIFT, - REST_SERVICE, - SAGEMAKER, - SALESFORCE, - SAP_ERP, - SAP_HANA, - SAS, - SCIKIT, - SFTP, - SIGMA, - SINGLESTORE, - SNOWFLAKE, - SPARK, - SPLINE, - SQLITE, - STARROCKS, - SUPERSET, - SYNAPSE, - TABLEAU, - TERADATA, - TIMESCALE, - TOPIC_DEFAULT, - TRINO, - UNITYCATALOG, - VERTICA, -} from '../constants/Services.constant'; import { EntityType } from '../enums/entity.enum'; import { ExplorePageTabs } from '../enums/Explore.enum'; import { @@ -169,6 +71,7 @@ import { getMlmodelConfig } from './MlmodelServiceUtils'; import { getPipelineConfig } from './PipelineServiceUtils'; import { getSearchServiceConfig } from './SearchServiceUtils'; import { getSecurityConfig } from './SecurityServiceUtils'; +import { getServiceIcon } from './ServiceIconUtils'; import { getSearchIndexFromService, getTestConnectionName, @@ -414,149 +317,45 @@ class ServiceUtilClassBase { return EntityType.TABLE; } - private readonly serviceLogoMap = new Map([ - [this.DatabaseServiceTypeSmallCase.CustomDatabase, DATABASE_DEFAULT], - [this.DatabaseServiceTypeSmallCase.Mysql, MYSQL], - [this.DatabaseServiceTypeSmallCase.Redshift, REDSHIFT], - [this.DatabaseServiceTypeSmallCase.BigQuery, BIGQUERY], - [this.DatabaseServiceTypeSmallCase.BigTable, BIGTABLE], - [this.DatabaseServiceTypeSmallCase.Hive, HIVE], - [this.DatabaseServiceTypeSmallCase.Impala, IMPALA], - [this.DatabaseServiceTypeSmallCase.Postgres, POSTGRES], - [this.DatabaseServiceTypeSmallCase.Oracle, ORACLE], - [this.DatabaseServiceTypeSmallCase.Snowflake, SNOWFLAKE], - [this.DatabaseServiceTypeSmallCase.Mssql, MSSQL], - [this.DatabaseServiceTypeSmallCase.Athena, ATHENA], - [this.DatabaseServiceTypeSmallCase.Presto, PRESTO], - [this.DatabaseServiceTypeSmallCase.Trino, TRINO], - [this.DatabaseServiceTypeSmallCase.Glue, GLUE], - [this.DatabaseServiceTypeSmallCase.DomoDatabase, DOMO], - [this.DatabaseServiceTypeSmallCase.MariaDB, MARIADB], - [this.DatabaseServiceTypeSmallCase.Vertica, VERTICA], - [this.DatabaseServiceTypeSmallCase.AzureSQL, AZURESQL], - [this.DatabaseServiceTypeSmallCase.Clickhouse, CLICKHOUSE], - [this.DatabaseServiceTypeSmallCase.Databricks, DATABRICK], - [this.DatabaseServiceTypeSmallCase.UnityCatalog, UNITYCATALOG], - [this.DatabaseServiceTypeSmallCase.Db2, IBMDB2], - [this.DatabaseServiceTypeSmallCase.Doris, DORIS], - [this.DatabaseServiceTypeSmallCase.StarRocks, STARROCKS], - [this.DatabaseServiceTypeSmallCase.Druid, DRUID], - [this.DatabaseServiceTypeSmallCase.DynamoDB, DYNAMODB], - [this.DatabaseServiceTypeSmallCase.Exasol, EXASOL], - [this.DatabaseServiceTypeSmallCase.SingleStore, SINGLESTORE], - [this.DatabaseServiceTypeSmallCase.SQLite, SQLITE], - [this.DatabaseServiceTypeSmallCase.Salesforce, SALESFORCE], - [this.DatabaseServiceTypeSmallCase.SapHana, SAP_HANA], - [this.DatabaseServiceTypeSmallCase.SapERP, SAP_ERP], - [this.DatabaseServiceTypeSmallCase.DeltaLake, DELTALAKE], - [this.DatabaseServiceTypeSmallCase.PinotDB, PINOT], - [this.DatabaseServiceTypeSmallCase.Datalake, DATALAKE], - [this.DatabaseServiceTypeSmallCase.MongoDB, MONGODB], - [this.DatabaseServiceTypeSmallCase.Cassandra, CASSANDRA], - [this.DatabaseServiceTypeSmallCase.SAS, SAS], - [this.DatabaseServiceTypeSmallCase.Couchbase, COUCHBASE], - [this.DatabaseServiceTypeSmallCase.Cockroach, COCKROACH], - [this.DatabaseServiceTypeSmallCase.Greenplum, GREENPLUM], - [this.DatabaseServiceTypeSmallCase.Teradata, TERADATA], - [this.DatabaseServiceTypeSmallCase.Synapse, SYNAPSE], - [this.DatabaseServiceTypeSmallCase.BurstIQ, BURSTIQ], - [this.DatabaseServiceTypeSmallCase.Timescale, TIMESCALE], - [this.MessagingServiceTypeSmallCase.CustomMessaging, TOPIC_DEFAULT], - [this.MessagingServiceTypeSmallCase.Kafka, KAFKA], - [this.MessagingServiceTypeSmallCase.Redpanda, REDPANDA], - [this.MessagingServiceTypeSmallCase.Kinesis, KINESIS], - [this.DashboardServiceTypeSmallCase.CustomDashboard, DASHBOARD_DEFAULT], - [this.DashboardServiceTypeSmallCase.Superset, SUPERSET], - [this.DashboardServiceTypeSmallCase.Looker, LOOKER], - [this.DashboardServiceTypeSmallCase.Tableau, TABLEAU], - [this.DashboardServiceTypeSmallCase.Hex, HEX], - [this.DashboardServiceTypeSmallCase.Redash, REDASH], - [this.DashboardServiceTypeSmallCase.Metabase, METABASE], - [this.DashboardServiceTypeSmallCase.PowerBI, POWERBI], - [this.DashboardServiceTypeSmallCase.QuickSight, QUICKSIGHT], - [this.DashboardServiceTypeSmallCase.DomoDashboard, DOMO], - [this.DashboardServiceTypeSmallCase.Mode, MODE], - [this.DashboardServiceTypeSmallCase.QlikSense, QLIK_SENSE], - [this.DashboardServiceTypeSmallCase.QlikCloud, QLIK_SENSE], - [this.DashboardServiceTypeSmallCase.Lightdash, LIGHT_DASH], - [this.DashboardServiceTypeSmallCase.Sigma, SIGMA], - [this.DashboardServiceTypeSmallCase.MicroStrategy, MICROSTRATEGY], - [this.DashboardServiceTypeSmallCase.Grafana, GRAFANA], - [this.PipelineServiceTypeSmallCase.CustomPipeline, PIPELINE_DEFAULT], - [this.PipelineServiceTypeSmallCase.Airflow, AIRFLOW], - [this.PipelineServiceTypeSmallCase.Airbyte, AIRBYTE], - [this.PipelineServiceTypeSmallCase.Dagster, DAGSTER], - [this.PipelineServiceTypeSmallCase.Fivetran, FIVETRAN], - [this.PipelineServiceTypeSmallCase.DBTCloud, DBT], - [this.PipelineServiceTypeSmallCase.GluePipeline, GLUE], - [this.PipelineServiceTypeSmallCase.KafkaConnect, KAFKA], - [this.PipelineServiceTypeSmallCase.Spark, SPARK], - [this.PipelineServiceTypeSmallCase.Spline, SPLINE], - [this.PipelineServiceTypeSmallCase.Nifi, NIFI], - [this.PipelineServiceTypeSmallCase.DomoPipeline, DOMO], - [this.PipelineServiceTypeSmallCase.DatabricksPipeline, DATABRICK], - [this.PipelineServiceTypeSmallCase.OpenLineage, OPENLINEAGE], - [this.PipelineServiceTypeSmallCase.Flink, FLINK], - [this.MlModelServiceTypeSmallCase.CustomMlModel, ML_MODEL_DEFAULT], - [this.MlModelServiceTypeSmallCase.Mlflow, MLFLOW], - [this.MlModelServiceTypeSmallCase.Sklearn, SCIKIT], - [this.MlModelServiceTypeSmallCase.SageMaker, SAGEMAKER], - [this.MetadataServiceTypeSmallCase.Amundsen, AMUNDSEN], - [this.MetadataServiceTypeSmallCase.Atlas, ATLAS], - [this.MetadataServiceTypeSmallCase.AlationSink, ALATIONSINK], - [this.MetadataServiceTypeSmallCase.OpenMetadata, LOGO], - [this.StorageServiceTypeSmallCase.CustomStorage, CUSTOM_STORAGE_DEFAULT], - [this.StorageServiceTypeSmallCase.S3, AMAZON_S3], - [this.StorageServiceTypeSmallCase.Gcs, GCS], - [this.SearchServiceTypeSmallCase.CustomSearch, CUSTOM_SEARCH_DEFAULT], - [this.SearchServiceTypeSmallCase.ElasticSearch, ELASTIC_SEARCH], - [this.SearchServiceTypeSmallCase.OpenSearch, OPEN_SEARCH], - [this.ApiServiceTypeSmallCase.REST, REST_SERVICE], - [this.DriveServiceTypeSmallCase.CustomDrive, CUSTOM_DRIVE_DEFAULT], - [this.DriveServiceTypeSmallCase.GoogleDrive, GOOGLE_DRIVE], - [this.DriveServiceTypeSmallCase.SFTP, SFTP], - [this.DatabaseServiceTypeSmallCase.Iomete, IOMETE], - ]); - private getDefaultLogoForServiceType(type: string): string { const serviceTypes = this.getSupportedServiceFromList(); if (serviceTypes.messagingServices.includes(type)) { - return TOPIC_DEFAULT; + return getServiceIcon('topicdefault'); } if (serviceTypes.dashboardServices.includes(type)) { - return DASHBOARD_DEFAULT; + return getServiceIcon('dashboarddefault'); } if (serviceTypes.pipelineServices.includes(type)) { - return PIPELINE_DEFAULT; + return getServiceIcon('pipelinedefault'); } if (serviceTypes.databaseServices.includes(type)) { - return DATABASE_DEFAULT; + return getServiceIcon('databasedefault'); } if (serviceTypes.mlmodelServices.includes(type)) { - return ML_MODEL_DEFAULT; + return getServiceIcon('mlmodeldefault'); } if (serviceTypes.storageServices.includes(type)) { - return CUSTOM_STORAGE_DEFAULT; + return getServiceIcon('storagedefault'); } if (serviceTypes.searchServices.includes(type)) { - return CUSTOM_SEARCH_DEFAULT; + return getServiceIcon('searchdefault'); } if (serviceTypes.securityServices.includes(type)) { - return DEFAULT_SERVICE; + return getServiceIcon('securitydefault'); } if (serviceTypes.driveServices.includes(type)) { - return CUSTOM_DRIVE_DEFAULT; + return getServiceIcon('drivedefault'); + } + if (serviceTypes.apiServices.includes(type)) { + return getServiceIcon('restservice'); } - return DEFAULT_SERVICE; + return getServiceIcon('defaultservice'); } - public getServiceLogo(type: string): string { - const lowerType = toLower(type); - const logo = this.serviceLogoMap.get(lowerType); - - return logo ?? this.getDefaultLogoForServiceType(type); + public getServiceLogo(type: string) { + return getServiceIcon(type) ?? this.getDefaultLogoForServiceType(type); } public getServiceTypeLogo(searchSource: { diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/StorageServiceUtils.ts b/openmetadata-ui/src/main/resources/ui/src/utils/StorageServiceUtils.ts index 9ccb4e64c529..1c62f61e0ef5 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/StorageServiceUtils.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/StorageServiceUtils.ts @@ -21,7 +21,7 @@ import s3Connection from '../jsons/connectionSchemas/connections/storage/s3Conne export const getStorageConfig = (type: StorageServiceType) => { let schema = {}; const uiSchema = { ...COMMON_UI_SCHEMA }; - switch (type as unknown as StorageServiceType) { + switch (type) { case StorageServiceType.S3: { schema = s3Connection; @@ -37,6 +37,9 @@ export const getStorageConfig = (type: StorageServiceType) => { break; } + + default: + break; } return cloneDeep({ schema, uiSchema }); diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/StringsUtils.ts b/openmetadata-ui/src/main/resources/ui/src/utils/StringsUtils.ts index 20dd50021d26..3002ea1ab0b6 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/StringsUtils.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/StringsUtils.ts @@ -17,9 +17,13 @@ import { get, isString } from 'lodash'; import i18n from './i18next/LocalUtil'; export const stringToSlug = (dataString: string, slugString = '') => { - return dataString.toLowerCase().replace(/ /g, slugString); + return dataString.toLowerCase().replaceAll(' ', slugString); }; +// will add back slash "\" before quote in string if present +export const getQueryWithSlash = (query: string): string => + query.replaceAll(/["']/g, String.raw`\$&`); + /** * Convert a template string into HTML DOM nodes * Same as React.createElement(type, options, children) @@ -66,8 +70,7 @@ export const getJSONFromString = (data: string): string | null => { try { // Format string if possible and return valid JSON return JSON.parse(data); - } catch (e) { - // Invalid JSON, return null + } catch (_error) { return null; } }; @@ -87,7 +90,7 @@ export const bytesToSize = (bytes: number) => { } else if (bytes < 0) { return `N/A`; } else { - const i = parseInt( + const i = Number.parseInt( Math.floor(Math.log(bytes) / Math.log(1024)).toString(), 10 ); @@ -158,15 +161,6 @@ export const getDecodedFqn = (fqn: string, plusAsSpace = false) => { return uri; }; -/** - * - * @param url - Url to be check - * @returns - True if url is external otherwise false - */ -export const isExternalUrl = (url = '') => { - return /^https?:\/\//.test(url); -}; - /** * * @param a compare value one @@ -189,41 +183,41 @@ export const customServiceComparator = (a: string, b: string): number => { export const replacePlus = (fqn: string) => fqn.replaceAll('+', ' '); export const ES_RESERVED_CHARACTERS: Record = { - '+': '\\+', - '-': '\\-', - '=': '\\=', - '&': '\\&', - '&&': '\\&&', - '||': '\\||', - '>': '\\>', - '<': '\\<', - '!': '\\!', - '(': '\\(', - ')': '\\)', - '{': '\\{', - '}': '\\}', - '[': '\\[', - ']': '\\]', - '^': '\\^', - '"': '\\"', - '~': '\\~', - '*': '\\*', - '?': '\\?', - ':': '\\:', - '\\': '\\\\', - '/': '\\/', + '+': String.raw`\+`, + '-': String.raw`\-`, + '=': String.raw`\=`, + '&': String.raw`\&`, + '&&': String.raw`\&&`, + '||': String.raw`\||`, + '>': String.raw`\>`, + '<': String.raw`\<`, + '!': String.raw`\!`, + '(': String.raw`\(`, + ')': String.raw`\)`, + '{': String.raw`\{`, + '}': String.raw`\}`, + '[': String.raw`\[`, + ']': String.raw`\]`, + '^': String.raw`\^`, + '"': String.raw`\"`, + '~': String.raw`\~`, + '*': String.raw`\*`, + '?': String.raw`\?`, + ':': String.raw`\:`, + '\\': String.raw`\\`, + '/': String.raw`\/`, }; export const escapeESReservedCharacters = (text?: string) => { const reUnescapedHtml = /[\\[\]#+=&|> { return ES_RESERVED_CHARACTERS[char] ?? char; }; return text && reHasUnescapedHtml.test(text) - ? text.replace(reUnescapedHtml, getReplacedChar) + ? text.replaceAll(reUnescapedHtml, getReplacedChar) : text ?? ''; }; @@ -251,7 +245,7 @@ export const formatJsonString = (jsonString: string, indent = '') => { } return formattedJson; - } catch (error) { + } catch (_error) { // Return the original JSON string if parsing fails return jsonString; } @@ -276,7 +270,7 @@ export const replaceCallback = (character: string) => { * @returns A UUID string */ export const generateUUID = () => { - return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace( + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replaceAll( /[xy]/g, replaceCallback ); @@ -315,7 +309,7 @@ export const jsonToCSV = ( } const escaped = typeof value === 'string' - ? value.replace(/\\/g, '\\\\').replace(/"/g, '\\"') + ? value.replaceAll('\\', '\\\\').replaceAll('"', String.raw`\"`) : value.toString(); // handle quotes in content return `"${escaped}"`; // wrap each field in quotes @@ -350,7 +344,7 @@ export function removeAttachmentsWithoutUrl(htmlString: string): string { doc.querySelectorAll('div[data-type="file-attachment"]'); attachments.forEach((div: HTMLDivElement) => { - const url: string | null = div.getAttribute('data-url'); + const url = div.dataset.url; if (!url) { div.remove(); } diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/TableUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/TableUtils.tsx index 6aa877895381..09021e6a38a6 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/TableUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/TableUtils.tsx @@ -126,6 +126,7 @@ import { ReactComponent as TaskIcon } from '../assets/svg/task-ic.svg'; import { ReactComponent as UserIcon } from '../assets/svg/user.svg'; import { ActivityFeedTab } from '../components/ActivityFeed/ActivityFeedTab/ActivityFeedTab.component'; import { ActivityFeedLayoutType } from '../components/ActivityFeed/ActivityFeedTab/ActivityFeedTab.interface'; +import withSuspenseFallback from '../components/AppRouter/withSuspenseFallback'; import { CustomPropertyTable } from '../components/common/CustomPropertyTable/CustomPropertyTable'; import ErrorPlaceHolder from '../components/common/ErrorWithPlaceholder/ErrorPlaceHolder'; import Loader from '../components/common/Loader/Loader'; @@ -135,11 +136,7 @@ import TabsLabel from '../components/common/TabsLabel/TabsLabel.component'; import { TabProps } from '../components/common/TabsLabel/TabsLabel.interface'; import { GenericTab } from '../components/Customization/GenericTab/GenericTab'; import { CommonWidgets } from '../components/DataAssets/CommonWidgets/CommonWidgets'; -import DataObservabilityTab from '../components/Database/Profiler/DataObservability/DataObservabilityTab'; -import SampleDataTableComponent from '../components/Database/SampleDataTable/SampleDataTable.component'; import SchemaTable from '../components/Database/SchemaTable/SchemaTable.component'; -import TableQueries from '../components/Database/TableQueries/TableQueries'; -import { ContractTab } from '../components/DataContract/ContractTab/ContractTab'; import { useEntityExportModalProvider } from '../components/Entity/EntityExportModalProvider/EntityExportModalProvider.component'; import { SourceType } from '../components/SearchedData/SearchedData.interface'; import { NON_SERVICE_TYPE_ASSETS } from '../constants/Assets.constants'; @@ -182,7 +179,6 @@ import { } from '../pages/TableDetailsPageV1/FrequentlyJoinedTables/FrequentlyJoinedTables.component'; import { PartitionedKeys } from '../pages/TableDetailsPageV1/PartitionedKeys/PartitionedKeys.component'; import ConstraintIcon from '../pages/TableDetailsPageV1/TableConstraints/ConstraintIcon'; -import TableConstraints from '../pages/TableDetailsPageV1/TableConstraints/TableConstraints'; import { exportTableDetailsInCSV } from '../rest/tableAPI'; import { extractApiEndpointFields } from './APIEndpoints/APIEndpointUtils'; import { @@ -203,16 +199,54 @@ import { ordinalize } from './StringsUtils'; import { TableDetailPageTabProps } from './TableClassBase'; import { TableFieldsInfoCommonEntities } from './TableUtils.interface'; import { extractTopicFields } from './TopicDetailsUtils'; -const KnowledgeGraph = lazy( - () => import('../components/KnowledgeGraph/KnowledgeGraph') + +const SampleDataTableComponent = withSuspenseFallback( + lazy( + () => + import('../components/Database/SampleDataTable/SampleDataTable.component') + ) +); + +const TableQueries = withSuspenseFallback( + lazy(() => import('../components/Database/TableQueries/TableQueries')) +); + +const ContractTab = withSuspenseFallback( + lazy(() => + import('../components/DataContract/ContractTab/ContractTab').then( + (module) => ({ default: module.ContractTab }) + ) + ) +); + +const DataObservabilityTab = withSuspenseFallback( + lazy( + () => + import( + '../components/Database/Profiler/DataObservability/DataObservabilityTab' + ) + ) +); + +const EntityLineageTab = withSuspenseFallback( + lazy(() => + import('../components/Lineage/EntityLineageTab/EntityLineageTab').then( + (module) => ({ default: module.EntityLineageTab }) + ) + ) ); -const EntityLineageTab = lazy(() => - import('../components/Lineage/EntityLineageTab/EntityLineageTab').then( - (module) => ({ default: module.EntityLineageTab }) +const TableConstraints = withSuspenseFallback( + lazy( + () => + import('../pages/TableDetailsPageV1/TableConstraints/TableConstraints') ) ); +const KnowledgeGraph = withSuspenseFallback( + lazy(() => import('../components/KnowledgeGraph/KnowledgeGraph')) +); + export const getUsagePercentile = (pctRank: number, isLiteral = false) => { const percentile = Math.round(pctRank * 10) / 10; const ordinalPercentile = ordinalize(percentile); diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/TagsUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/TagsUtils.tsx index c30251434b49..4d98d9e34320 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/TagsUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/TagsUtils.tsx @@ -14,7 +14,6 @@ import { CheckOutlined, CloseOutlined } from '@ant-design/icons'; import { Space, Tag as AntdTag, Tooltip, Typography } from 'antd'; import { AxiosError } from 'axios'; -import i18next from 'i18next'; import { isString, omit } from 'lodash'; import { EntityTags } from 'Models'; import type { CustomTagProps } from 'rc-select/lib/BaseSelect'; @@ -52,6 +51,7 @@ import { } from '../rest/tagAPI'; import { getEntityName } from './EntityUtils'; import { getQueryFilterToIncludeApprovedTerm } from './GlossaryUtils'; +import i18n from './i18next/LocalUtil'; import { checkPermissionEntityResource } from './PermissionsUtils'; import { getClassificationTagPath, @@ -228,11 +228,11 @@ export const getUsageCountLink = (tagFQN: string) => { export const getTagPlaceholder = (isGlossaryType: boolean): string => isGlossaryType - ? i18next.t('label.search-entity', { - entity: i18next.t('label.glossary-term-plural'), + ? i18n.t('label.search-entity', { + entity: i18n.t('label.glossary-term-plural'), }) - : i18next.t('label.search-entity', { - entity: i18next.t('label.tag-plural'), + : i18n.t('label.search-entity', { + entity: i18n.t('label.tag-plural'), }); export const tagRender = (customTagProps: CustomTagProps) => { diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/TourUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/TourUtils.tsx index aab82b3ba336..60827895769c 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/TourUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/TourUtils.tsx @@ -11,10 +11,9 @@ * limitations under the License. */ -import i18next from 'i18next'; import { EntityTabs } from '../enums/entity.enum'; import { CurrentTourPageType } from '../enums/tour.enum'; -import { Transi18next } from './CommonUtils'; +import i18n, { Transi18next } from './i18next/LocalUtil'; interface ArgObject { searchTerm: string; @@ -36,7 +35,7 @@ export const getTourSteps = ({ i18nKey="message.tour-step-activity-feed" renderElement={} values={{ - text: i18next.t('label.activity-feed-plural'), + text: i18n.t('label.activity-feed-plural'), }} />

@@ -51,7 +50,7 @@ export const getTourSteps = ({ i18nKey="message.tour-step-search-for-matching-dataset" renderElement={} values={{ - text: i18next.t('label.search'), + text: i18n.t('label.search'), }} />

@@ -69,7 +68,7 @@ export const getTourSteps = ({ renderElement={} values={{ text: searchTerm, - enterText: i18next.t('label.enter'), + enterText: i18n.t('label.enter'), }} />

@@ -92,7 +91,7 @@ export const getTourSteps = ({ i18nKey="message.tour-step-explore-summary-asset" renderElement={} values={{ - text: i18next.t('label.explore'), + text: i18n.t('label.explore'), }} />

@@ -125,7 +124,7 @@ export const getTourSteps = ({ i18nKey="message.tour-high-level-assets-information-step" renderElement={} values={{ - text: i18next.t('label.schema'), + text: i18n.t('label.schema'), }} />

@@ -140,7 +139,7 @@ export const getTourSteps = ({ i18nKey="message.tour-owner-step" renderElement={} values={{ - text: i18next.t('label.schema'), + text: i18n.t('label.schema'), }} />

@@ -155,7 +154,7 @@ export const getTourSteps = ({ i18nKey="message.tour-follow-step" renderElement={} values={{ - text: i18next.t('label.schema'), + text: i18n.t('label.schema'), }} />

@@ -170,7 +169,7 @@ export const getTourSteps = ({ i18nKey="message.tour-step-get-to-know-table-schema" renderElement={} values={{ - text: i18next.t('label.schema'), + text: i18n.t('label.schema'), }} />

@@ -189,7 +188,7 @@ export const getTourSteps = ({ i18nKey="message.tour-step-click-on-entity-tab" renderElement={} values={{ - text: i18next.t('label.sample-data'), + text: i18n.t('label.sample-data'), }} />

@@ -206,7 +205,7 @@ export const getTourSteps = ({ i18nKey="message.tour-step-look-at-sample-data" renderElement={} values={{ - text: i18next.t('label.sample-data'), + text: i18n.t('label.sample-data'), }} />

@@ -227,7 +226,7 @@ export const getTourSteps = ({ i18nKey="message.tour-step-click-on-entity-tab" renderElement={} values={{ - text: i18next.t('label.data-observability'), + text: i18n.t('label.data-observability'), }} />

@@ -241,8 +240,8 @@ export const getTourSteps = ({ i18nKey="message.tour-step-discover-data-assets-with-data-profile" renderElement={} values={{ - text: i18next.t('label.data-entity', { - entity: i18next.t('label.profiler'), + text: i18n.t('label.data-entity', { + entity: i18n.t('label.profiler'), }), }} /> @@ -265,7 +264,7 @@ export const getTourSteps = ({ i18nKey="message.tour-step-click-on-entity-tab" renderElement={} values={{ - text: i18next.t('label.lineage'), + text: i18n.t('label.lineage'), }} />

@@ -279,7 +278,7 @@ export const getTourSteps = ({ i18nKey="message.tour-step-trace-path-across-tables" renderElement={} values={{ - text: i18next.t('label.lineage'), + text: i18n.t('label.lineage'), }} />

diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/UserDataUtils.ts b/openmetadata-ui/src/main/resources/ui/src/utils/UserDataUtils.ts index 543adbeaf305..4e4d6005b0fb 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/UserDataUtils.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/UserDataUtils.ts @@ -17,7 +17,6 @@ import { get, isEqual } from 'lodash'; import { OidcUser } from '../components/Auth/AuthProviders/AuthProvider.interface'; import { updateUserDetail } from '../rest/userAPI'; import { User } from './../generated/entity/teams/user'; -import { getImages } from './CommonUtils'; import i18n from './i18next/LocalUtil'; import { getImageWithResolutionAndFallback, @@ -26,6 +25,28 @@ import { import { showErrorToast } from './ToastUtils'; import userClassBase from './UserClassBase'; +export const imageTypes = { + image: 's96-c', + image192: 's192-c', + image24: 's24-c', + image32: 's32-c', + image48: 's48-c', + image512: 's512-c', + image72: 's72-c', +}; + +export const getImages = (imageUri: string) => { + const imagesObj: typeof imageTypes = imageTypes; + for (const type in imageTypes) { + imagesObj[type as keyof typeof imageTypes] = imageUri.replace( + 's96-c', + imageTypes[type as keyof typeof imageTypes] + ); + } + + return imagesObj; +}; + export const getUserDataFromOidc = ( userData: User, oidcUser: OidcUser @@ -34,10 +55,7 @@ export const getUserDataFromOidc = ( ? getImages(oidcUser.profile.picture) : undefined; const profileEmail = oidcUser.profile.email; - const email = - profileEmail && profileEmail.indexOf('@') !== -1 - ? profileEmail - : userData.email; + const email = profileEmail?.includes('@') ? profileEmail : userData.email; return { ...userData, diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/Users.util.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/Users.util.tsx index 66b5e0e436f8..fa7779f5e4ec 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/Users.util.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/Users.util.tsx @@ -27,7 +27,7 @@ import { } from '../constants/constants'; import { MASKED_EMAIL } from '../constants/User.constants'; import { EntityReference, User } from '../generated/entity/teams/user'; -import { getIsErrorMatch } from './CommonUtils'; +import { getIsErrorMatch } from './APIUtils'; import { getEntityName } from './EntityUtils'; import { t } from './i18next/LocalUtil'; import { LIST_CAP } from './PermissionsUtils'; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/WebAnalyticsUtils.ts b/openmetadata-ui/src/main/resources/ui/src/utils/WebAnalyticsUtils.ts index 3c0610832a16..e17a1ff714b7 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/WebAnalyticsUtils.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/WebAnalyticsUtils.ts @@ -28,7 +28,7 @@ import { import { PageViewEvent } from '../generated/analytics/webAnalyticEventType/pageViewEvent'; import { postWebAnalyticEvent } from '../rest/WebAnalyticsAPI'; import { AnalyticsData } from './../components/WebAnalytics/WebAnalytics.interface'; -import { getPathNameFromWindowLocation } from './RouterUtils'; +import { getPathNameFromWindowLocation } from './LocationUtils'; /** * Check if url is valid or not and return the pathname @@ -77,7 +77,8 @@ const handlePostAnalytic = async ( // collect the event data await postWebAnalyticEvent(webAnalyticEventData); } catch (error) { - // silently ignore the error + // eslint-disable-next-line no-console + console.error('Error tracking web analytic event:', error); } }; @@ -92,7 +93,7 @@ export const trackPageView = (pageData: AnalyticsData, userId?: string) => { const { payload } = pageData; - const { location, navigator, performance, document } = window; + const { location, navigator, performance, document } = globalThis; const { hostname } = location; const pageLoadTime = getPageLoadTime(performance); @@ -134,7 +135,7 @@ export const trackCustomEvent = (eventData: AnalyticsData, userId?: string) => { const { payload } = eventData; const { meta, event: eventValue } = payload; - const { location } = window; + const { location } = globalThis; // timestamp for the current event const timestamp = meta.ts; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/i18next/LocalUtil.ts b/openmetadata-ui/src/main/resources/ui/src/utils/i18next/LocalUtil.tsx similarity index 66% rename from openmetadata-ui/src/main/resources/ui/src/utils/i18next/LocalUtil.ts rename to openmetadata-ui/src/main/resources/ui/src/utils/i18next/LocalUtil.tsx index c702fa235fd2..840c6b8f54c0 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/i18next/LocalUtil.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/i18next/LocalUtil.tsx @@ -11,36 +11,26 @@ * limitations under the License. */ -import i18n, { t as i18nextT } from 'i18next'; +import i18next, { t as i18nextT } from 'i18next'; import LanguageDetector from 'i18next-browser-languagedetector'; -import { initReactI18next } from 'react-i18next'; -import { getInitOptions, languageMap } from './i18nextUtil'; -import { SupportedLocales } from './LocalUtil.interface'; +import { ReactNode } from 'react'; +import { initReactI18next, Trans } from 'react-i18next'; +import { getInitOptions } from './i18nextUtil'; +import localUtilClassBase from './LocalUtilClassBase'; -// Function to detect browser language -export const detectBrowserLanguage = (): SupportedLocales => { - const browserLang = navigator.language; - const browserLangs = navigator.languages || [browserLang]; - let browserLanguage = undefined; - for (const lang of browserLangs) { - const langCode = lang.split('-')[0]; - - if (languageMap[langCode]) { - browserLanguage = languageMap[langCode]; - - return browserLanguage; +i18next + .use(LanguageDetector) + .use(initReactI18next) + .init(getInitOptions()) + .then(async () => { + if (i18next.language !== i18next.resolvedLanguage) { + await i18next.changeLanguage(i18next.language); } - } + }); - // English is the default language when we don't support browser language - return browserLanguage ?? SupportedLocales.English; -}; - -// Initialize i18next (language) -i18n - .use(LanguageDetector) // Detects system language - .use(initReactI18next) - .init(getInitOptions()); +i18next.on('languageChanged', async (lng) => { + await localUtilClassBase.loadLocales(lng); +}); export const t = (key: string, options?: Record): string => { const translation = i18nextT(key, options); @@ -79,4 +69,19 @@ export const translateWithNestedKeys = ( return t(label, translatedParams); }; -export default i18n; +export const Transi18next = ({ + i18nKey, + values, + renderElement, + ...otherProps +}: { + i18nKey: string; + values?: object; + renderElement: ReactNode; +}): JSX.Element => ( + + {renderElement} + +); + +export default i18next; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/i18next/LocalUtilClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/i18next/LocalUtilClassBase.ts new file mode 100644 index 000000000000..a7840219811a --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/utils/i18next/LocalUtilClassBase.ts @@ -0,0 +1,76 @@ +/* + * 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 i18next from './LocalUtil'; + +const LOCALE_LOADERS: Record< + string, + () => Promise<{ default: Record }> +> = { + 'en-US': () => import('../../locale/languages/en-us.json'), + 'ko-KR': () => import('../../locale/languages/ko-kr.json'), + 'fr-FR': () => import('../../locale/languages/fr-fr.json'), + 'zh-CN': () => import('../../locale/languages/zh-cn.json'), + 'zh-TW': () => import('../../locale/languages/zh-tw.json'), + 'ja-JP': () => import('../../locale/languages/ja-jp.json'), + 'pt-BR': () => import('../../locale/languages/pt-br.json'), + 'pt-PT': () => import('../../locale/languages/pt-pt.json'), + 'es-ES': () => import('../../locale/languages/es-es.json'), + 'gl-ES': () => import('../../locale/languages/gl-es.json'), + 'ru-RU': () => import('../../locale/languages/ru-ru.json'), + 'de-DE': () => import('../../locale/languages/de-de.json'), + 'he-HE': () => import('../../locale/languages/he-he.json'), + 'nl-NL': () => import('../../locale/languages/nl-nl.json'), + 'pr-PR': () => import('../../locale/languages/pr-pr.json'), + 'th-TH': () => import('../../locale/languages/th-th.json'), + 'mr-IN': () => import('../../locale/languages/mr-in.json'), + 'tr-TR': () => import('../../locale/languages/tr-tr.json'), + 'ar-SA': () => import('../../locale/languages/ar-sa.json'), +}; + +class LocalUtilClassBase { + private static _instance: LocalUtilClassBase; + + async loadLocales(locale: string): Promise { + if (i18next.hasResourceBundle(locale, 'translation')) { + return; + } + + const loader = LOCALE_LOADERS[locale]; + if (!loader) { + return; + } + + const translations = await loader(); + i18next.addResourceBundle( + locale, + 'translation', + translations.default, + true + ); + } + + static getInstance(): LocalUtilClassBase { + if (!LocalUtilClassBase._instance) { + LocalUtilClassBase._instance = new LocalUtilClassBase(); + } + + return LocalUtilClassBase._instance; + } +} + +const localUtilClassBase = LocalUtilClassBase.getInstance(); + +export { LocalUtilClassBase }; + +export default localUtilClassBase; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/i18next/i18nextUtil.ts b/openmetadata-ui/src/main/resources/ui/src/utils/i18next/i18nextUtil.ts index 7d6d527ee82c..b866263496d4 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/i18next/i18nextUtil.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/i18next/i18nextUtil.ts @@ -13,25 +13,7 @@ import i18next, { InitOptions } from 'i18next'; import { map, upperCase } from 'lodash'; -import arSA from '../../locale/languages/ar-sa.json'; -import deDe from '../../locale/languages/de-de.json'; import enUS from '../../locale/languages/en-us.json'; -import esES from '../../locale/languages/es-es.json'; -import frFR from '../../locale/languages/fr-fr.json'; -import glES from '../../locale/languages/gl-es.json'; -import heHE from '../../locale/languages/he-he.json'; -import jaJP from '../../locale/languages/ja-jp.json'; -import koKR from '../../locale/languages/ko-kr.json'; -import mrIN from '../../locale/languages/mr-in.json'; -import nlNL from '../../locale/languages/nl-nl.json'; -import prPR from '../../locale/languages/pr-pr.json'; -import ptBR from '../../locale/languages/pt-br.json'; -import ptPT from '../../locale/languages/pt-pt.json'; -import ruRU from '../../locale/languages/ru-ru.json'; -import thTH from '../../locale/languages/th-th.json'; -import trTR from '../../locale/languages/tr-tr.json'; -import zhCN from '../../locale/languages/zh-cn.json'; -import zhTW from '../../locale/languages/zh-tw.json'; import { SupportedLocales } from './LocalUtil.interface'; export const languageSelectOptions = map(SupportedLocales, (value, key) => ({ @@ -45,24 +27,6 @@ export const getInitOptions = (): InitOptions => { supportedLngs: Object.values(SupportedLocales), resources: { 'en-US': { translation: enUS }, - 'ko-KR': { translation: koKR }, - 'fr-FR': { translation: frFR }, - 'zh-CN': { translation: zhCN }, - 'zh-TW': { translation: zhTW }, - 'ja-JP': { translation: jaJP }, - 'pt-BR': { translation: ptBR }, - 'pt-PT': { translation: ptPT }, - 'es-ES': { translation: esES }, - 'gl-ES': { translation: glES }, - 'ru-RU': { translation: ruRU }, - 'de-DE': { translation: deDe }, - 'he-HE': { translation: heHE }, - 'nl-NL': { translation: nlNL }, - 'pr-PR': { translation: prPR }, - 'th-TH': { translation: thTH }, - 'mr-IN': { translation: mrIN }, - 'tr-TR': { translation: trTR }, - 'ar-SA': { translation: arSA }, }, fallbackLng: ['en-US'], detection: { @@ -70,12 +34,12 @@ export const getInitOptions = (): InitOptions => { caches: ['cookie'], // cache user language on }, interpolation: { - escapeValue: false, // XSS safety provided by React + escapeValue: false, }, missingKeyHandler: (_lngs, _ns, key) => // eslint-disable-next-line no-console console.error(`i18next: key not found "${key}"`), - saveMissing: true, // Required for missing key handler + saveMissing: true, }; }; diff --git a/openmetadata-ui/src/main/resources/ui/vite.config.ts b/openmetadata-ui/src/main/resources/ui/vite.config.ts index 9a02c03e3c6a..3052f0d6b65c 100644 --- a/openmetadata-ui/src/main/resources/ui/vite.config.ts +++ b/openmetadata-ui/src/main/resources/ui/vite.config.ts @@ -38,19 +38,19 @@ export default defineConfig(({ mode }) => { // Don't replace ${basePath} placeholder - it will be replaced at runtime by Java backend // Add ${basePath} prefix to asset paths (with or without leading slash) return html - .replace( + .replaceAll( /(]*src=["'])(\.\/)?assets\//g, '$1${basePath}assets/' ) - .replace( + .replaceAll( /(]*href=["'])(\.\/)?assets\//g, '$1${basePath}assets/' ) - .replace( + .replaceAll( /(]*src=["'])(\.\/)?assets\//g, '$1${basePath}assets/' ) - .replace( + .replaceAll( /(]*src=["'])(\.\/)?images\//g, '$1${basePath}images/' ); @@ -73,6 +73,9 @@ export default defineConfig(({ mode }) => { ext: '.gz', threshold: 1024, // Only compress files larger than 1KB deleteOriginFile: false, // Keep original files for fallback + // Skip binary formats that are already compressed — re-compressing + // them wastes build CPU and saves zero bytes. + filter: /\.(js|mjs|css|html|svg|json|wasm)(\?.*)?$/i, }), mode === 'production' && viteCompression({ @@ -80,6 +83,8 @@ export default defineConfig(({ mode }) => { ext: '.br', threshold: 1024, // Only compress files larger than 1KB deleteOriginFile: false, // Keep original files for fallback + // Same exclusion list — woff2 is already brotli-compressed internally. + filter: /\.(js|mjs|css|html|svg|json|wasm)(\?.*)?$/i, }), ].filter(Boolean), @@ -169,30 +174,33 @@ export default defineConfig(({ mode }) => { cssMinify: 'esbuild', cssCodeSplit: true, reportCompressedSize: false, - chunkSizeWarningLimit: 5000, + chunkSizeWarningLimit: 1500, rollupOptions: { output: { - manualChunks: { - 'react-vendor': ['react', 'react-dom', 'react-router-dom'], - 'antd-vendor': ['antd', '@ant-design/icons'], - 'editor-vendor': [ - '@tiptap/react', - '@tiptap/starter-kit', - '@tiptap/extension-link', - ], - 'chart-vendor': ['recharts', 'reactflow'], - }, assetFileNames: (assetInfo) => { - const fileName = assetInfo.name || ''; - const info = fileName.split('.'); - const ext = info[info.length - 1]; + const names = assetInfo.names ?? []; + const fileName = names.length > 0 ? names[0] : ''; + const ext = fileName ? path.extname(fileName).toLowerCase() : ''; - if (/png|jpe?g|svg|gif|tiff|bmp|ico/i.test(ext)) { + if (/\.(png|jpe?g|svg|gif|tiff|bmp|ico)$/i.test(ext)) { return `images/[name]-[hash][extname]`; } return `assets/[name]-[hash][extname]`; }, + manualChunks: (id) => { + if (id.includes('node_modules')) { + if (id.includes('antd')) { + return 'vendor-antd'; + } + if (id.includes('@openmetadata/ui-core-components')) { + return 'vendor-untitled'; + } + if (id.includes('@untitledui/icons')) { + return 'vendor-untitled-icons'; + } + } + }, }, }, },