From 1564b308be33c1e73bff622a06d889fe2a6bc407 Mon Sep 17 00:00:00 2001 From: Karan Hotchandani <33024356+karanh37@users.noreply.github.com> Date: Mon, 11 Aug 2025 11:55:06 +0530 Subject: [PATCH] App widgets oss (#22743) * update lineage provider * add plugin registry for applications * fix tests * add documentation for plugin --- .../AppRouter/AuthenticatedAppRouter.tsx | 8 ++ .../LeftSidebar/LeftSidebar.component.tsx | 57 +++++++++----- .../MyData/LeftSidebar/LeftSidebar.test.tsx | 7 ++ .../AppDetails/AppDetails.component.tsx | 21 ++++- .../AppDetails/AppDetails.test.tsx | 4 + .../AppDetails/ApplicationsClassBase.ts | 6 ++ .../ApplicationsProvider.interface.ts | 2 + .../ApplicationsProvider.tsx | 22 +++++- .../Applications/plugins/AppPlugin.ts | 66 ++++++++++++++++ .../LineageProvider.interface.tsx | 2 + .../LineageProvider/LineageProvider.tsx | 76 +++++++++++++------ 11 files changed, 224 insertions(+), 47 deletions(-) create mode 100644 openmetadata-ui/src/main/resources/ui/src/components/Settings/Applications/plugins/AppPlugin.ts 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 891fd58e1db..b9b1a99317e 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 @@ -28,6 +28,7 @@ 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 AdminProtectedRoute from './AdminProtectedRoute'; import withSuspenseFallback from './withSuspenseFallback'; @@ -278,7 +279,9 @@ const AddMetricPage = withSuspenseFallback( const AuthenticatedAppRouter: FunctionComponent = () => { const { permissions } = usePermissionProvider(); const { t } = useTranslation(); + const { plugins } = useApplicationsProvider(); + const pluginRoutes = plugins.flatMap((plugin) => plugin.getRoutes?.() || []); const createBotPermission = useMemo( () => checkPermission(Operation.Create, ResourceEntity.USER, permissions) && @@ -686,6 +689,11 @@ const AuthenticatedAppRouter: FunctionComponent = () => { path={ROUTES.EDIT_KPI} /> + {/* Plugin routes */} + {pluginRoutes.map((route, idx) => { + return ; + })} + } path={ROUTES.HOME} /> { [handleLogoutClick] ); + const { plugins } = useApplicationsProvider(); + + const pluginSidebarActions = useMemo(() => { + return plugins + .flatMap((plugin) => plugin.getSidebarActions?.() ?? []) + .sort((a, b) => (a.index ?? 999) - (b.index ?? 999)); + }, [plugins]); + const menuItems = useMemo(() => { - return [ - ...sideBarItems.map((item) => { - return { - key: item.key, - icon: , - label: , - children: item.children?.map((item: LeftSidebarItemType) => { - return { - key: item.key, - icon: , - label: , - }; - }), - }; - }), - ]; - }, [sideBarItems]); + const mergedItems = (() => { + const baseItems = [...sideBarItems]; + + pluginSidebarActions.forEach((pluginItem) => { + if (typeof pluginItem.index === 'number' && pluginItem.index >= 0) { + baseItems.splice( + Math.min(pluginItem.index, baseItems.length), + 0, + pluginItem + ); + } else { + baseItems.push(pluginItem); + } + }); + + return baseItems; + })(); + + // Map to menu structure + return mergedItems.map((item) => ({ + key: item.key, + icon: , + label: , + children: item.children?.map((child) => ({ + key: child.key, + icon: , + label: , + })), + })); + }, [sideBarItems, pluginSidebarActions]); const handleMenuClick: MenuProps['onClick'] = useCallback(() => { setOpenKeys([]); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/MyData/LeftSidebar/LeftSidebar.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/MyData/LeftSidebar/LeftSidebar.test.tsx index 6c35e78771f..786e1daa143 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/MyData/LeftSidebar/LeftSidebar.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/MyData/LeftSidebar/LeftSidebar.test.tsx @@ -14,6 +14,13 @@ import { render, screen } from '@testing-library/react'; import { BrowserRouter } from 'react-router-dom'; import LeftSidebar from './LeftSidebar.component'; +jest.mock( + '../../Settings/Applications/ApplicationsProvider/ApplicationsProvider', + () => ({ + useApplicationsProvider: () => ({ applications: [], plugins: [] }), + }) +); + describe('LeftSidebar', () => { it('renders sidebar links correctly', () => { render( diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Applications/AppDetails/AppDetails.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Applications/AppDetails/AppDetails.component.tsx index 0bc2109fbba..aa35e1f600c 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Applications/AppDetails/AppDetails.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Applications/AppDetails/AppDetails.component.tsx @@ -33,7 +33,7 @@ import { ItemType } from 'antd/lib/menu/hooks/useItems'; import { AxiosError } from 'axios'; import { compare } from 'fast-json-patch'; import { isEmpty } from 'lodash'; -import { useCallback, useEffect, useMemo, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; import { ReactComponent as IconExternalLink } from '../../../../assets/svg/external-links.svg'; @@ -71,6 +71,7 @@ import TabsLabel from '../../../common/TabsLabel/TabsLabel.component'; import ConfirmationModal from '../../../Modals/ConfirmationModal/ConfirmationModal'; import PageLayoutV1 from '../../../PageLayoutV1/PageLayoutV1'; import ApplicationConfiguration from '../ApplicationConfiguration/ApplicationConfiguration'; +import { useApplicationsProvider } from '../ApplicationsProvider/ApplicationsProvider'; import AppLogo from '../AppLogo/AppLogo.component'; import AppRunsHistory from '../AppRunsHistory/AppRunsHistory.component'; import AppSchedule from '../AppSchedule/AppSchedule.component'; @@ -95,6 +96,7 @@ const AppDetails = () => { isSaveLoading: false, }); const { getResourceLimit } = useLimitStore(); + const { plugins } = useApplicationsProvider(); const fetchAppDetails = useCallback(async () => { setLoadingState((prev) => ({ ...prev, isFetchLoading: true })); @@ -323,6 +325,17 @@ const AppDetails = () => { } }; + // Check if there's a plugin configuration component for this app + const pluginConfigComponent = useMemo(() => { + if (!appData?.name || !plugins.length) { + return null; + } + + const plugin = plugins.find((p) => p.name === appData.name); + + return plugin?.getConfigComponent?.(appData) || null; + }, [appData?.name, plugins]); + const tabs = useMemo(() => { const tabConfiguration = appData?.appConfiguration && appData.allowConfiguration && jsonSchema @@ -335,7 +348,11 @@ const AppDetails = () => { /> ), key: ApplicationTabs.CONFIGURATION, - children: ( + children: pluginConfigComponent ? ( + // Use plugin configuration component if available + React.createElement(pluginConfigComponent) + ) : ( + // Fall back to default ApplicationConfiguration ({ useFqn: jest.fn().mockReturnValue({ fqn: 'mockFQN' }), })); +jest.mock('../ApplicationsProvider/ApplicationsProvider', () => ({ + useApplicationsProvider: () => ({ applications: [], plugins: [] }), +})); + const mockConfigureApp = jest.fn(); const mockDeployApp = jest.fn(); const mockRestoreApp = jest.fn(); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Applications/AppDetails/ApplicationsClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Applications/AppDetails/ApplicationsClassBase.ts index 123bdf2aa4a..1f18ee6fa98 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Applications/AppDetails/ApplicationsClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Applications/AppDetails/ApplicationsClassBase.ts @@ -14,6 +14,7 @@ import { FC } from 'react'; import { AppType } from '../../../../generated/entity/applications/app'; import { getScheduleOptionsFromSchedules } from '../../../../utils/SchedularUtils'; +import { AppPlugin } from '../plugins/AppPlugin'; class ApplicationsClassBase { public importSchema(fqn: string) { @@ -56,6 +57,11 @@ class ApplicationsClassBase { return import(`../../../../assets/img/appScreenshots/${screenshotName}`); } + public appPluginRegistry: Record< + string, + new (name: string, isInstalled: boolean) => AppPlugin + > = {}; + public getScheduleOptionsForApp( app: string, appType: AppType, diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Applications/ApplicationsProvider/ApplicationsProvider.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Applications/ApplicationsProvider/ApplicationsProvider.interface.ts index b758d9cc6e8..05b6bfe3be2 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Applications/ApplicationsProvider/ApplicationsProvider.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Applications/ApplicationsProvider/ApplicationsProvider.interface.ts @@ -11,7 +11,9 @@ * limitations under the License. */ import { EntityReference } from '../../../../generated/entity/type'; +import type { AppPlugin } from '../plugins/AppPlugin'; export type ApplicationsContextType = { applications: EntityReference[]; + plugins: AppPlugin[]; }; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Applications/ApplicationsProvider/ApplicationsProvider.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Applications/ApplicationsProvider/ApplicationsProvider.tsx index 3f4596b7351..dbbfdfebac4 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Applications/ApplicationsProvider/ApplicationsProvider.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Applications/ApplicationsProvider/ApplicationsProvider.tsx @@ -25,6 +25,8 @@ import { EntityReference } from '../../../../generated/entity/type'; import { useApplicationStore } from '../../../../hooks/useApplicationStore'; import { getInstalledApplicationList } from '../../../../rest/applicationAPI'; import Loader from '../../../common/Loader/Loader'; +import applicationsClassBase from '../AppDetails/ApplicationsClassBase'; +import type { AppPlugin } from '../plugins/AppPlugin'; import { ApplicationsContextType } from './ApplicationsProvider.interface'; export const ApplicationsContext = createContext({} as ApplicationsContextType); @@ -45,7 +47,7 @@ export const ApplicationsProvider = ({ children }: { children: ReactNode }) => { (app) => app.name ?? app.fullyQualifiedName ?? '' ); setApplicationsName(applicationsNameList); - } catch (err) { + } catch { // do not handle error } finally { setLoading(false); @@ -60,10 +62,24 @@ export const ApplicationsProvider = ({ children }: { children: ReactNode }) => { } }, []); - const appContext = useMemo(() => { - return { applications }; + const installedPluginInstances: AppPlugin[] = useMemo(() => { + return applications + .map((app) => { + if (!app.name) { + return null; + } + + const PluginClass = applicationsClassBase.appPluginRegistry[app.name]; + + return PluginClass ? new PluginClass(app.name, true) : null; + }) + .filter(Boolean) as AppPlugin[]; }, [applications]); + const appContext = useMemo(() => { + return { applications, plugins: installedPluginInstances }; + }, [applications, installedPluginInstances]); + return ( {loading ? : children} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Applications/plugins/AppPlugin.ts b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Applications/plugins/AppPlugin.ts new file mode 100644 index 00000000000..00b847a9f99 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Applications/plugins/AppPlugin.ts @@ -0,0 +1,66 @@ +/* + * 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 { FC } from 'react'; +import { RouteProps } from 'react-router-dom'; +import { App } from '../../../../generated/entity/applications/app'; +import { LeftSidebarItem } from '../../../MyData/LeftSidebar/LeftSidebar.interface'; + +export interface LeftSidebarItemExample extends LeftSidebarItem { + index: number; +} + +/** + * Interface defining the structure and capabilities of an application plugin. + * + * This interface allows plugins to extend the OpenMetadata application with + * custom components, routes, and sidebar actions. Plugins can be installed + * or uninstalled dynamically and provide modular functionality. + */ +export interface AppPlugin { + /** + * The unique name of the app received from the /apps endpoint. + */ + name: string; + + /** + * Indicates whether the app is currently installed and active. + * Used to determine plugin availability and UI state. + */ + isInstalled: boolean; + + /** + * Optional method that returns a React component for plugin configuration. + * It is the responsibility of this component to update application data using patchApplication API + * + * @param app - The App entity containing application details and configuration + * @returns A React functional component for plugin settings/configuration, + * or null if no configuration is needed. + */ + getConfigComponent?(app: App): FC | null; + + /** + * Optional method that provides custom routes for the plugin. + * + * @returns An array of route properties that define the plugin's + * navigation structure and page routing. + */ + getRoutes?(): Array; + + /** + * Optional method that provides custom sidebar actions for the plugin. + * + * @returns An array of sidebar items that will be displayed in the + * left sidebar when the plugin is active. + */ + getSidebarActions?(): Array; +} diff --git a/openmetadata-ui/src/main/resources/ui/src/context/LineageProvider/LineageProvider.interface.tsx b/openmetadata-ui/src/main/resources/ui/src/context/LineageProvider/LineageProvider.interface.tsx index 3e1755024c2..1c0d9454551 100644 --- a/openmetadata-ui/src/main/resources/ui/src/context/LineageProvider/LineageProvider.interface.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/context/LineageProvider/LineageProvider.interface.tsx @@ -61,6 +61,7 @@ export interface LineageContextType { platformView: LineagePlatformView; expandAllColumns: boolean; isPlatformLineage: boolean; + entityFqn: string; toggleColumnView: () => void; onInitReactFlow: (reactFlowInstance: ReactFlowInstance) => void; onPaneClick: () => void; @@ -98,5 +99,6 @@ export interface LineageContextType { ) => void; onUpdateLayerView: (layers: LineageLayer[]) => void; redraw: () => Promise; + updateEntityFqn: (entityFqn: string) => void; dqHighlightedEdges?: Set; } diff --git a/openmetadata-ui/src/main/resources/ui/src/context/LineageProvider/LineageProvider.tsx b/openmetadata-ui/src/main/resources/ui/src/context/LineageProvider/LineageProvider.tsx index 61a6a83e8b5..ffe3fa6d28d 100644 --- a/openmetadata-ui/src/main/resources/ui/src/context/LineageProvider/LineageProvider.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/context/LineageProvider/LineageProvider.tsx @@ -217,6 +217,21 @@ const LineageProvider = ({ children }: LineageProviderProps) => { const [isPlatformLineage, setIsPlatformLineage] = useState(false); const [dqHighlightedEdges, setDqHighlightedEdges] = useState>(); + // Add state for entityFqn that can be updated independently of URL params + const [entityFqn, setEntityFqn] = useState(decodedFqn); + + // Update entityFqn when decodedFqn changes (for backward compatibility) + useEffect(() => { + if (decodedFqn) { + setEntityFqn(decodedFqn); + } + }, [decodedFqn]); + + // Function to update entityFqn + const updateEntityFqn = useCallback((fqn: string) => { + setEntityFqn(fqn); + }, []); + const lineageLayer = useMemo(() => { const param = location.search; const searchData = QueryString.parse( @@ -276,7 +291,7 @@ const LineageProvider = ({ children }: LineageProviderProps) => { } = createEdgesAndEdgeMaps( allNodes, lineageData.edges ?? [], - decodedFqn, + entityFqn, activeLayer.includes(LineageLayer.ColumnLevelLineage), isFirstTime ? true : undefined ); @@ -284,7 +299,7 @@ const LineageProvider = ({ children }: LineageProviderProps) => { const initialNodes = createNodes( allNodes, lineageData.edges ?? [], - decodedFqn, + entityFqn, incomingMap, outgoingMap, activeLayer.includes(LineageLayer.ColumnLevelLineage), @@ -334,7 +349,7 @@ const LineageProvider = ({ children }: LineageProviderProps) => { setColumnsHavingLineage(columnsHavingLineage); }, [ - decodedFqn, + entityFqn, activeLayer, isEditMode, reactFlowInstance, @@ -379,7 +394,7 @@ const LineageProvider = ({ children }: LineageProviderProps) => { setLineageData(res); - const { nodes, edges, entity } = parseLineageData(res, '', decodedFqn); + const { nodes, edges, entity } = parseLineageData(res, '', entityFqn); const updatedEntityLineage = { nodes, edges, @@ -399,7 +414,7 @@ const LineageProvider = ({ children }: LineageProviderProps) => { setLoading(false); } }, - [] + [entityFqn] ); const fetchLineageData = useCallback( @@ -423,7 +438,7 @@ const LineageProvider = ({ children }: LineageProviderProps) => { }); setLineageData(res); - const { nodes, edges, entity } = parseLineageData(res, fqn, decodedFqn); + const { nodes, edges, entity } = parseLineageData(res, fqn, entityFqn); const updatedEntityLineage = { nodes, edges, @@ -443,7 +458,7 @@ const LineageProvider = ({ children }: LineageProviderProps) => { setLoading(false); } }, - [queryFilter, decodedFqn] + [queryFilter, entityFqn] ); const onPlatformViewChange = useCallback( @@ -473,17 +488,17 @@ const LineageProvider = ({ children }: LineageProviderProps) => { const exportLineageData = useCallback( async (_: string) => { return exportLineageAsync( - decodedFqn, + entityFqn, entityType, lineageConfig, queryFilter ); }, - [entityType, decodedFqn, lineageConfig, queryFilter] + [entityType, entityFqn, lineageConfig, queryFilter] ); const onExportClick = useCallback(() => { - if (decodedFqn || isPlatformLineagePage) { + if (entityFqn || isPlatformLineagePage) { showModal({ ...(isPlatformLineagePage ? { @@ -491,7 +506,7 @@ const LineageProvider = ({ children }: LineageProviderProps) => { exportTypes: [ExportTypes.PNG], } : { - name: decodedFqn, + name: entityFqn, exportTypes: [ExportTypes.CSV, ExportTypes.PNG], }), title: t('label.lineage'), @@ -502,7 +517,7 @@ const LineageProvider = ({ children }: LineageProviderProps) => { } }, [ entityType, - decodedFqn, + entityFqn, lineageConfig, queryFilter, nodes, @@ -552,7 +567,7 @@ const LineageProvider = ({ children }: LineageProviderProps) => { const { nodes: newNodes, edges: newEdges } = parseLineageData( concatenatedLineageData, node.fullyQualifiedName ?? '', - decodedFqn + entityFqn ); const uniqueNodes = [...(entityLineage.nodes ?? [])]; @@ -625,7 +640,15 @@ const LineageProvider = ({ children }: LineageProviderProps) => { ); } }, - [nodes, edges, lineageConfig, entityLineage, setEntityLineage, queryFilter] + [ + nodes, + edges, + lineageConfig, + entityLineage, + setEntityLineage, + queryFilter, + entityFqn, + ] ); const handleLineageTracing = useCallback( @@ -686,6 +709,7 @@ const LineageProvider = ({ children }: LineageProviderProps) => { isPlatformLineage?: boolean ) => { setEntity(entity); + setEntityFqn(entity?.fullyQualifiedName ?? ''); setEntityType(entityType); setIsPlatformLineage(isPlatformLineage ?? false); if (isPlatformLineage && !entity) { @@ -958,7 +982,7 @@ const LineageProvider = ({ children }: LineageProviderProps) => { const { nodes: newNodes, edges: newEdges } = parseLineageData( concatenatedLineageData, parentNode.data.node.fullyQualifiedName, - decodedFqn + entityFqn ); updateLineageData( @@ -1174,7 +1198,7 @@ const LineageProvider = ({ children }: LineageProviderProps) => { createEdgesAndEdgeMaps( allNodes, allEdges, - decodedFqn, + entityFqn, activeLayer.includes(LineageLayer.ColumnLevelLineage) ); setEdges(createdEdges); @@ -1191,7 +1215,7 @@ const LineageProvider = ({ children }: LineageProviderProps) => { }); } }, - [entityLineage, nodes, decodedFqn] + [entityLineage, nodes, entityFqn] ); const onAddPipelineClick = useCallback(() => { @@ -1499,7 +1523,7 @@ const LineageProvider = ({ children }: LineageProviderProps) => { }, [entityLineage, redrawLineage]); const onPlatformViewUpdate = useCallback(() => { - if (entity && decodedFqn && entityType) { + if (entity && entityFqn && entityType) { if (platformView === LineagePlatformView.Service && entity?.service) { fetchLineageData( entity?.service.fullyQualifiedName ?? '', @@ -1525,7 +1549,7 @@ const LineageProvider = ({ children }: LineageProviderProps) => { lineageConfig ); } else if (platformView === LineagePlatformView.None) { - fetchLineageData(decodedFqn, entityType, lineageConfig); + fetchLineageData(entityFqn, entityType, lineageConfig); } else if (isPlatformLineage) { fetchPlatformLineage( getEntityTypeFromPlatformView(platformView), @@ -1541,7 +1565,7 @@ const LineageProvider = ({ children }: LineageProviderProps) => { }, [ entity, entityType, - decodedFqn, + entityFqn, lineageConfig, platformView, queryFilter, @@ -1571,7 +1595,7 @@ const LineageProvider = ({ children }: LineageProviderProps) => { getUpstreamDownstreamNodesEdges( updatedEntityLineage.edges ?? [], updatedEntityLineage.nodes ?? [], - decodedFqn + entityFqn ); const updatedNodes = @@ -1588,7 +1612,7 @@ const LineageProvider = ({ children }: LineageProviderProps) => { nodes: updatedNodes as EntityReference[], }); } - }, [isEditMode, updatedEntityLineage, decodedFqn]); + }, [isEditMode, updatedEntityLineage, entityFqn]); useEffect(() => { if (isEditMode) { @@ -1658,6 +1682,8 @@ const LineageProvider = ({ children }: LineageProviderProps) => { expandAllColumns, platformView, isPlatformLineage, + entityFqn, + updateEntityFqn, toggleColumnView, onInitReactFlow, onPaneClick, @@ -1708,6 +1734,8 @@ const LineageProvider = ({ children }: LineageProviderProps) => { columnsHavingLineage, expandAllColumns, isPlatformLineage, + entityFqn, + updateEntityFqn, toggleColumnView, onInitReactFlow, onPaneClick, @@ -1755,9 +1783,9 @@ const LineageProvider = ({ children }: LineageProviderProps) => { useEffect(() => { if (activeLayer.includes(LineageLayer.DataObservability)) { - fetchDataQualityLineage(decodedFqn, lineageConfig); + fetchDataQualityLineage(entityFqn, lineageConfig); } - }, [activeLayer, decodedFqn, lineageConfig]); + }, [activeLayer, entityFqn, lineageConfig]); useEffect(() => { if (