diff --git a/openmetadata-ui/src/main/resources/ui/playwright/support/entity/DatabaseClass.ts b/openmetadata-ui/src/main/resources/ui/playwright/support/entity/DatabaseClass.ts
index 612c8226613..cb836d4fd22 100644
--- a/openmetadata-ui/src/main/resources/ui/playwright/support/entity/DatabaseClass.ts
+++ b/openmetadata-ui/src/main/resources/ui/playwright/support/entity/DatabaseClass.ts
@@ -14,7 +14,12 @@ import { APIRequestContext, expect, Page } from '@playwright/test';
import { Operation } from 'fast-json-patch';
import { SERVICE_TYPE } from '../../constant/service';
import { ServiceTypes } from '../../constant/settings';
-import { assignDomain, removeDomain, uuid } from '../../utils/common';
+import {
+ assignDomain,
+ removeDomain,
+ uuid,
+ verifyDomainLinkInCard,
+} from '../../utils/common';
import {
addMultiOwner,
addOwner,
@@ -250,17 +255,16 @@ export class DatabaseClass extends EntityClass {
).toBeVisible();
}
- async verifyDomainChangeInES(page: Page, domain: Domain['responseData']) {
- // Verify domain change in ES
+ async verifyDomainChangeInES(page: Page, domains: Domain['responseData'][]) {
const searchTerm = this.tableResponseData?.['fullyQualifiedName'];
await page.getByTestId('searchBox').fill(searchTerm);
await page.getByTestId('searchBox').press('Enter');
- await expect(
- page
- .getByTestId(`table-data-card_${searchTerm}`)
- .getByTestId('domains-link')
- ).toContainText(domain.displayName);
+ const entityCard = page.getByTestId(`table-data-card_${searchTerm}`);
+
+ for (const domain of domains) {
+ await verifyDomainLinkInCard(entityCard, domain);
+ }
await page.getByTestId('searchBox').clear();
}
@@ -272,7 +276,7 @@ export class DatabaseClass extends EntityClass {
}
async verifyDomainPropagation(page: Page, domain: Domain['responseData']) {
- await this.verifyDomainChangeInES(page, domain);
+ await this.verifyDomainChangeInES(page, [domain]);
await this.visitEntityPage(page);
}
diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/common.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/common.ts
index 5c40e7c91f0..002107daef9 100644
--- a/openmetadata-ui/src/main/resources/ui/playwright/utils/common.ts
+++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/common.ts
@@ -10,7 +10,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import { Browser, expect, Page, request } from '@playwright/test';
+import { Browser, expect, Locator, Page, request } from '@playwright/test';
import { randomUUID } from 'crypto';
import { SidebarItem } from '../constant/sidebar';
import { adjectives, nouns } from '../constant/user';
@@ -411,6 +411,23 @@ export const generateRandomUsername = (prefix = '') => {
};
};
+export const verifyDomainLinkInCard = async (
+ entityCard: Locator,
+ domain: Domain['responseData']
+) => {
+ const domainLink = entityCard.getByTestId('domain-link').filter({
+ hasText: domain.displayName,
+ });
+
+ await expect(domainLink).toBeVisible();
+ await expect(domainLink).toContainText(domain.displayName);
+
+ const href = await domainLink.getAttribute('href');
+
+ expect(href).toContain('/domain/');
+ await expect(domainLink).toBeEnabled();
+};
+
export const verifyDomainPropagation = async (
page: Page,
domain: Domain['responseData'],
@@ -418,12 +435,16 @@ export const verifyDomainPropagation = async (
) => {
await page.getByTestId('searchBox').fill(childFqnSearchTerm);
await page.getByTestId('searchBox').press('Enter');
+ await page.waitForSelector(`[data-testid*="table-data-card"]`);
- await expect(
- page
- .getByTestId(`table-data-card_${childFqnSearchTerm}`)
- .getByTestId('domains-link')
- ).toContainText(domain.displayName);
+ const entityCard = page.getByTestId(`table-data-card_${childFqnSearchTerm}`);
+
+ await expect(entityCard).toBeVisible();
+
+ const domainLink = entityCard.getByTestId('domain-link').first();
+
+ await expect(domainLink).toBeVisible();
+ await expect(domainLink).toContainText(domain.displayName);
};
export const replaceAllSpacialCharWith_ = (text: string) => {
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ExploreV1/ExploreSearchCard/ExploreSearchCard.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ExploreV1/ExploreSearchCard/ExploreSearchCard.test.tsx
new file mode 100644
index 00000000000..9e58db555d8
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/components/ExploreV1/ExploreSearchCard/ExploreSearchCard.test.tsx
@@ -0,0 +1,110 @@
+/*
+ * 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 '@testing-library/jest-dom';
+import { render, screen } from '@testing-library/react';
+import { MemoryRouter } from 'react-router-dom';
+import searchClassBase from '../../../utils/SearchClassBase';
+import ExploreSearchCard from './ExploreSearchCard';
+import { ExploreSearchCardProps } from './ExploreSearchCard.interface';
+
+jest.mock('../../../utils/RouterUtils', () => ({
+ getDomainPath: jest.fn().mockReturnValue('/mock-domain'),
+}));
+
+jest.mock('../../../utils/EntityUtils', () => ({
+ getEntityName: jest.fn().mockReturnValue('Mock Entity'),
+ highlightSearchText: jest.fn().mockReturnValue(''),
+}));
+
+jest.mock('../../../utils/SearchClassBase', () => ({
+ __esModule: true,
+ default: {
+ getListOfEntitiesWithoutDomain: jest.fn(),
+ getListOfEntitiesWithoutTier: jest.fn().mockReturnValue([]),
+ getServiceIcon: jest.fn().mockReturnValue(service-icon),
+ getEntityBreadcrumbs: jest.fn().mockReturnValue([]),
+ getEntityIcon: jest.fn().mockReturnValue(entity-icon),
+ getEntityLink: jest.fn().mockReturnValue('/entity/test'),
+ getEntityName: jest.fn().mockReturnValue('Test Domain'),
+ getSearchEntityLinkTarget: jest.fn().mockReturnValue('_self'),
+ },
+}));
+
+jest.mock('../../common/DomainDisplay/DomainDisplay.component', () => ({
+ DomainDisplay: jest
+ .fn()
+ .mockReturnValue(
Domain Display
),
+}));
+
+const baseSource: ExploreSearchCardProps['source'] = {
+ id: 'base-1',
+ fullyQualifiedName: 'test.fqn',
+ name: 'test',
+ entityType: 'table',
+ tags: [],
+ owners: [],
+ domains: [],
+};
+
+const defaultProps: Omit = {
+ id: '1',
+ showEntityIcon: false,
+};
+
+const renderCard = (
+ sourceOverrides: Partial
+) =>
+ render(
+
+
+
+ );
+
+describe('ExploreSearchCard - Domain section', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('renders DomainDisplay component', () => {
+ renderCard({
+ domains: [{ id: '1', fullyQualifiedName: 'domain.test', type: 'domain' }],
+ });
+
+ expect(screen.getByTestId('domain-display')).toBeInTheDocument();
+ });
+
+ it('renders empty Domain row when no domains exist and entity requires domain', () => {
+ (
+ searchClassBase.getListOfEntitiesWithoutDomain as jest.Mock
+ ).mockReturnValue([]);
+
+ renderCard({ domains: [] });
+
+ expect(screen.queryByTestId('domain-icon')).not.toBeInTheDocument();
+
+ expect(screen.getByTestId('Domain')).toBeInTheDocument();
+ });
+
+ it('does not render Domain when entityType is excluded from domains', () => {
+ (
+ searchClassBase.getListOfEntitiesWithoutDomain as jest.Mock
+ ).mockReturnValue(['table']);
+
+ renderCard({ domains: [] });
+
+ expect(screen.queryByText('Domain')).not.toBeInTheDocument();
+ });
+});
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ExploreV1/ExploreSearchCard/ExploreSearchCard.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ExploreV1/ExploreSearchCard/ExploreSearchCard.tsx
index e02b99fdfdd..d772743d333 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/ExploreV1/ExploreSearchCard/ExploreSearchCard.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/ExploreV1/ExploreSearchCard/ExploreSearchCard.tsx
@@ -30,13 +30,13 @@ import { Table } from '../../../generated/entity/data/table';
import { EntityReference } from '../../../generated/entity/type';
import { TagLabel } from '../../../generated/tests/testCase';
import { AssetCertification } from '../../../generated/type/assetCertification';
-import { getEntityName, highlightSearchText } from '../../../utils/EntityUtils';
-import { getDomainPath } from '../../../utils/RouterUtils';
+import { highlightSearchText } from '../../../utils/EntityUtils';
import searchClassBase from '../../../utils/SearchClassBase';
import { stringToHTML } from '../../../utils/StringsUtils';
import { getUsagePercentile } from '../../../utils/TableUtils';
import { useRequiredParams } from '../../../utils/useRequiredParams';
import CertificationTag from '../../common/CertificationTag/CertificationTag';
+import { DomainDisplay } from '../../common/DomainDisplay/DomainDisplay.component';
import { OwnerLabel } from '../../common/OwnerLabel/OwnerLabel.component';
import TitleBreadcrumb from '../../common/TitleBreadcrumb/TitleBreadcrumb.component';
import TableDataCardBody from '../../Database/TableDataCardBody/TableDataCardBody';
@@ -84,14 +84,13 @@ const ExploreSearchCard: React.FC = forwardRef<
);
const _otherDetails: ExtraInfo[] = [
- ...(source?.domains
- ? source.domains.map((domain) => ({
- key: 'Domains',
- value: getDomainPath(domain.fullyQualifiedName) ?? '',
- placeholderText: getEntityName(domain),
- isLink: true,
- openInNewTab: false,
- }))
+ ...(source?.domains && source.domains.length > 0
+ ? [
+ {
+ key: 'Domains',
+ value: ,
+ },
+ ]
: !searchClassBase
.getListOfEntitiesWithoutDomain()
.includes(source?.entityType ?? '')
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
new file mode 100644
index 00000000000..f1a464dcb7e
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/components/common/DomainDisplay/DomainDisplay.component.tsx
@@ -0,0 +1,126 @@
+/*
+ * 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 { Dropdown, Typography } from 'antd';
+import { Link } from 'react-router-dom';
+import { ReactComponent as DomainIcon } from '../../../assets/svg/ic-domain.svg';
+import { EntityReference } from '../../../generated/entity/type';
+import { getEntityName } from '../../../utils/EntityUtils';
+import { getDomainPath } from '../../../utils/RouterUtils';
+
+interface DomainDisplayProps {
+ domains: EntityReference[];
+ showIcon?: boolean;
+ className?: string;
+}
+
+const DomainLink: React.FC<{
+ domain: EntityReference;
+}> = ({ domain }) => (
+ <>
+
+
+ {getEntityName(domain)}
+
+
+ >
+);
+
+export const DomainDisplay = ({
+ domains,
+ showIcon = true,
+ className = '',
+}: DomainDisplayProps) => {
+ if (!domains || domains.length === 0) {
+ return null;
+ }
+
+ if (domains.length > 1) {
+ const firstDomain = domains[0];
+ const remainingDomains = domains.slice(1);
+ const remainingCount = remainingDomains.length;
+
+ const dropdownItems = remainingDomains.map((domain, index) => ({
+ key: index,
+ label: (
+
+
+
+
+ {getEntityName(domain)}
+
+
+
+ ),
+ }));
+
+ return (
+
+ {showIcon && (
+
+
+
+ )}
+
+
+
+
+
+
+
+ {`+${remainingCount}`}
+
+
+
+
+
+ );
+ }
+
+ return (
+
+ {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
new file mode 100644
index 00000000000..78987b4a202
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/components/common/DomainDisplay/DomainDisplay.test.tsx
@@ -0,0 +1,269 @@
+/*
+ * 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 '@testing-library/jest-dom';
+import { render, screen } from '@testing-library/react';
+import { MemoryRouter } from 'react-router-dom';
+import { EntityReference } from '../../../generated/entity/type';
+import { DomainDisplay } from './DomainDisplay.component';
+
+jest.mock('../../../utils/EntityUtils', () => ({
+ getEntityName: jest
+ .fn()
+ .mockImplementation((entity) => entity?.name || 'Unknown'),
+}));
+
+jest.mock('../../../utils/RouterUtils', () => ({
+ getDomainPath: jest
+ .fn()
+ .mockImplementation((fqn: string) => `/domain/${fqn}`),
+}));
+
+jest.mock('../../../assets/svg/ic-domain.svg', () => ({
+ ReactComponent: () =>