From c57f7bb14ae8f3dfd4c8c180dc26a29fec52bc61 Mon Sep 17 00:00:00 2001 From: Manav Sharma <123449950+manavmax@users.noreply.github.com> Date: Sat, 25 Apr 2026 02:56:01 +0530 Subject: [PATCH 01/11] Fixes 27726: match domain colors on assigned assets --- .../DomainLabelV2/DomainLabelV2.tsx | 32 +++- .../DataProduct/DataProductListPage.tsx | 12 +- .../DomainDisplay/DomainDisplay.component.tsx | 42 ++++- .../DomainDisplay/DomainDisplay.test.tsx | 78 ++++++++- .../DomainLabel/DomainLabel.component.tsx | 38 ++++- .../common/DomainLabel/DomainLabel.test.tsx | 67 +++++++- .../common/atoms/table/useCellRenderer.tsx | 21 +-- .../ui/src/utils/DomainStyleUtils.tsx | 156 ++++++++++++++++++ 8 files changed, 383 insertions(+), 63 deletions(-) create mode 100644 openmetadata-ui/src/main/resources/ui/src/utils/DomainStyleUtils.tsx diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/DomainLabelV2/DomainLabelV2.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/DomainLabelV2/DomainLabelV2.tsx index 78b7ac5a061c..232f84bca5bb 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/DomainLabelV2/DomainLabelV2.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/DomainLabelV2/DomainLabelV2.tsx @@ -25,6 +25,11 @@ import { getAPIfromSource, getEntityAPIfromSource, } from '../../../utils/Assets/AssetsUtils'; +import { + getDomainReferenceBadgeStyle, + getDomainReferenceIconColor, + useDomainsWithStyle, +} from '../../../utils/DomainStyleUtils'; import { renderDomainLink } from '../../../utils/DomainUtils'; import { getEntityName } from '../../../utils/EntityUtils'; import { showErrorToast } from '../../../utils/ToastUtils'; @@ -49,6 +54,7 @@ export const DomainLabelV2 = < const { id: entityId, fullyQualifiedName: entityFqn, domains } = data; const { t } = useTranslation(); const [activeDomain, setActiveDomain] = useState([]); + const resolvedDomains = useDomainsWithStyle(activeDomain); const handleDomainSave = useCallback( async (selectedDomain: EntityReference | EntityReference[]) => { @@ -111,16 +117,18 @@ export const DomainLabelV2 = < } else { setActiveDomain([domains]); } + } else { + setActiveDomain([]); } }, [domains]); const domainLink = useMemo(() => { if ( - activeDomain && - Array.isArray(activeDomain) && - activeDomain.length > 0 + resolvedDomains && + Array.isArray(resolvedDomains) && + resolvedDomains.length > 0 ) { - return activeDomain.map((domain) => { + return resolvedDomains.map((domain, index) => { const inheritedIcon = domain?.inherited ? ( +
); } - }, [activeDomain]); + }, [resolvedDomains, props.showDomainHeading, props.textClassName, t]); const hasPermission = useMemo(() => { return props?.hasPermission ?? (permissions?.EditAll && !data?.deleted); @@ -214,7 +230,7 @@ export const DomainLabelV2 = < {selectableList} ); - }, [activeDomain, hasPermission, selectableList]); + }, [domainLink, props.showDomainHeading, selectableList, t]); return label; }; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataProduct/DataProductListPage.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataProduct/DataProductListPage.tsx index c7165a3527ed..4eef9e2a5a08 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataProduct/DataProductListPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataProduct/DataProductListPage.tsx @@ -17,7 +17,6 @@ import { Card, Typography, } from '@openmetadata/ui-core-components'; -import { Globe01 } from '@untitledui/icons'; import { useForm } from 'antd/lib/form/Form'; import { isEmpty } from 'lodash'; import { useSnackbar } from 'notistack'; @@ -54,6 +53,7 @@ import { hasActiveSearchOrFilter } from '../common/atoms/shared/utils/hasActiveS import EntityCardView from '../common/EntityCardView/EntityCardView.component'; import EntityListingTable from '../common/EntityListingTable/EntityListingTable.component'; import { ColumnDef } from '../common/EntityListingTable/EntityListingTable.interface'; +import { DomainDisplay } from '../common/DomainDisplay/DomainDisplay.component'; import ErrorPlaceHolder from '../common/ErrorWithPlaceholder/ErrorPlaceHolder'; import { OwnerLabel } from '../common/OwnerLabel/OwnerLabel.component'; import TagBadgeList from '../common/TagBadgeList/TagBadgeList.component'; @@ -217,16 +217,8 @@ const DataProductListPage = () => { if (!domains?.length) { return {NO_DATA}; } - const domain = domains[0]; - return ( - - - - {domain.displayName || domain.name} - - - ); + return ; } case 'tags': return ( diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/DomainDisplay/DomainDisplay.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/DomainDisplay/DomainDisplay.component.tsx index f1a464dcb7e4..177ac3d6c084 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/DomainDisplay/DomainDisplay.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/DomainDisplay/DomainDisplay.component.tsx @@ -13,7 +13,13 @@ import { Dropdown, Typography } from 'antd'; import { Link } from 'react-router-dom'; import { ReactComponent as DomainIcon } from '../../../assets/svg/ic-domain.svg'; +import { DE_ACTIVE_COLOR } from '../../../constants/constants'; import { EntityReference } from '../../../generated/entity/type'; +import { + getDomainReferenceBadgeStyle, + getDomainReferenceIconColor, + useDomainsWithStyle, +} from '../../../utils/DomainStyleUtils'; import { getEntityName } from '../../../utils/EntityUtils'; import { getDomainPath } from '../../../utils/RouterUtils'; @@ -43,20 +49,29 @@ export const DomainDisplay = ({ showIcon = true, className = '', }: DomainDisplayProps) => { - if (!domains || domains.length === 0) { + const resolvedDomains = useDomainsWithStyle(domains); + + if (!resolvedDomains || resolvedDomains.length === 0) { return null; } - if (domains.length > 1) { - const firstDomain = domains[0]; - const remainingDomains = domains.slice(1); + if (resolvedDomains.length > 1) { + const firstDomain = resolvedDomains[0]; + const remainingDomains = resolvedDomains.slice(1); const remainingCount = remainingDomains.length; const dropdownItems = remainingDomains.map((domain, index) => ({ key: index, label: ( -
- +
+ @@ -73,6 +88,7 @@ export const DomainDisplay = ({ {showIcon && (
)} -
+
)} -
- +
+
); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/DomainDisplay/DomainDisplay.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/DomainDisplay/DomainDisplay.test.tsx index 78987b4a2029..969640f7ac1e 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/DomainDisplay/DomainDisplay.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/DomainDisplay/DomainDisplay.test.tsx @@ -11,9 +11,12 @@ * limitations under the License. */ import '@testing-library/jest-dom'; -import { render, screen } from '@testing-library/react'; +import { render, screen, waitFor } from '@testing-library/react'; +import { ComponentProps } from 'react'; import { MemoryRouter } from 'react-router-dom'; import { EntityReference } from '../../../generated/entity/type'; +import { getDomainByName } from '../../../rest/domainAPI'; +import { clearDomainStyleCache } from '../../../utils/DomainStyleUtils'; import { DomainDisplay } from './DomainDisplay.component'; jest.mock('../../../utils/EntityUtils', () => ({ @@ -28,41 +31,72 @@ jest.mock('../../../utils/RouterUtils', () => ({ .mockImplementation((fqn: string) => `/domain/${fqn}`), })); +jest.mock('../../../rest/domainAPI', () => ({ + getDomainByName: jest.fn(), +})); + jest.mock('../../../assets/svg/ic-domain.svg', () => ({ - ReactComponent: () =>
Domain Icon
, + ReactComponent: ({ color }: { color?: string }) => ( +
+ Domain Icon +
+ ), })); +const mockGetDomainByName = getDomainByName as jest.MockedFunction< + typeof getDomainByName +>; + const mockDomain1: EntityReference = { id: 'domain-1', fullyQualifiedName: 'domain.one', name: 'Domain One', type: 'domain', -}; + style: { + color: '#dc2626', + }, +} as EntityReference; const mockDomain2: EntityReference = { id: 'domain-2', fullyQualifiedName: 'domain.two', name: 'Domain Two', type: 'domain', -}; + style: { + color: '#2563eb', + }, +} as EntityReference; const mockDomain3: EntityReference = { id: 'domain-3', fullyQualifiedName: 'domain.three', name: 'Domain Three', type: 'domain', + style: { + color: '#16a34a', + }, +} as EntityReference; + +type DomainDisplayTestProps = Omit, 'domains'> & { + domains?: EntityReference[] | null; }; -const renderDomainDisplay = (props: any) => +const renderDomainDisplay = ( + props: DomainDisplayTestProps +) => render( - + )} /> ); describe('DomainDisplay Component', () => { beforeEach(() => { jest.clearAllMocks(); + clearDomainStyleCache(); + mockGetDomainByName.mockResolvedValue( + {} as Awaited> + ); }); it('should render nothing when domains array is empty', () => { @@ -266,4 +300,36 @@ describe('DomainDisplay Component', () => { expect(screen.getByText('+2')).toBeInTheDocument(); expect(screen.queryByText('Domain Two')).not.toBeInTheDocument(); }); + + it('should fetch the domain style and apply the matching icon color', async () => { + const unresolvedDomain = { + id: 'domain-unresolved', + fullyQualifiedName: 'domain.one', + name: 'Domain One', + type: 'domain', + } as EntityReference; + + mockGetDomainByName.mockResolvedValue( + { + style: { + color: '#0891b2', + }, + } as Awaited> + ); + + renderDomainDisplay({ domains: [unresolvedDomain] }); + + await waitFor(() => + expect(mockGetDomainByName).toHaveBeenCalledWith('domain.one', { + fields: 'style', + }) + ); + + await waitFor(() => + expect(screen.getByTestId('domain-icon')).toHaveAttribute( + 'data-color', + '#0891b2' + ) + ); + }); }); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/DomainLabel/DomainLabel.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/DomainLabel/DomainLabel.component.tsx index 40a3804ae120..18c0681821b9 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/DomainLabel/DomainLabel.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/DomainLabel/DomainLabel.component.tsx @@ -28,6 +28,11 @@ import { getAPIfromSource, getEntityAPIfromSource, } from '../../../utils/Assets/AssetsUtils'; +import { + getDomainReferenceBadgeStyle, + getDomainReferenceIconColor, + useDomainsWithStyle, +} from '../../../utils/DomainStyleUtils'; import { renderDomainLink } from '../../../utils/DomainUtils'; import { showErrorToast } from '../../../utils/ToastUtils'; import { AssetsUnion } from '../../DataAssets/AssetsSelectionModal/AssetSelectionModal.interface'; @@ -53,6 +58,7 @@ export const DomainLabel = ({ }: DomainLabelProps) => { const { t } = useTranslation(); const [activeDomain, setActiveDomain] = useState([]); + const resolvedDomains = useDomainsWithStyle(activeDomain); const defaultDomainText = useMemo(() => { return showDashPlaceholder @@ -115,11 +121,11 @@ export const DomainLabel = ({ const domainLink = useMemo(() => { if ( - activeDomain && - Array.isArray(activeDomain) && - activeDomain.length > 0 + resolvedDomains && + Array.isArray(resolvedDomains) && + resolvedDomains.length > 0 ) { - const domains = activeDomain.map((domain) => { + const domains = resolvedDomains.map((domain, index) => { const inheritedIcon = domain?.inherited ? ( + key={ + domain.id ?? + domain.fullyQualifiedName ?? + domain.name ?? + `domain-${index}` + } + style={getDomainReferenceBadgeStyle(domain)}> {/* condition to show icon for new layout perticulary for multiple domains */} {(!headerLayout || (headerLayout && multiple)) && ( ); }, [ - activeDomain, + resolvedDomains, + defaultDomainText, domainDisplayName, showDomainHeading, textClassName, multiple, headerLayout, + t, ]); const selectableList = useMemo(() => { @@ -276,7 +290,15 @@ export const DomainLabel = ({
); - }, [activeDomain, hasPermission, selectableList]); + }, [ + activeDomain.length, + defaultDomainText, + domainLink, + headerLayout, + selectableList, + showDomainHeading, + t, + ]); return label; }; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/DomainLabel/DomainLabel.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/DomainLabel/DomainLabel.test.tsx index 3074a17007ee..be8361469f5d 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/DomainLabel/DomainLabel.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/DomainLabel/DomainLabel.test.tsx @@ -12,6 +12,7 @@ */ import '@testing-library/jest-dom'; import { render, screen } from '@testing-library/react'; +import { ComponentProps } from 'react'; import { MemoryRouter } from 'react-router-dom'; import { EntityType } from '../../../enums/entity.enum'; import { EntityReference } from '../../../generated/entity/type'; @@ -47,12 +48,30 @@ jest.mock('../../../utils/DomainUtils', () => ({ )), })); +jest.mock('../../../utils/DomainStyleUtils', () => ({ + getDomainReferenceBadgeStyle: jest + .fn() + .mockImplementation((domain) => + domain?.style?.color ? { borderColor: domain.style.color } : undefined + ), + getDomainReferenceIconColor: jest + .fn() + .mockImplementation((domain, fallbackColor) => + domain?.style?.color ?? fallbackColor + ), + useDomainsWithStyle: jest.fn().mockImplementation((domains) => domains), +})); + jest.mock('../../../utils/ToastUtils', () => ({ showErrorToast: jest.fn(), })); jest.mock('../../../assets/svg/ic-domain.svg', () => ({ - ReactComponent: () =>
Domain Icon
, + ReactComponent: ({ color }: { color?: string }) => ( +
+ Domain Icon +
+ ), })); jest.mock('../../../assets/svg/ic-inherit.svg', () => ({ @@ -61,10 +80,16 @@ jest.mock('../../../assets/svg/ic-inherit.svg', () => ({ jest.mock('../DomainSelectableList/DomainSelectableList.component', () => ({ __esModule: true, - default: ({ onUpdate, selectedDomain }: any) => ( + default: ({ + onUpdate, + selectedDomain, + }: { + onUpdate?: (domain: EntityReference | EntityReference[]) => void; + selectedDomain?: EntityReference | EntityReference[]; + }) => ( ), @@ -100,10 +125,21 @@ const defaultProps = { entityId: 'test-id', }; -const renderDomainLabel = (props: any = {}) => +type DomainLabelTestProps = Partial< + Omit, 'domains'> +> & { + domains?: EntityReference[] | EntityReference; +}; + +const renderDomainLabel = ( + props: DomainLabelTestProps = {} +) => render( - + )} + /> ); @@ -270,4 +306,25 @@ describe('DomainLabel Component', () => { expect(screen.getByTestId('no-domain-text')).toBeInTheDocument(); }); + + it('should apply domain color to the badge and icon when style is available', () => { + const styledDomain = { + ...mockDomain1, + style: { + color: '#7c3aed', + }, + } as EntityReference & { + style: { + color: string; + }; + }; + + renderDomainLabel({ domains: [styledDomain] }); + + expect(screen.getByText('Domain One').closest('.domain-link-container')).toHaveStyle( + { + borderColor: '#7c3aed', + } + ); + }); }); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/atoms/table/useCellRenderer.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/atoms/table/useCellRenderer.tsx index 3bb2530879af..3b5662712521 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/atoms/table/useCellRenderer.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/atoms/table/useCellRenderer.tsx @@ -11,12 +11,12 @@ * limitations under the License. */ -import { AvatarGroup, Box, Typography, useTheme } from '@mui/material'; +import { AvatarGroup, Box, Typography } from '@mui/material'; import { Avatar } from '@openmetadata/ui-core-components'; -import { Globe01 } from '@untitledui/icons'; import { ReactNode, useMemo } from 'react'; import { EntityType } from '../../../../enums/entity.enum'; import { EntityReference } from '../../../../generated/entity/type'; +import { DomainDisplay } from '../../DomainDisplay/DomainDisplay.component'; import { getEntityName } from '../../../../utils/EntityUtils'; import { getEntityAvatarProps } from '../../../../utils/IconUtils'; import { ProfilePicture } from '../ProfilePicture'; @@ -37,7 +37,6 @@ export const useCellRenderer = < props: UseCellRendererProps ) => { const { renderers = {}, chipSize = 'large' } = props; - const theme = useTheme(); const defaultRenderers: CellRenderer = useMemo( () => ({ @@ -173,22 +172,10 @@ export const useCellRenderer = < ); } - const domain = domains[0]; - - return ( - - - - {domain.displayName || domain.name} - - - ); + return ; }, }), - [renderers, theme, chipSize] + [renderers, chipSize] ); const renderCell = useMemo( diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/DomainStyleUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/DomainStyleUtils.tsx new file mode 100644 index 000000000000..d9729e950314 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/utils/DomainStyleUtils.tsx @@ -0,0 +1,156 @@ +/* + * 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 { CSSProperties, useEffect, useState } from 'react'; +import { Style } from '../generated/entity/domains/domain'; +import { EntityReference } from '../generated/entity/type'; +import { getDomainByName } from '../rest/domainAPI'; + +export type StyledDomainReference = EntityReference & { + style?: Style; +}; + +const domainStyleCache = new Map(); +const domainStyleRequestCache = new Map>(); + +export const clearDomainStyleCache = () => { + domainStyleCache.clear(); + domainStyleRequestCache.clear(); +}; + +const getStyleFromDomainReference = ( + domain: EntityReference +): Style | undefined => (domain as StyledDomainReference).style; + +const getStyledDomainReference = ( + domain: EntityReference +): StyledDomainReference => { + const style = getStyleFromDomainReference(domain); + const domainFqn = domain.fullyQualifiedName; + + if (domainFqn && style !== undefined) { + domainStyleCache.set(domainFqn, style); + + return domain as StyledDomainReference; + } + + const cachedStyle = domainFqn ? domainStyleCache.get(domainFqn) : undefined; + + if (cachedStyle) { + return { ...domain, style: cachedStyle }; + } + + return domain as StyledDomainReference; +}; + +const fetchDomainStyle = async (domainFqn: string) => { + const cachedStyle = domainStyleCache.get(domainFqn); + + if (cachedStyle !== undefined) { + return cachedStyle ?? undefined; + } + + const cachedRequest = domainStyleRequestCache.get(domainFqn); + + if (cachedRequest) { + return cachedRequest; + } + + const request = getDomainByName(domainFqn, { fields: 'style' }) + .then((domain) => { + const style = domain.style; + + domainStyleCache.set(domainFqn, style ?? null); + + return style; + }) + .finally(() => { + domainStyleRequestCache.delete(domainFqn); + }); + + domainStyleRequestCache.set(domainFqn, request); + + return request; +}; + +export const useDomainsWithStyle = ( + domains?: EntityReference[] +): StyledDomainReference[] => { + const [resolvedDomains, setResolvedDomains] = useState( + () => (domains ?? []).map(getStyledDomainReference) + ); + + useEffect(() => { + const nextDomains = (domains ?? []).map(getStyledDomainReference); + setResolvedDomains(nextDomains); + + const missingDomainFqns = nextDomains + .map((domain) => domain.fullyQualifiedName) + .filter((domainFqn): domainFqn is string => { + if (!domainFqn) { + return false; + } + + return !domainStyleCache.has(domainFqn); + }); + + if (missingDomainFqns.length === 0) { + return; + } + + let isCancelled = false; + + void Promise.all( + [...new Set(missingDomainFqns)].map(async (domainFqn) => [ + domainFqn, + await fetchDomainStyle(domainFqn), + ]) + ) + .then(() => { + if (isCancelled) { + return; + } + + setResolvedDomains((currentDomains) => + currentDomains.map(getStyledDomainReference) + ); + }) + .catch(() => undefined); + + return () => { + isCancelled = true; + }; + }, [domains]); + + return resolvedDomains; +}; + +export const getDomainReferenceColor = (domain: EntityReference): string | undefined => + getStyleFromDomainReference(domain)?.color; + +export const getDomainReferenceIconColor = ( + domain: EntityReference, + fallbackColor: string +): string => getDomainReferenceColor(domain) ?? fallbackColor; + +export const getDomainReferenceBadgeStyle = ( + domain: EntityReference +): CSSProperties | undefined => { + const color = getDomainReferenceColor(domain); + + return color + ? { + borderColor: color, + boxShadow: `inset 3px 0 0 ${color}`, + } + : undefined; +}; From 5d44c0497b981d7db6ca1d2e364d1d356525784e Mon Sep 17 00:00:00 2001 From: Manav Sharma <123449950+manavmax@users.noreply.github.com> Date: Sat, 25 Apr 2026 04:24:49 +0530 Subject: [PATCH 02/11] fix(ui): harden domain style cache behavior --- .../DomainDisplay/DomainDisplay.test.tsx | 92 +++++++++++++++--- .../ui/src/utils/DomainStyleUtils.tsx | 94 +++++++++++++++---- 2 files changed, 159 insertions(+), 27 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/DomainDisplay/DomainDisplay.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/DomainDisplay/DomainDisplay.test.tsx index 969640f7ac1e..a393f3168538 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/DomainDisplay/DomainDisplay.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/DomainDisplay/DomainDisplay.test.tsx @@ -77,13 +77,14 @@ const mockDomain3: EntityReference = { }, } as EntityReference; -type DomainDisplayTestProps = Omit, 'domains'> & { +type DomainDisplayTestProps = Omit< + ComponentProps, + 'domains' +> & { domains?: EntityReference[] | null; }; -const renderDomainDisplay = ( - props: DomainDisplayTestProps -) => +const renderDomainDisplay = (props: DomainDisplayTestProps) => render( )} /> @@ -309,13 +310,11 @@ describe('DomainDisplay Component', () => { type: 'domain', } as EntityReference; - mockGetDomainByName.mockResolvedValue( - { - style: { - color: '#0891b2', - }, - } as Awaited> - ); + mockGetDomainByName.mockResolvedValue({ + style: { + color: '#0891b2', + }, + } as Awaited>); renderDomainDisplay({ domains: [unresolvedDomain] }); @@ -332,4 +331,75 @@ describe('DomainDisplay Component', () => { ) ); }); + + it('should cache failed domain style lookups and avoid refetching on rerender', async () => { + const unresolvedDomain = { + id: 'domain-unresolved', + fullyQualifiedName: 'domain.one', + name: 'Domain One', + type: 'domain', + } as EntityReference; + + mockGetDomainByName.mockRejectedValue(new Error('request failed')); + + const { rerender } = render( + + + + ); + + await waitFor(() => expect(mockGetDomainByName).toHaveBeenCalledTimes(1)); + + rerender( + + + + ); + + await waitFor(() => + expect(screen.getByText('Domain One Updated')).toBeInTheDocument() + ); + + expect(mockGetDomainByName).toHaveBeenCalledTimes(1); + }); + + it('should refetch cached styles after the ttl expires', async () => { + const unresolvedDomain = { + id: 'domain-unresolved', + fullyQualifiedName: 'domain.one', + name: 'Domain One', + type: 'domain', + } as EntityReference; + const nowSpy = jest.spyOn(Date, 'now'); + + nowSpy.mockReturnValue(0); + mockGetDomainByName.mockResolvedValue({ + style: { + color: '#0891b2', + }, + } as Awaited>); + + const { rerender } = render( + + + + ); + + await waitFor(() => expect(mockGetDomainByName).toHaveBeenCalledTimes(1)); + + nowSpy.mockReturnValue(5 * 60 * 1000 + 1); + rerender( + + + + ); + + await waitFor(() => expect(mockGetDomainByName).toHaveBeenCalledTimes(2)); + + nowSpy.mockRestore(); + }); }); diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/DomainStyleUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/DomainStyleUtils.tsx index d9729e950314..45d1a63906fa 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/DomainStyleUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/DomainStyleUtils.tsx @@ -19,7 +19,14 @@ export type StyledDomainReference = EntityReference & { style?: Style; }; -const domainStyleCache = new Map(); +type CachedDomainStyleEntry = { + expiresAt: number; + style: Style | null; +}; + +const DOMAIN_STYLE_CACHE_TTL_MS = 5 * 60 * 1000; + +const domainStyleCache = new Map(); const domainStyleRequestCache = new Map>(); export const clearDomainStyleCache = () => { @@ -31,6 +38,53 @@ const getStyleFromDomainReference = ( domain: EntityReference ): Style | undefined => (domain as StyledDomainReference).style; +const getDomainSignature = (domain: EntityReference): string => + [ + domain.id ?? '', + domain.fullyQualifiedName ?? '', + domain.name ?? '', + domain.displayName ?? '', + domain.type ?? '', + domain.inherited ? '1' : '0', + getStyleFromDomainReference(domain)?.color ?? '', + ].join('::'); + +const pruneExpiredDomainStyleCache = (now = Date.now()) => { + for (const [domainFqn, cachedEntry] of domainStyleCache.entries()) { + if (cachedEntry.expiresAt <= now) { + domainStyleCache.delete(domainFqn); + } + } +}; + +const setCachedDomainStyle = (domainFqn: string, style: Style | null) => { + const now = Date.now(); + + pruneExpiredDomainStyleCache(now); + domainStyleCache.set(domainFqn, { + expiresAt: now + DOMAIN_STYLE_CACHE_TTL_MS, + style, + }); +}; + +const getCachedDomainStyle = ( + domainFqn: string +): CachedDomainStyleEntry | undefined => { + const cachedEntry = domainStyleCache.get(domainFqn); + + if (!cachedEntry) { + return undefined; + } + + if (cachedEntry.expiresAt <= Date.now()) { + domainStyleCache.delete(domainFqn); + + return undefined; + } + + return cachedEntry; +}; + const getStyledDomainReference = ( domain: EntityReference ): StyledDomainReference => { @@ -38,25 +92,25 @@ const getStyledDomainReference = ( const domainFqn = domain.fullyQualifiedName; if (domainFqn && style !== undefined) { - domainStyleCache.set(domainFqn, style); + setCachedDomainStyle(domainFqn, style); return domain as StyledDomainReference; } - const cachedStyle = domainFqn ? domainStyleCache.get(domainFqn) : undefined; + const cachedEntry = domainFqn ? getCachedDomainStyle(domainFqn) : undefined; - if (cachedStyle) { - return { ...domain, style: cachedStyle }; + if (cachedEntry?.style) { + return { ...domain, style: cachedEntry.style }; } return domain as StyledDomainReference; }; const fetchDomainStyle = async (domainFqn: string) => { - const cachedStyle = domainStyleCache.get(domainFqn); + const cachedEntry = getCachedDomainStyle(domainFqn); - if (cachedStyle !== undefined) { - return cachedStyle ?? undefined; + if (cachedEntry) { + return cachedEntry.style ?? undefined; } const cachedRequest = domainStyleRequestCache.get(domainFqn); @@ -69,10 +123,15 @@ const fetchDomainStyle = async (domainFqn: string) => { .then((domain) => { const style = domain.style; - domainStyleCache.set(domainFqn, style ?? null); + setCachedDomainStyle(domainFqn, style ?? null); return style; }) + .catch(() => { + setCachedDomainStyle(domainFqn, null); + + return undefined; + }) .finally(() => { domainStyleRequestCache.delete(domainFqn); }); @@ -85,9 +144,11 @@ const fetchDomainStyle = async (domainFqn: string) => { export const useDomainsWithStyle = ( domains?: EntityReference[] ): StyledDomainReference[] => { - const [resolvedDomains, setResolvedDomains] = useState( - () => (domains ?? []).map(getStyledDomainReference) - ); + const cacheWindow = Math.floor(Date.now() / DOMAIN_STYLE_CACHE_TTL_MS); + const domainsSignature = (domains ?? []).map(getDomainSignature).join('|'); + const [resolvedDomains, setResolvedDomains] = useState< + StyledDomainReference[] + >(() => (domains ?? []).map(getStyledDomainReference)); useEffect(() => { const nextDomains = (domains ?? []).map(getStyledDomainReference); @@ -100,7 +161,7 @@ export const useDomainsWithStyle = ( return false; } - return !domainStyleCache.has(domainFqn); + return !getCachedDomainStyle(domainFqn); }); if (missingDomainFqns.length === 0) { @@ -129,13 +190,14 @@ export const useDomainsWithStyle = ( return () => { isCancelled = true; }; - }, [domains]); + }, [cacheWindow, domainsSignature]); return resolvedDomains; }; -export const getDomainReferenceColor = (domain: EntityReference): string | undefined => - getStyleFromDomainReference(domain)?.color; +export const getDomainReferenceColor = ( + domain: EntityReference +): string | undefined => getStyleFromDomainReference(domain)?.color; export const getDomainReferenceIconColor = ( domain: EntityReference, From 5f2421b93887e6425b342c7a197c5d23867c3fef Mon Sep 17 00:00:00 2001 From: Manav Sharma <123449950+manavmax@users.noreply.github.com> Date: Sat, 25 Apr 2026 14:41:45 +0530 Subject: [PATCH 03/11] fix(ui): resolve PR lint and playwright failures --- .../ui/playwright/e2e/Pages/Users.spec.ts | 1 + .../ui/playwright/utils/searchSettingUtils.ts | 1 + .../DataProduct/DataProductListPage.tsx | 2 +- .../common/DomainLabel/DomainLabel.test.tsx | 18 ++++++++---------- .../common/atoms/table/useCellRenderer.tsx | 2 +- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Users.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Users.spec.ts index a1becb2c0e08..14aa7d554f6d 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Users.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Users.spec.ts @@ -33,6 +33,7 @@ import { TeamClass } from '../../support/team/TeamClass'; import { UserClass } from '../../support/user/UserClass'; import { createAdminApiContext, performAdminLogin } from '../../utils/admin'; import { + getApiContext, redirectToHomePage, toastNotification, uuid, diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/searchSettingUtils.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/searchSettingUtils.ts index 055903820850..db4460ddc6c5 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/searchSettingUtils.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/searchSettingUtils.ts @@ -29,6 +29,7 @@ export const mockEntitySearchConfig = { assetType: 'table', searchFields: [ { field: 'displayName.keyword', boost: 20, matchType: 'exact' }, + { field: 'name.keyword', boost: 20, matchType: 'exact' }, { field: 'name', boost: 10, matchType: 'phrase' }, { field: 'name.ngram', boost: 1, matchType: 'fuzzy' }, { field: 'name.compound', boost: 8, matchType: 'standard' }, diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataProduct/DataProductListPage.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataProduct/DataProductListPage.tsx index 4eef9e2a5a08..0f5f6c2d71e9 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataProduct/DataProductListPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataProduct/DataProductListPage.tsx @@ -50,10 +50,10 @@ import { useTitleAndCount } from '../common/atoms/navigation/useTitleAndCount'; import { useViewToggle } from '../common/atoms/navigation/useViewToggle'; import { usePaginationControls } from '../common/atoms/pagination/usePaginationControls'; import { hasActiveSearchOrFilter } from '../common/atoms/shared/utils/hasActiveSearchOrFilter'; +import { DomainDisplay } from '../common/DomainDisplay/DomainDisplay.component'; import EntityCardView from '../common/EntityCardView/EntityCardView.component'; import EntityListingTable from '../common/EntityListingTable/EntityListingTable.component'; import { ColumnDef } from '../common/EntityListingTable/EntityListingTable.interface'; -import { DomainDisplay } from '../common/DomainDisplay/DomainDisplay.component'; import ErrorPlaceHolder from '../common/ErrorWithPlaceholder/ErrorPlaceHolder'; import { OwnerLabel } from '../common/OwnerLabel/OwnerLabel.component'; import TagBadgeList from '../common/TagBadgeList/TagBadgeList.component'; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/DomainLabel/DomainLabel.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/DomainLabel/DomainLabel.test.tsx index be8361469f5d..c63f3817f85c 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/DomainLabel/DomainLabel.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/DomainLabel/DomainLabel.test.tsx @@ -56,8 +56,8 @@ jest.mock('../../../utils/DomainStyleUtils', () => ({ ), getDomainReferenceIconColor: jest .fn() - .mockImplementation((domain, fallbackColor) => - domain?.style?.color ?? fallbackColor + .mockImplementation( + (domain, fallbackColor) => domain?.style?.color ?? fallbackColor ), useDomainsWithStyle: jest.fn().mockImplementation((domains) => domains), })); @@ -131,9 +131,7 @@ type DomainLabelTestProps = Partial< domains?: EntityReference[] | EntityReference; }; -const renderDomainLabel = ( - props: DomainLabelTestProps = {} -) => +const renderDomainLabel = (props: DomainLabelTestProps = {}) => render( { renderDomainLabel({ domains: [styledDomain] }); - expect(screen.getByText('Domain One').closest('.domain-link-container')).toHaveStyle( - { - borderColor: '#7c3aed', - } - ); + expect( + screen.getByText('Domain One').closest('.domain-link-container') + ).toHaveStyle({ + borderColor: '#7c3aed', + }); }); }); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/atoms/table/useCellRenderer.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/atoms/table/useCellRenderer.tsx index 3b5662712521..78c2847d99f4 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/atoms/table/useCellRenderer.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/atoms/table/useCellRenderer.tsx @@ -16,9 +16,9 @@ import { Avatar } from '@openmetadata/ui-core-components'; import { ReactNode, useMemo } from 'react'; import { EntityType } from '../../../../enums/entity.enum'; import { EntityReference } from '../../../../generated/entity/type'; -import { DomainDisplay } from '../../DomainDisplay/DomainDisplay.component'; import { getEntityName } from '../../../../utils/EntityUtils'; import { getEntityAvatarProps } from '../../../../utils/IconUtils'; +import { DomainDisplay } from '../../DomainDisplay/DomainDisplay.component'; import { ProfilePicture } from '../ProfilePicture'; import { CellRenderer, ColumnConfig } from '../shared/types'; import TagsCell from './TagsCell'; From 3e1d0e69fee92919d8c36d666d4af5fa965cf1b4 Mon Sep 17 00:00:00 2001 From: Manav Sharma <123449950+manavmax@users.noreply.github.com> Date: Sun, 26 Apr 2026 01:07:56 +0530 Subject: [PATCH 04/11] test(playwright): stabilize glossary asset assignment --- .../ui/playwright/e2e/Pages/Glossary.spec.ts | 153 ++++++++---------- 1 file changed, 67 insertions(+), 86 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Glossary.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Glossary.spec.ts index 8bc26779008b..f6b48acb2c31 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Glossary.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Glossary.spec.ts @@ -110,6 +110,42 @@ const user4 = new UserClass(); const adminUser = new UserClass(); test.describe('Glossary tests', () => { + const selectGlossaryTermInPicker = async ({ + page, + searchTerm, + displayName, + fullyQualifiedName, + }: { + page: import('@playwright/test').Page; + searchTerm: string; + displayName: string; + fullyQualifiedName: string; + }) => { + const glossaryInput = page.locator('#tagsForm_tags'); + await expect(glossaryInput).toBeVisible(); + + const searchGlossaryTerm = page.waitForResponse( + (response) => + response.url().includes('/api/v1/search/query') && + response.url().includes('index=glossaryTerm') && + response.url().includes(encodeURIComponent(searchTerm)) + ); + + await glossaryInput.fill(searchTerm); + await searchGlossaryTerm; + await waitForAllLoadersToDisappear(page); + + const glossaryTermOption = page + .getByTestId(`tag-${fullyQualifiedName}`) + .first(); + await expect(glossaryTermOption).toBeVisible(); + await glossaryTermOption.click(); + + await expect( + page.locator(`[data-testid="tag-selector"]:has-text("${displayName}")`) + ).toBeVisible(); + }; + test.beforeAll(async ({ browser }) => { const { afterAction, apiContext } = await performAdminLogin(browser); await user2.create(apiContext); @@ -485,6 +521,7 @@ test.describe('Glossary tests', () => { test('Add and Remove Assets', async ({ browser }) => { test.slow(true); + test.setTimeout(300000); const { page, afterAction, apiContext } = await performAdminLogin(browser); const glossary1 = new Glossary(); @@ -528,42 +565,21 @@ test.describe('Glossary tests', () => { // Select 1st term await page.click('[data-testid="tag-selector"] #tagsForm_tags'); - - const glossaryRequest = page.waitForResponse( - `/api/v1/search/query?q=*&index=glossaryTerm&from=0&size=25&deleted=false&track_total_hits=true&getHierarchy=true` - ); - await page.type( - '[data-testid="tag-selector"] #tagsForm_tags', - glossaryTerm1.data.name - ); - await glossaryRequest; - - await page.getByText(glossaryTerm1.data.displayName).click(); - await page - .locator( - `[data-testid="tag-selector"]:has-text("${glossaryTerm1.data.displayName}")` - ) - .waitFor(); + await selectGlossaryTermInPicker({ + page, + searchTerm: glossaryTerm1.data.name, + displayName: glossaryTerm1.data.displayName, + fullyQualifiedName: glossaryTerm1.data.fullyQualifiedName, + }); // Select 2nd term await page.click('[data-testid="tag-selector"] #tagsForm_tags'); - - const glossaryRequest2 = page.waitForResponse( - `/api/v1/search/query?q=*&index=glossaryTerm&from=0&size=25&deleted=false&track_total_hits=true&getHierarchy=true` - ); - await page.type( - '[data-testid="tag-selector"] #tagsForm_tags', - glossaryTerm2.data.name - ); - await glossaryRequest2; - - await page.getByText(glossaryTerm2.data.displayName).click(); - - await page - .locator( - `[data-testid="tag-selector"]:has-text("${glossaryTerm2.data.displayName}")` - ) - .waitFor(); + await selectGlossaryTermInPicker({ + page, + searchTerm: glossaryTerm2.data.name, + displayName: glossaryTerm2.data.displayName, + fullyQualifiedName: glossaryTerm2.data.fullyQualifiedName, + }); const patchRequest = page.waitForResponse( (res) => @@ -583,42 +599,21 @@ test.describe('Glossary tests', () => { // Select 1st term await page.click('[data-testid="tag-selector"] #tagsForm_tags'); - - const glossaryRequest3 = page.waitForResponse( - `/api/v1/search/query?q=*&index=glossaryTerm&from=0&size=25&deleted=false&track_total_hits=true&getHierarchy=true` - ); - await page.type( - '[data-testid="tag-selector"] #tagsForm_tags', - glossaryTerm3.data.name - ); - await glossaryRequest3; - - await page.getByText(glossaryTerm3.data.displayName).click(); - await page - .locator( - `[data-testid="tag-selector"]:has-text("${glossaryTerm3.data.displayName}")` - ) - .waitFor(); + await selectGlossaryTermInPicker({ + page, + searchTerm: glossaryTerm3.data.name, + displayName: glossaryTerm3.data.displayName, + fullyQualifiedName: glossaryTerm3.data.fullyQualifiedName, + }); // Select 2nd term await page.click('[data-testid="tag-selector"] #tagsForm_tags'); - - const glossaryRequest4 = page.waitForResponse( - `/api/v1/search/query?q=*&index=glossaryTerm&from=0&size=25&deleted=false&track_total_hits=true&getHierarchy=true` - ); - await page.type( - '[data-testid="tag-selector"] #tagsForm_tags', - glossaryTerm4.data.name - ); - await glossaryRequest4; - - await page.getByText(glossaryTerm4.data.displayName).click(); - - await page - .locator( - `[data-testid="tag-selector"]:has-text("${glossaryTerm4.data.displayName}")` - ) - .waitFor(); + await selectGlossaryTermInPicker({ + page, + searchTerm: glossaryTerm4.data.name, + displayName: glossaryTerm4.data.displayName, + fullyQualifiedName: glossaryTerm4.data.fullyQualifiedName, + }); const patchRequest2 = page.waitForResponse(`/api/v1/dashboards/*`); @@ -656,26 +651,12 @@ test.describe('Glossary tests', () => { ); await page.click('[data-testid="tag-selector"]'); - - const glossaryRequest5 = page.waitForResponse( - `/api/v1/search/query?q=*&index=glossaryTerm&from=0&size=25&deleted=false&track_total_hits=true&getHierarchy=true` - ); - await page.type( - '[data-testid="tag-selector"] #tagsForm_tags', - glossaryTerm3.data.name - ); - await glossaryRequest5; - - await page - .getByRole('tree') - .getByTestId(`tag-${glossaryTerm3.data.fullyQualifiedName}`) - .click(); - - await page - .locator( - `[data-testid="tag-selector"]:has-text("${glossaryTerm3.data.displayName}")` - ) - .waitFor(); + await selectGlossaryTermInPicker({ + page, + searchTerm: glossaryTerm3.data.name, + displayName: glossaryTerm3.data.displayName, + fullyQualifiedName: glossaryTerm3.data.fullyQualifiedName, + }); const patchRequest3 = page.waitForResponse(`/api/v1/charts/*`); From 4cdba8b0b152a7fff610504d9140b56877644a3b Mon Sep 17 00:00:00 2001 From: Manav Sharma <123449950+manavmax@users.noreply.github.com> Date: Sun, 26 Apr 2026 12:01:53 +0530 Subject: [PATCH 05/11] test(playwright): stabilize failing CI shards --- .../Features/LineagePipelineAnnotator.spec.ts | 99 ++++++++++++++++++- .../ui/playwright/e2e/Pages/Glossary.spec.ts | 13 ++- .../ui/playwright/e2e/Pages/Users.spec.ts | 1 + 3 files changed, 110 insertions(+), 3 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/LineagePipelineAnnotator.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/LineagePipelineAnnotator.spec.ts index e011ab24469f..954136391054 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/LineagePipelineAnnotator.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/LineagePipelineAnnotator.spec.ts @@ -11,7 +11,7 @@ * limitations under the License. */ -import { expect, test } from '@playwright/test'; +import { APIRequestContext, expect, test } from '@playwright/test'; import { TableClass } from '../../support/entity/TableClass'; import { getAuthContext, @@ -43,6 +43,37 @@ let topicFqn: string; let pipelineFqn: string; const LINEAGE_API = '/api/v1/lineage/getLineage?fqn=*'; +type LineageData = { + nodes?: Record; + downstreamEdges?: Record< + string, + { + pipeline?: { + fullyQualifiedName?: string; + }; + } + >; +}; + +const getLineageData = async ( + apiContext: APIRequestContext, + fqn: string, + type: string, + upstreamDepth = 1, + downstreamDepth = 1 +): Promise => { + const response = await apiContext.get( + `/api/v1/lineage/getLineage?fqn=${encodeURIComponent( + fqn + )}&type=${type}&upstreamDepth=${upstreamDepth}&downstreamDepth=${downstreamDepth}` + ); + + if (!response.ok()) { + return null; + } + + return (await response.json()) as LineageData; +}; test.describe('Lineage Pipeline Annotator', () => { test.beforeAll(async ({ browser }) => { @@ -109,7 +140,7 @@ test.describe('Lineage Pipeline Annotator', () => { .then((r) => r.json()); pipelineFqn = pipelineResp.fullyQualifiedName; - await apiContext.put('/api/v1/lineage', { + const addLineageResponse = await apiContext.put('/api/v1/lineage', { data: { edge: { fromEntity: { id: table.entityResponseData.id, type: 'table' }, @@ -121,6 +152,70 @@ test.describe('Lineage Pipeline Annotator', () => { }, }, }); + expect(addLineageResponse.ok()).toBe(true); + + const tableFqn = table.entityResponseData.fullyQualifiedName ?? ''; + + await expect + .poll( + async () => { + const data = await getLineageData(apiContext, tableFqn, 'table'); + + return Object.keys(data?.nodes ?? {}).includes(topicFqn); + }, + { timeout: 30000, intervals: [1000, 2000, 3000] } + ) + .toBe(true); + + await expect + .poll( + async () => { + const data = await getLineageData(apiContext, tableFqn, 'table'); + const downstreamEdges = Object.values(data?.downstreamEdges ?? {}); + + return downstreamEdges.some( + (edge) => edge?.pipeline?.fullyQualifiedName === pipelineFqn + ); + }, + { timeout: 30000, intervals: [1000, 2000, 3000] } + ) + .toBe(true); + + await expect + .poll( + async () => { + const data = await getLineageData( + apiContext, + pipelineServiceFqn, + 'pipelineService' + ); + const nodeFqns = Object.keys(data?.nodes ?? {}); + + return ( + nodeFqns.includes(dbServiceFqn) && + nodeFqns.includes(messagingServiceFqn) + ); + }, + { timeout: 30000, intervals: [1000, 2000, 3000] } + ) + .toBe(true); + + await expect + .poll( + async () => { + const data = await getLineageData( + apiContext, + dbServiceFqn, + 'databaseService', + 0, + 1 + ); + + return Object.keys(data?.nodes ?? {}).includes(pipelineServiceFqn); + }, + { timeout: 30000, intervals: [1000, 2000, 3000] } + ) + .toBe(true); await apiContext.dispose(); await page.close(); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Glossary.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Glossary.spec.ts index 693af4307e51..4dcd704af266 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Glossary.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Glossary.spec.ts @@ -125,6 +125,15 @@ test.describe('Glossary tests', () => { const glossaryTermOption = page .getByTestId(`tag-${fullyQualifiedName}`) .first(); + const selectedGlossaryTerm = page.getByTestId( + `selected-tag-${fullyQualifiedName}` + ); + const selectedGlossaryTermText = page.locator( + `[data-testid="tag-selector"]:has-text("${displayName}")` + ); + const saveButton = page + .locator('.ant-select-dropdown') + .getByTestId('saveAssociatedTag'); await expect(glossaryInput).toBeVisible(); @@ -140,10 +149,12 @@ test.describe('Glossary tests', () => { } ) .toBe(true); + await glossaryTermOption.scrollIntoViewIfNeeded(); await glossaryTermOption.click(); + await saveButton.waitFor({ state: 'visible' }); await expect( - page.locator(`[data-testid="tag-selector"]:has-text("${displayName}")`) + selectedGlossaryTerm.or(selectedGlossaryTermText) ).toBeVisible(); }; diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Users.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Users.spec.ts index 14aa7d554f6d..dded748059d9 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Users.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Users.spec.ts @@ -1389,6 +1389,7 @@ base.describe( base( 'User Performance across different entities pages', async ({ browser }) => { + base.setTimeout(300000); const { page, afterAction } = await performUserLogin(browser, user); for (const entity of entities) { From 4d1d9d03d5324bee68087c82fda8fe58d5f4462a Mon Sep 17 00:00:00 2001 From: Manav Sharma <123449950+manavmax@users.noreply.github.com> Date: Sun, 26 Apr 2026 13:06:05 +0530 Subject: [PATCH 06/11] ci: harden OpenMetadata test environment retries --- .../actions/setup-openmetadata-test-environment/action.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/actions/setup-openmetadata-test-environment/action.yml b/.github/actions/setup-openmetadata-test-environment/action.yml index afe52f9938fe..7dd68370adaa 100644 --- a/.github/actions/setup-openmetadata-test-environment/action.yml +++ b/.github/actions/setup-openmetadata-test-environment/action.yml @@ -99,6 +99,8 @@ runs: INGESTION_DEPENDENCY: ${{ inputs.ingestion_dependency }} with: timeout_minutes: 60 - max_attempts: 2 + max_attempts: 3 + retry_wait_seconds: 30 retry_on: error + on_retry_command: cd ./docker/development && docker compose down --remove-orphans && sudo rm -rf ${PWD}/docker-volume command: ${{ inputs.startup-script }} ${{ inputs.args }} From 2a0c56a627250def08c30a567f7c625d0f87e99d Mon Sep 17 00:00:00 2001 From: Manav Sharma <123449950+manavmax@users.noreply.github.com> Date: Sun, 26 Apr 2026 15:49:43 +0530 Subject: [PATCH 07/11] test(playwright): stabilize remaining CI shards --- .../playwright/e2e/Flow/ServiceForm.spec.ts | 23 +++++--- .../e2e/Pages/ExplorePageRightPanel.spec.ts | 2 + .../ui/playwright/e2e/Pages/Glossary.spec.ts | 59 ++++++++++++------- .../e2e/Pages/ServiceEntity.spec.ts | 2 + 4 files changed, 59 insertions(+), 27 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Flow/ServiceForm.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Flow/ServiceForm.spec.ts index 3a3140fc9aa6..7dffec08c07c 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Flow/ServiceForm.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Flow/ServiceForm.spec.ts @@ -11,7 +11,7 @@ * limitations under the License. */ -import { expect, test } from '@playwright/test'; +import { expect, Page, test } from '@playwright/test'; import * as fs from 'fs'; import * as path from 'path'; import { PLAYWRIGHT_INGESTION_TAG_OBJ } from '../../constant/config'; @@ -38,6 +38,18 @@ const SERVICE_NAMES = { const adminUser = new UserClass(); +const openDashboardServiceForm = async (page: Page, serviceTestId: string) => { + await page.goto('/dashboardServices/add-service', { + timeout: 120000, + waitUntil: 'domcontentloaded', + }); + await page.waitForURL('**/dashboardServices/add-service', { + waitUntil: 'domcontentloaded', + }); + await waitForAllLoadersToDisappear(page, 'loader', 120000); + await expect(page.getByTestId(serviceTestId)).toBeVisible(); +}; + // use the admin user to login test.use({ storageState: 'playwright/.auth/admin.json' }); @@ -83,8 +95,7 @@ test.describe( test('Verify form selects are working properly', async ({ page }) => { test.slow(); - await page.goto('/dashboardServices/add-service'); - await waitForAllLoadersToDisappear(page); + await openDashboardServiceForm(page, 'Superset'); await page.click(`[data-testid="Superset"]`); await page.click('[data-testid="next-button"]'); @@ -243,8 +254,7 @@ test.describe( test('Verify SSL cert upload with long filename and UI overflow handling', async ({ page, }) => { - await page.goto('/dashboardServices/add-service'); - await waitForAllLoadersToDisappear(page); + await openDashboardServiceForm(page, 'Superset'); await page.click(`[data-testid="Superset"]`); await page.click('[data-testid="next-button"]'); @@ -387,8 +397,7 @@ test.describe( test('Verify if string input inside oneOf config works properly', async ({ page, }) => { - await page.goto('/dashboardServices/add-service'); - await waitForAllLoadersToDisappear(page); + await openDashboardServiceForm(page, 'Looker'); await page.getByTestId('Looker').click(); await page.getByTestId('next-button').click(); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/ExplorePageRightPanel.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/ExplorePageRightPanel.spec.ts index 0843775cb338..ee48fe2f59a0 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/ExplorePageRightPanel.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/ExplorePageRightPanel.spec.ts @@ -1195,6 +1195,8 @@ test.describe('Right Panel Test Suite', () => { }); test.describe('Data Steward User - Permission Verification', () => { + test.describe.configure({ timeout: 180000 }); + const dataStewardEntityMap = { table: new TableClass(), dashboard: new DashboardClass(), diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Glossary.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Glossary.spec.ts index 4dcd704af266..2670b6caa2c2 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Glossary.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Glossary.spec.ts @@ -110,6 +110,30 @@ const user4 = new UserClass(); const adminUser = new UserClass(); test.describe('Glossary tests', () => { + const waitForTagSaveToFinish = async ({ + page, + responseMatcher, + }: { + page: import('@playwright/test').Page; + responseMatcher: + | string + | RegExp + | (( + response: import('@playwright/test').Response + ) => boolean | Promise); + }) => { + const saveButton = page.getByTestId('saveAssociatedTag'); + const response = page.waitForResponse(responseMatcher); + + await saveButton.click(); + await response; + await saveButton + .locator('[data-icon="loading"]') + .waitFor({ state: 'detached' }); + await expect(saveButton).not.toBeVisible(); + await waitForAllLoadersToDisappear(page); + }; + const selectGlossaryTermInPicker = async ({ page, inputValue, @@ -533,7 +557,7 @@ test.describe('Glossary tests', () => { test('Add and Remove Assets', async ({ browser }) => { test.slow(true); - test.setTimeout(300000); + test.setTimeout(420000); const { page, afterAction, apiContext } = await performAdminLogin(browser); const glossary1 = new Glossary(); @@ -593,16 +617,13 @@ test.describe('Glossary tests', () => { fullyQualifiedName: glossaryTerm2.data.fullyQualifiedName, }); - const patchRequest = page.waitForResponse( - (res) => - res.url().includes('/api/v1/dashboards/') && - res.request().method() === 'PATCH' - ); - await expect(page.getByTestId('saveAssociatedTag')).toBeEnabled(); - - await page.getByTestId('saveAssociatedTag').click(); - await patchRequest; + await waitForTagSaveToFinish({ + page, + responseMatcher: (res) => + res.url().includes('/api/v1/dashboards/') && + res.request().method() === 'PATCH', + }); // Add non mutually exclusive tags await page.click( @@ -627,12 +648,11 @@ test.describe('Glossary tests', () => { fullyQualifiedName: glossaryTerm4.data.fullyQualifiedName, }); - const patchRequest2 = page.waitForResponse(`/api/v1/dashboards/*`); - await expect(page.getByTestId('saveAssociatedTag')).toBeEnabled(); - - await page.getByTestId('saveAssociatedTag').click(); - await patchRequest2; + await waitForTagSaveToFinish({ + page, + responseMatcher: `/api/v1/dashboards/*`, + }); // Check if the terms are present await expect( @@ -670,12 +690,11 @@ test.describe('Glossary tests', () => { fullyQualifiedName: glossaryTerm3.data.fullyQualifiedName, }); - const patchRequest3 = page.waitForResponse(`/api/v1/charts/*`); - await expect(page.getByTestId('saveAssociatedTag')).toBeEnabled(); - - await page.getByTestId('saveAssociatedTag').click(); - await patchRequest3; + await waitForTagSaveToFinish({ + page, + responseMatcher: `/api/v1/charts/*`, + }); // Check if the term is present const tagSelectorText = await page diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/ServiceEntity.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/ServiceEntity.spec.ts index 058caf666c36..645e5a53f16a 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/ServiceEntity.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/ServiceEntity.spec.ts @@ -84,6 +84,8 @@ Object.entries(entities).forEach(([key, EntityClass]) => { const deleteEntity = new EntityClass(); test.describe(key, () => { + test.describe.configure({ timeout: 180000 }); + test.beforeAll('Setup pre-requests', async ({ browser }) => { const { apiContext, afterAction } = await performAdminLogin(browser); From 91682dec72bb1f0eb4628e1fb852abaab72f5fe3 Mon Sep 17 00:00:00 2001 From: Manav Sharma <123449950+manavmax@users.noreply.github.com> Date: Sun, 26 Apr 2026 18:23:21 +0530 Subject: [PATCH 08/11] test(playwright): shorten glossary asset flow --- .../ui/playwright/e2e/Pages/Glossary.spec.ts | 101 +++++++++++------- 1 file changed, 60 insertions(+), 41 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Glossary.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Glossary.spec.ts index 2670b6caa2c2..8050dde2a3c1 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Glossary.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Glossary.spec.ts @@ -10,7 +10,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import test, { expect } from '@playwright/test'; +import test, { APIRequestContext, expect } from '@playwright/test'; import { get } from 'lodash'; import { SidebarItem } from '../../constant/sidebar'; import { Domain } from '../../support/domain/Domain'; @@ -110,6 +110,42 @@ const user4 = new UserClass(); const adminUser = new UserClass(); test.describe('Glossary tests', () => { + const waitForGlossaryTermAssetsCount = async ({ + apiContext, + glossaryTerm, + assetsCount, + }: { + apiContext: APIRequestContext; + glossaryTerm: GlossaryTerm; + assetsCount: number; + }) => { + await expect + .poll( + async () => { + const response = await apiContext.get( + `/api/v1/glossaryTerms/name/${encodeURIComponent( + glossaryTerm.responseData.fullyQualifiedName + )}?fields=assets` + ); + + if (!response.ok()) { + return -1; + } + + const glossaryTermData = (await response.json()) as { + assets?: unknown[]; + }; + + return glossaryTermData.assets?.length ?? 0; + }, + { + timeout: 180000, + intervals: [2000, 5000, 10000], + } + ) + .toBe(assetsCount); + }; + const waitForTagSaveToFinish = async ({ page, responseMatcher, @@ -557,7 +593,7 @@ test.describe('Glossary tests', () => { test('Add and Remove Assets', async ({ browser }) => { test.slow(true); - test.setTimeout(420000); + test.setTimeout(480000); const { page, afterAction, apiContext } = await performAdminLogin(browser); const glossary1 = new Glossary(); @@ -583,15 +619,6 @@ test.describe('Glossary tests', () => { try { await test.step('Add asset to glossary term using entity', async () => { - await sidebarClick(page, SidebarItem.GLOSSARY); - - await selectActiveGlossary(page, glossary2.data.displayName); - await goToAssetsTab(page, glossaryTerm3.data.displayName); - - await page - .getByText("Looks like you haven't added any data assets yet.") - .waitFor(); - await dashboardEntity.visitEntityPage(page); // Dashboard Entity Right Panel @@ -677,41 +704,33 @@ test.describe('Glossary tests', () => { expect(await icons.count()).toBe(3); - // Add Glossary to Dashboard Charts - await page.click( - '[data-testid="glossary-tags-0"] > [data-testid="tags-wrapper"] > [data-testid="glossary-container"] > [data-testid="entity-tags"] [data-testid="add-tag"]' + const chartTagResponse = await apiContext.patch( + `/api/v1/charts/${dashboardEntity.chartsResponseData.id}`, + { + data: [ + { + op: 'add', + path: '/tags/0', + value: { + tagFQN: glossaryTerm3.responseData.fullyQualifiedName, + source: 'Glossary', + }, + }, + ], + headers: { + 'Content-Type': 'application/json-patch+json', + }, + } ); - await page.click('[data-testid="tag-selector"]'); - await selectGlossaryTermInPicker({ - page, - inputValue: glossaryTerm3.data.name, - displayName: glossaryTerm3.data.displayName, - fullyQualifiedName: glossaryTerm3.data.fullyQualifiedName, - }); + expect(chartTagResponse.ok()).toBeTruthy(); - await expect(page.getByTestId('saveAssociatedTag')).toBeEnabled(); - await waitForTagSaveToFinish({ - page, - responseMatcher: `/api/v1/charts/*`, + await waitForGlossaryTermAssetsCount({ + apiContext, + glossaryTerm: glossaryTerm3, + assetsCount: 2, }); - // Check if the term is present - const tagSelectorText = await page - .locator( - '[data-testid="glossary-tags-0"] [data-testid="glossary-container"] [data-testid="tags"]' - ) - .innerText(); - - expect(tagSelectorText).toContain(glossaryTerm3.data.displayName); - - // Check if the icon is visible - const icon = page.locator( - '[data-testid="glossary-tags-0"] > [data-testid="tags-wrapper"] > [data-testid="glossary-container"] [data-testid="glossary-icon"]' - ); - - await expect(icon).toBeVisible(); - await sidebarClick(page, SidebarItem.GLOSSARY); await selectActiveGlossary(page, glossary2.data.displayName); From a2712228a662b1788a48f7a3d8e907a8e93d18a2 Mon Sep 17 00:00:00 2001 From: Manav Sharma <123449950+manavmax@users.noreply.github.com> Date: Sun, 26 Apr 2026 21:33:26 +0530 Subject: [PATCH 09/11] test(playwright): use glossary assets endpoint --- .../resources/ui/playwright/e2e/Pages/Glossary.spec.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Glossary.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Glossary.spec.ts index 8050dde2a3c1..a86400388b19 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Glossary.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Glossary.spec.ts @@ -123,9 +123,7 @@ test.describe('Glossary tests', () => { .poll( async () => { const response = await apiContext.get( - `/api/v1/glossaryTerms/name/${encodeURIComponent( - glossaryTerm.responseData.fullyQualifiedName - )}?fields=assets` + `/api/v1/glossaryTerms/${glossaryTerm.responseData.id}/assets?limit=1` ); if (!response.ok()) { @@ -133,10 +131,12 @@ test.describe('Glossary tests', () => { } const glossaryTermData = (await response.json()) as { - assets?: unknown[]; + paging?: { + total?: number; + }; }; - return glossaryTermData.assets?.length ?? 0; + return glossaryTermData.paging?.total ?? 0; }, { timeout: 180000, From 20ab33d2ec4cced4348890ef64f1e40b8c0eeadb Mon Sep 17 00:00:00 2001 From: Manav Sharma <123449950+manavmax@users.noreply.github.com> Date: Sun, 3 May 2026 20:13:04 +0530 Subject: [PATCH 10/11] Fix flaky data contract inheritance playwright test --- .../e2e/Pages/DataContractInheritance.spec.ts | 103 ++++++++++++++---- 1 file changed, 83 insertions(+), 20 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DataContractInheritance.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DataContractInheritance.spec.ts index e16409b3865f..d116a25dbe0a 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DataContractInheritance.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DataContractInheritance.spec.ts @@ -177,6 +177,58 @@ const openContractTab = async (page: Page) => { await waitForAllLoadersToDisappear(page); }; +const waitForTableContractState = async ( + page: Page, + table: TableClass, + expectedState: string +) => { + const { apiContext, afterAction } = await getApiContext(page); + + try { + await expect + .poll( + async () => { + const response = await apiContext.get( + `/api/v1/dataContracts/entity?entityId=${table.entityResponseData.id}&entityType=table&fields=owners` + ); + + if (!response.ok()) { + return 'absent'; + } + + const contract = (await response.json()) as { + inherited?: boolean; + name?: string; + }; + + return `${contract.inherited ? 'inherited' : 'direct'}:${contract.name ?? ''}`; + }, + { + timeout: 60000, + intervals: [1000, 2000, 5000], + } + ) + .toBe(expectedState); + } finally { + await afterAction(); + } +}; + +const visitTableContractTab = async ({ + page, + table, + expectedContractState, +}: { + page: Page; + table: TableClass; + expectedContractState: string; +}) => { + await table.visitEntityPage(page); + await waitForTableContractState(page, table, expectedContractState); + await openContractTab(page); + await waitForAllLoadersToDisappear(page); +}; + const startAddingContract = async (page: Page) => { await expect(page.getByTestId('no-data-placeholder')).toBeVisible(); await expect(page.getByTestId('add-contract-button')).toBeVisible(); @@ -836,10 +888,11 @@ test.describe('Data Contract Inheritance', () => { }); await test.step('Navigate to asset and verify inherited contract', async () => { - await tableForEditInheritedTest.visitEntityPage(page); - await openContractTab(page); - - await waitForAllLoadersToDisappear(page); + await visitTableContractTab({ + page, + table: tableForEditInheritedTest, + expectedContractState: `inherited:${DP_CONTRACT_DETAILS.name}`, + }); // Verify the inherited contract is displayed await expect(page.getByTestId('contract-title')).toContainText( @@ -973,10 +1026,11 @@ test.describe('Data Contract Inheritance', () => { }); await test.step('Navigate to asset and verify delete is disabled for inherited contract', async () => { - await tableForDeleteDisabledTest.visitEntityPage(page); - await openContractTab(page); - - await waitForAllLoadersToDisappear(page); + await visitTableContractTab({ + page, + table: tableForDeleteDisabledTest, + expectedContractState: `inherited:${DP_CONTRACT_DETAILS.name}`, + }); // Verify the inherited contract is displayed await expect(page.getByTestId('contract-title')).toContainText( @@ -1045,10 +1099,11 @@ test.describe('Data Contract Inheritance', () => { }); await test.step('Navigate to asset and run validation on inherited contract', async () => { - await tableForRunValidationTest.visitEntityPage(page); - await openContractTab(page); - - await waitForAllLoadersToDisappear(page); + await visitTableContractTab({ + page, + table: tableForRunValidationTest, + expectedContractState: `inherited:${DP_CONTRACT_DETAILS.name}`, + }); // Verify the inherited contract is displayed await expect(page.getByTestId('contract-title')).toContainText( @@ -1126,10 +1181,11 @@ test.describe('Data Contract Inheritance', () => { }); await test.step('Verify asset shows inherited contract', async () => { - await tableForRemoveAssetTest.visitEntityPage(page); - await openContractTab(page); - - await waitForAllLoadersToDisappear(page); + await visitTableContractTab({ + page, + table: tableForRemoveAssetTest, + expectedContractState: `inherited:${DP_CONTRACT_DETAILS.name}`, + }); // Verify the inherited contract is displayed await expect(page.getByTestId('contract-title')).toContainText( @@ -1166,6 +1222,7 @@ test.describe('Data Contract Inheritance', () => { await test.step('Verify asset no longer shows inherited contract', async () => { await tableForRemoveAssetTest.visitEntityPage(page); + await waitForTableContractState(page, tableForRemoveAssetTest, 'absent'); await openContractTab(page); await waitForAllLoadersToDisappear(page); @@ -1223,10 +1280,11 @@ test.describe('Data Contract Inheritance', () => { }); await test.step('Create asset own contract', async () => { - await tableForDeleteFallbackTest.visitEntityPage(page); - await openContractTab(page); - - await waitForAllLoadersToDisappear(page); + await visitTableContractTab({ + page, + table: tableForDeleteFallbackTest, + expectedContractState: `inherited:${DP_CONTRACT_DETAILS.name}`, + }); // Click edit to add asset's own contract await page.getByTestId('manage-contract-actions').click(); @@ -1301,6 +1359,11 @@ test.describe('Data Contract Inheritance', () => { await test.step('Verify asset now shows inherited contract from Data Product', async () => { // Wait for contract to reload after deletion await waitForAllLoadersToDisappear(page); + await waitForTableContractState( + page, + tableForDeleteFallbackTest, + `inherited:${DP_CONTRACT_DETAILS.name}` + ); // Refresh the page to ensure we get the latest contract state await page.reload(); From 79bb0d232855a6cf77a6afdcec06365ed0b1b9d2 Mon Sep 17 00:00:00 2001 From: Manav Sharma <123449950+manavmax@users.noreply.github.com> Date: Sun, 3 May 2026 20:25:42 +0530 Subject: [PATCH 11/11] Fix ui checkstyle for data contract spec --- .../ui/playwright/e2e/Pages/DataContractInheritance.spec.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DataContractInheritance.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DataContractInheritance.spec.ts index d116a25dbe0a..62ef337e3856 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DataContractInheritance.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DataContractInheritance.spec.ts @@ -201,7 +201,9 @@ const waitForTableContractState = async ( name?: string; }; - return `${contract.inherited ? 'inherited' : 'direct'}:${contract.name ?? ''}`; + return `${contract.inherited ? 'inherited' : 'direct'}:${ + contract.name ?? '' + }`; }, { timeout: 60000,