Revamp: improve data asset header layout and component styling (#23331)

* Revamp: improve data asset header layout and component styling

* Remove style

* fix minor styling

* Fix integration test

* fix no data placeholder

* Addressed PR comments

* fix integration test

* Fix e2e test

* Addressed comments

* Fix domain count overflow
This commit is contained in:
Anujkumar Yadav 2025-09-17 06:02:14 +05:30 committed by GitHub
parent 73eb1fd0d9
commit ba61af7465
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 400 additions and 97 deletions

View File

@ -1024,13 +1024,11 @@ test.describe('Bulk Import Export', () => {
page.getByTestId('glossary-container').getByTestId('add-tag') page.getByTestId('glossary-container').getByTestId('add-tag')
).toBeVisible(); ).toBeVisible();
await expect(page.getByTestId('Tier')).toContainText('No Tier'); await expect(page.getByTestId('Tier')).toContainText('--');
await expect(page.getByTestId('certification-label')).toContainText( await expect(page.getByTestId('certification-label')).toContainText('--');
'No Certification'
);
await expect(page.getByTestId('owner-label')).toContainText('No Owners'); await expect(page.getByTestId('owner-label')).toContainText('--');
await expect(page.getByTestId('no-domain-text')).toBeVisible(); await expect(page.getByTestId('no-domain-text')).toBeVisible();
}); });

View File

@ -107,7 +107,7 @@ test('Logical TestSuite', async ({ page }) => {
await assignDomain(page, domain1.responseData); await assignDomain(page, domain1.responseData);
// TODO: Add domain update // TODO: Add domain update
// await updateDomain(page, domain2.responseData); // await updateDomain(page, domain2.responseData);
await removeDomain(page, domain1.responseData); await removeDomain(page, domain1.responseData, false);
}); });
await test.step( await test.step(

View File

@ -262,7 +262,8 @@ export const updateDomain = async (
export const removeDomain = async ( export const removeDomain = async (
page: Page, page: Page,
domain: { name: string; displayName: string; fullyQualifiedName?: string } domain: { name: string; displayName: string; fullyQualifiedName?: string },
showDashPlaceholder = true
) => { ) => {
await page.getByTestId('add-domain').click(); await page.getByTestId('add-domain').click();
await page.waitForSelector('[data-testid="loader"]', { state: 'detached' }); await page.waitForSelector('[data-testid="loader"]', { state: 'detached' });
@ -280,7 +281,9 @@ export const removeDomain = async (
await patchReq; await patchReq;
await page.waitForSelector('[data-testid="loader"]', { state: 'detached' }); await page.waitForSelector('[data-testid="loader"]', { state: 'detached' });
await expect(page.getByTestId('no-domain-text')).toContainText('No Domains'); await expect(page.getByTestId('no-domain-text')).toContainText(
showDashPlaceholder ? '--' : 'No Domains'
);
}; };
export const assignDataProduct = async ( export const assignDataProduct = async (

View File

@ -478,7 +478,7 @@ export const removeTier = async (page: Page, endpoint: string) => {
await patchRequest; await patchRequest;
await clickOutside(page); await clickOutside(page);
await expect(page.getByTestId('Tier')).toContainText('No Tier'); await expect(page.getByTestId('Tier')).toContainText('--');
}; };
export const assignCertification = async ( export const assignCertification = async (
@ -509,9 +509,7 @@ export const removeCertification = async (page: Page, endpoint: string) => {
await patchRequest; await patchRequest;
await clickOutside(page); await clickOutside(page);
await expect(page.getByTestId('certification-label')).toContainText( await expect(page.getByTestId('certification-label')).toContainText('--');
'No Certification'
);
}; };
export const updateDescription = async ( export const updateDescription = async (

View File

@ -32,6 +32,7 @@ import { OwnerLabel } from '../../../components/common/OwnerLabel/OwnerLabel.com
import TierCard from '../../../components/common/TierCard/TierCard'; import TierCard from '../../../components/common/TierCard/TierCard';
import EntityHeaderTitle from '../../../components/Entity/EntityHeaderTitle/EntityHeaderTitle.component'; import EntityHeaderTitle from '../../../components/Entity/EntityHeaderTitle/EntityHeaderTitle.component';
import { AUTO_PILOT_APP_NAME } from '../../../constants/Applications.constant'; import { AUTO_PILOT_APP_NAME } from '../../../constants/Applications.constant';
import { NO_DATA_PLACEHOLDER } from '../../../constants/constants';
import { import {
EXCLUDE_AUTO_PILOT_SERVICE_TYPES, EXCLUDE_AUTO_PILOT_SERVICE_TYPES,
SERVICE_TYPES, SERVICE_TYPES,
@ -59,7 +60,6 @@ import { getDataQualityLineage } from '../../../rest/lineageAPI';
import { getContainerByName } from '../../../rest/storageAPI'; import { getContainerByName } from '../../../rest/storageAPI';
import { import {
getDataAssetsHeaderInfo, getDataAssetsHeaderInfo,
getEntityExtraInfoLength,
isDataAssetsWithServiceField, isDataAssetsWithServiceField,
} from '../../../utils/DataAssetsHeader.utils'; } from '../../../utils/DataAssetsHeader.utils';
import { getDataContractStatusIcon } from '../../../utils/DataContract/DataContractUtils'; import { getDataContractStatusIcon } from '../../../utils/DataContract/DataContractUtils';
@ -324,14 +324,6 @@ export const DataAssetsHeader = ({
[entityType, dataAsset, entityName, parentContainers] [entityType, dataAsset, entityName, parentContainers]
); );
const showCompressedExtraInfoItems = useMemo(
() =>
entityType === EntityType.METRIC
? false
: getEntityExtraInfoLength(extraInfo) <= 1,
[extraInfo, entityType]
);
const handleOpenTaskClick = () => { const handleOpenTaskClick = () => {
if (!dataAsset.fullyQualifiedName) { if (!dataAsset.fullyQualifiedName) {
return; return;
@ -663,15 +655,14 @@ export const DataAssetsHeader = ({
<Col span={24}> <Col span={24}>
<div <div
className={classNames('data-asset-header-metadata', { className="data-asset-header-metadata"
'data-asset-header-less-items': showCompressedExtraInfoItems,
})}
data-testid="data-asset-header-metadata"> data-testid="data-asset-header-metadata">
{showDomain && ( {showDomain && (
<> <>
<DomainLabel <DomainLabel
headerLayout headerLayout
multiple multiple
showDashPlaceholder
afterDomainUpdateAction={afterDomainUpdateAction} afterDomainUpdateAction={afterDomainUpdateAction}
domains={(dataAsset as EntitiesWithDomainField).domains} domains={(dataAsset as EntitiesWithDomainField).domains}
entityFqn={dataAsset.fullyQualifiedName ?? ''} entityFqn={dataAsset.fullyQualifiedName ?? ''}
@ -687,6 +678,8 @@ export const DataAssetsHeader = ({
</> </>
)} )}
<OwnerLabel <OwnerLabel
showDashPlaceholder
avatarSize={24}
hasPermission={editOwnerPermission} hasPermission={editOwnerPermission}
isCompactView={false} isCompactView={false}
maxVisibleOwners={4} maxVisibleOwners={4}
@ -697,7 +690,7 @@ export const DataAssetsHeader = ({
{tierSuggestionRender ?? ( {tierSuggestionRender ?? (
<TierCard currentTier={tier?.tagFQN} updateTier={onTierUpdate}> <TierCard currentTier={tier?.tagFQN} updateTier={onTierUpdate}>
<Space <Space
className="d-flex tier-container align-start" className="d-flex align-start"
data-testid="header-tier-container"> data-testid="header-tier-container">
{tier ? ( {tier ? (
<div className="d-flex flex-col gap-2"> <div className="d-flex flex-col gap-2">
@ -719,6 +712,7 @@ export const DataAssetsHeader = ({
</div> </div>
<TagsV1 <TagsV1
hideIcon
startWith={TAG_START_WITH.SOURCE_ICON} startWith={TAG_START_WITH.SOURCE_ICON}
tag={tier} tag={tier}
tagProps={{ tagProps={{
@ -746,9 +740,7 @@ export const DataAssetsHeader = ({
<span <span
className="font-medium no-tier-text text-sm" className="font-medium no-tier-text text-sm"
data-testid="Tier"> data-testid="Tier">
{t('label.no-entity', { {NO_DATA_PLACEHOLDER}
entity: t('label.tier'),
})}
</span> </span>
</div> </div>
)} )}
@ -819,9 +811,7 @@ export const DataAssetsHeader = ({
certification={(dataAsset as Table).certification!} certification={(dataAsset as Table).certification!}
/> />
) : ( ) : (
t('label.no-entity', { NO_DATA_PLACEHOLDER
entity: t('label.certification'),
})
)} )}
</div> </div>
</Typography.Text> </Typography.Text>

View File

@ -269,7 +269,7 @@ describe('DataAssetsHeader component', () => {
it('should not render the Tier data if not present', () => { it('should not render the Tier data if not present', () => {
render(<DataAssetsHeader {...mockProps} dataAsset={mockProps.dataAsset} />); render(<DataAssetsHeader {...mockProps} dataAsset={mockProps.dataAsset} />);
expect(screen.getByTestId('Tier')).toContainHTML('label.no-entity'); expect(screen.getByTestId('Tier')).toContainHTML('--');
}); });
it('should not call getDataQualityLineage, if isDqAlertSupported and alert supported is false', () => { it('should not call getDataQualityLineage, if isDqAlertSupported and alert supported is false', () => {
@ -389,9 +389,7 @@ describe('DataAssetsHeader component', () => {
// Test without certification when serviceCategory is undefined // Test without certification when serviceCategory is undefined
render(<DataAssetsHeader {...mockProps} />); render(<DataAssetsHeader {...mockProps} />);
expect(screen.getByTestId('certification-label')).toContainHTML( expect(screen.getByTestId('certification-label')).toContainHTML('--');
'label.no-entity'
);
// Reset the mock to original value // Reset the mock to original value
useRequiredParamsMock.mockReturnValue({ useRequiredParamsMock.mockReturnValue({

View File

@ -20,18 +20,9 @@
gap: 16px; gap: 16px;
display: flex; display: flex;
background-color: @background-color; background-color: @background-color;
justify-content: space-evenly;
flex-wrap: wrap; flex-wrap: wrap;
} }
.data-asset-header-less-items {
justify-content: initial;
.ant-divider {
margin: 0 20px 0 100px;
}
}
.ant-space-item { .ant-space-item {
.entity-no-tier { .entity-no-tier {
color: @primary-heading-color; color: @primary-heading-color;

View File

@ -33,6 +33,7 @@ import { TagsV1Props } from './TagsV1.interface';
import './tagsV1.less'; import './tagsV1.less';
const TagsV1 = ({ const TagsV1 = ({
hideIcon,
tag, tag,
startWith, startWith,
className, className,
@ -131,16 +132,13 @@ const TagsV1 = ({
return ''; return '';
}, [newLook, tag.style?.color]); }, [newLook, tag.style?.color]);
const tagContent = useMemo( const renderIcon = useMemo(() => {
() => ( if (hideIcon) {
<div className="d-flex w-full h-full"> return null;
{tagColorBar} }
<div
className={classNames( if (tag.style?.iconURL) {
'd-flex items-center p-x-xs w-full', return (
tagChipStyleClass
)}>
{tag.style?.iconURL ? (
<img <img
className="m-r-xss" className="m-r-xss"
data-testid="icon" data-testid="icon"
@ -148,17 +146,29 @@ const TagsV1 = ({
src={tag.style.iconURL} src={tag.style.iconURL}
width={12} width={12}
/> />
) : ( );
startIcon }
)}
<Typography.Paragraph return startIcon;
ellipsis }, [hideIcon, tag.style?.iconURL, startIcon]);
className="m-0 tags-label"
const tagContent = useMemo(
() => (
<div className="d-flex w-full">
{tagColorBar}
<div
className={classNames(
'd-flex items-center p-x-xs w-full tag-content-container',
tagChipStyleClass
)}>
{renderIcon}
<Typography.Text
className="m-0 tags-label text-truncate truncate w-max-full"
data-testid={`tag-${tag.tagFQN}`} data-testid={`tag-${tag.tagFQN}`}
ellipsis={{ tooltip: false }}
style={{ color: tag.style?.color }}> style={{ color: tag.style?.color }}>
{tagName} {tagName}
</Typography.Paragraph> </Typography.Text>
</div> </div>
</div> </div>
), ),
@ -205,11 +215,12 @@ const TagsV1 = ({
<Tag <Tag
className="tag-chip tag-chip-add-button" className="tag-chip tag-chip-add-button"
icon={<PlusIcon height={16} name="plus" width={16} />}> icon={<PlusIcon height={16} name="plus" width={16} />}>
<Typography.Paragraph <Typography.Text
className="m-0 text-xs font-medium text-primary" className="m-0 text-xs font-medium text-primary text-truncate truncate w-max-full"
data-testid="add-tag"> data-testid="add-tag"
ellipsis={{ tooltip: false }}>
{getTagDisplay(tagName)} {getTagDisplay(tagName)}
</Typography.Paragraph> </Typography.Text>
</Tag> </Tag>
), ),
[tagName] [tagName]

View File

@ -21,6 +21,7 @@ export interface DataTestId {
} }
export type TagsV1Props = { export type TagsV1Props = {
hideIcon?: boolean;
tag: TagLabel | HighlightedTagLabel; tag: TagLabel | HighlightedTagLabel;
startWith: TAG_START_WITH; startWith: TAG_START_WITH;
showOnlyName?: boolean; showOnlyName?: boolean;

View File

@ -37,11 +37,12 @@
.ant-tag.tag-chip-content { .ant-tag.tag-chip-content {
width: max-content; width: max-content;
height: 30px; max-width: 200px;
margin: 0px; margin: 0px;
border: none; border: none;
padding: 0; padding: 2px 0;
background: @tag-background-color; background: @tag-background-color;
border-radius: @border-rad-xs;
&.diff-added { &.diff-added {
background: @success-background-color; background: @success-background-color;
@ -90,8 +91,15 @@
} }
.tags-label { .tags-label {
display: inline-block; display: block;
font-size: 12px; font-size: 12px;
font-weight: 500; font-weight: 500;
color: inherit; color: inherit;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.tag-content-container {
min-width: 0;
} }

View File

@ -57,6 +57,7 @@ const CertificationTag = ({
backgroundColor: certification.tagLabel.style?.color backgroundColor: certification.tagLabel.style?.color
? certification.tagLabel.style?.color + '33' ? certification.tagLabel.style?.color + '33'
: '#f8f8f8', : '#f8f8f8',
padding: '2px 6px',
} }
: {}; : {};

View File

@ -19,7 +19,10 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { ReactComponent as DomainIcon } from '../../../assets/svg/ic-domain.svg'; import { ReactComponent as DomainIcon } from '../../../assets/svg/ic-domain.svg';
import { ReactComponent as InheritIcon } from '../../../assets/svg/ic-inherit.svg'; import { ReactComponent as InheritIcon } from '../../../assets/svg/ic-inherit.svg';
import { DE_ACTIVE_COLOR } from '../../../constants/constants'; import {
DE_ACTIVE_COLOR,
NO_DATA_PLACEHOLDER,
} from '../../../constants/constants';
import { EntityReference } from '../../../generated/entity/type'; import { EntityReference } from '../../../generated/entity/type';
import { import {
getAPIfromSource, getAPIfromSource,
@ -34,6 +37,7 @@ import './domain-label.less';
import { DomainLabelProps } from './DomainLabel.interface'; import { DomainLabelProps } from './DomainLabel.interface';
export const DomainLabel = ({ export const DomainLabel = ({
showDashPlaceholder,
afterDomainUpdateAction, afterDomainUpdateAction,
hasPermission, hasPermission,
domains, domains,
@ -50,6 +54,12 @@ export const DomainLabel = ({
const { t } = useTranslation(); const { t } = useTranslation();
const [activeDomain, setActiveDomain] = useState<EntityReference[]>([]); const [activeDomain, setActiveDomain] = useState<EntityReference[]>([]);
const defaultDomainText = useMemo(() => {
return showDashPlaceholder
? NO_DATA_PLACEHOLDER
: t('label.no-entity', { entity: t('label.domain-plural') });
}, [showDashPlaceholder]);
const handleDomainSave = useCallback( const handleDomainSave = useCallback(
async (selectedDomain: EntityReference | EntityReference[]) => { async (selectedDomain: EntityReference | EntityReference[]) => {
const entityDetails = getEntityAPIfromSource(entityType as AssetsUnion)( const entityDetails = getEntityAPIfromSource(entityType as AssetsUnion)(
@ -166,8 +176,12 @@ export const DomainLabel = ({
})), })),
className: 'domain-tooltip-list', className: 'domain-tooltip-list',
}}> }}>
<Typography.Text className="domain-count-button flex-center text-sm font-medium"> <Typography.Text
<span>+{remainingCount}</span> className={`flex-center cursor-pointer align-middle ant-typography-secondary domain-count-button ${
remainingCount <= 9 ? 'h-6 w-6' : ''
}`}
data-testid="domain-count-button">
<span className="ant-typography domain-count-label">{`+${remainingCount}`}</span>
</Typography.Text> </Typography.Text>
</Dropdown> </Dropdown>
</div> </div>
@ -185,7 +199,7 @@ export const DomainLabel = ({
textClassName textClassName
)} )}
data-testid="no-domain-text"> data-testid="no-domain-text">
{t('label.no-entity', { entity: t('label.domain-plural') })} {defaultDomainText}
</Typography.Text> </Typography.Text>
); );
}, [ }, [
@ -226,7 +240,7 @@ export const DomainLabel = ({
<Typography.Text className="domain-link right-panel-label m-r-xss"> <Typography.Text className="domain-link right-panel-label m-r-xss">
{activeDomain.length > 0 {activeDomain.length > 0
? t('label.domain-plural') ? t('label.domain-plural')
: t('label.no-entity', { entity: t('label.domain-plural') })} : defaultDomainText}
</Typography.Text> </Typography.Text>
)} )}
{selectableList} {selectableList}
@ -240,7 +254,7 @@ export const DomainLabel = ({
} }
return ( return (
<div className="d-flex flex-col domain-label-container gap-2 justify-start"> <div className="d-flex flex-col gap-2 justify-start">
{headerLayout && ( {headerLayout && (
<div <div
className="d-flex text-sm gap-1 font-medium items-center " className="d-flex text-sm gap-1 font-medium items-center "

View File

@ -17,6 +17,7 @@ import { User } from '../../../generated/entity/teams/user';
import { EntityReference } from '../../../generated/entity/type'; import { EntityReference } from '../../../generated/entity/type';
export type DomainLabelProps = { export type DomainLabelProps = {
showDashPlaceholder?: boolean;
afterDomainUpdateAction?: (asset: DataAssetWithDomains) => void; afterDomainUpdateAction?: (asset: DataAssetWithDomains) => void;
hasPermission?: boolean; hasPermission?: boolean;
domains: EntityReference[] | undefined; domains: EntityReference[] | undefined;

View File

@ -0,0 +1,273 @@
/*
* 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 { EntityType } from '../../../enums/entity.enum';
import { EntityReference } from '../../../generated/entity/type';
import { DomainLabel } from './DomainLabel.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('../../../utils/Assets/AssetsUtils', () => ({
getAPIfromSource: jest.fn().mockReturnValue(jest.fn()),
getEntityAPifromSource: jest.fn().mockReturnValue(jest.fn()),
}));
jest.mock('../../../utils/DomainUtils', () => ({
renderDomainLink: jest
.fn()
.mockImplementation((domain, displayName, className) => (
<a
className={`domain-link ${className || ''}`}
data-testid="domain-link"
href={`/domain/${domain.fullyQualifiedName}`}>
{displayName || domain.name}
</a>
)),
}));
jest.mock('../../../utils/ToastUtils', () => ({
showErrorToast: jest.fn(),
}));
jest.mock('../../../assets/svg/ic-domain.svg', () => ({
ReactComponent: () => <div data-testid="domain-icon">Domain Icon</div>,
}));
jest.mock('../../../assets/svg/ic-inherit.svg', () => ({
ReactComponent: () => <div data-testid="inherit-icon">Inherit Icon</div>,
}));
jest.mock('../DomainSelectableList/DomainSelectableList.component', () => ({
__esModule: true,
default: ({ onUpdate, selectedDomain }: any) => (
<button
data-testid="domain-selectable-list"
onClick={() => onUpdate && onUpdate(selectedDomain)}>
Select Domain
</button>
),
}));
// Mock data
const mockDomain1: EntityReference = {
id: 'domain-1',
fullyQualifiedName: 'domain.one',
name: 'Domain One',
type: 'domain',
};
const mockDomain2: EntityReference = {
id: 'domain-2',
fullyQualifiedName: 'domain.two',
name: 'Domain Two',
type: 'domain',
};
const mockInheritedDomain: EntityReference = {
id: 'domain-inherited',
fullyQualifiedName: 'domain.inherited',
name: 'Inherited Domain',
type: 'domain',
inherited: true,
};
const defaultProps = {
domains: [mockDomain1],
entityType: EntityType.TABLE,
entityFqn: 'test.table',
entityId: 'test-id',
};
const renderDomainLabel = (props: any = {}) =>
render(
<MemoryRouter>
<DomainLabel {...defaultProps} {...props} />
</MemoryRouter>
);
describe('DomainLabel Component', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('should render single domain correctly', () => {
renderDomainLabel({ domains: [mockDomain1] });
expect(screen.getByTestId('domain-link')).toBeInTheDocument();
expect(screen.getByText('Domain One')).toBeInTheDocument();
});
it('should render multiple domains with dropdown when multiple and headerLayout are true', () => {
renderDomainLabel({
domains: [mockDomain1, mockDomain2],
multiple: true,
headerLayout: true,
});
expect(screen.getByText('Domain One')).toBeInTheDocument();
expect(screen.getByTestId('domain-count-button')).toBeInTheDocument();
expect(screen.getByText('+1')).toBeInTheDocument();
});
it('should render all domains when multiple is true but headerLayout is false', () => {
renderDomainLabel({
domains: [mockDomain1, mockDomain2],
multiple: true,
headerLayout: false,
});
expect(screen.getByText('Domain One')).toBeInTheDocument();
expect(screen.getByText('Domain Two')).toBeInTheDocument();
expect(screen.queryByTestId('domain-count-button')).not.toBeInTheDocument();
});
it('should render "No Domains" text when domains array is empty', () => {
renderDomainLabel({ domains: [] });
expect(screen.getByTestId('no-domain-text')).toBeInTheDocument();
expect(screen.getByText('label.no-entity')).toBeInTheDocument();
});
it('should render "No Domains" text when domains is undefined', () => {
renderDomainLabel({ domains: undefined });
expect(screen.getByTestId('no-domain-text')).toBeInTheDocument();
expect(screen.getByText('label.no-entity')).toBeInTheDocument();
});
it('should render inherited domain with inherit icon', () => {
renderDomainLabel({ domains: [mockInheritedDomain] });
expect(screen.getByText('Inherited Domain')).toBeInTheDocument();
});
it('should handle domain with missing name', () => {
const domainWithoutName = {
...mockDomain1,
name: undefined,
};
renderDomainLabel({ domains: [domainWithoutName] });
expect(screen.getByTestId('domain-link')).toBeInTheDocument();
});
it('should render domain heading when showDomainHeading is true', () => {
renderDomainLabel({ showDomainHeading: true });
expect(screen.getByTestId('header-domain-container')).toBeInTheDocument();
expect(screen.getByText('label.domain-plural')).toBeInTheDocument();
});
it('should not render domain heading when showDomainHeading is false', () => {
renderDomainLabel({ showDomainHeading: false });
expect(screen.queryByText('Domains')).not.toBeInTheDocument();
});
it('should not show domain icon for single domain in header layout', () => {
renderDomainLabel({
headerLayout: true,
multiple: false,
domains: [mockDomain1],
});
expect(screen.queryByTestId('domain-icon')).not.toBeInTheDocument();
});
it('should render DomainSelectableList when hasPermission is true', () => {
renderDomainLabel({ hasPermission: true });
expect(screen.getByTestId('domain-selectable-list')).toBeInTheDocument();
});
it('should not render DomainSelectableList when hasPermission is false', () => {
renderDomainLabel({ hasPermission: false });
expect(
screen.queryByTestId('domain-selectable-list')
).not.toBeInTheDocument();
});
it('should not render DomainSelectableList when hasPermission is undefined', () => {
renderDomainLabel({ hasPermission: undefined });
expect(
screen.queryByTestId('domain-selectable-list')
).not.toBeInTheDocument();
});
it('should handle domains as single object instead of array', () => {
renderDomainLabel({ domains: mockDomain1 });
expect(screen.getByText('Domain One')).toBeInTheDocument();
});
it('should handle empty domain object', () => {
const emptyDomain = {} as EntityReference;
renderDomainLabel({ domains: [emptyDomain] });
expect(screen.getByTestId('domain-link')).toBeInTheDocument();
});
it('should handle domain with empty fullyQualifiedName', () => {
const domainWithEmptyFQN = {
...mockDomain1,
fullyQualifiedName: '',
};
renderDomainLabel({ domains: [domainWithEmptyFQN] });
expect(screen.getByTestId('domain-link')).toBeInTheDocument();
});
it('should handle mixed domain types (inherited and non-inherited)', () => {
renderDomainLabel({
domains: [mockDomain1, mockInheritedDomain],
multiple: true,
headerLayout: true,
});
expect(screen.getByText('Domain One')).toBeInTheDocument();
expect(screen.getByTestId('domain-count-button')).toBeInTheDocument();
});
it('should have proper test ID for domain count button', () => {
renderDomainLabel({
domains: [mockDomain1, mockDomain2],
multiple: true,
headerLayout: true,
});
expect(screen.getByTestId('domain-count-button')).toBeInTheDocument();
});
it('should have proper test ID for no domain text', () => {
renderDomainLabel({ domains: [] });
expect(screen.getByTestId('no-domain-text')).toBeInTheDocument();
});
});

View File

@ -13,6 +13,7 @@
import { EntityReference } from '../../../generated/tests/testCase'; import { EntityReference } from '../../../generated/tests/testCase';
export interface NoOwnerFoundProps { export interface NoOwnerFoundProps {
showDashPlaceholder?: boolean;
isCompactView: boolean; isCompactView: boolean;
placeHolder?: string; placeHolder?: string;
showLabel?: boolean; showLabel?: boolean;

View File

@ -13,13 +13,15 @@
import Icon from '@ant-design/icons'; import Icon from '@ant-design/icons';
import { Typography } from 'antd'; import { Typography } from 'antd';
import classNames from 'classnames'; import classNames from 'classnames';
import React from 'react'; import React, { useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { ReactComponent as IconUser } from '../../../assets/svg/user.svg'; import { ReactComponent as IconUser } from '../../../assets/svg/user.svg';
import { NO_DATA_PLACEHOLDER } from '../../../constants/constants';
import { UserTeamSelectableList } from '../UserTeamSelectableList/UserTeamSelectableList.component'; import { UserTeamSelectableList } from '../UserTeamSelectableList/UserTeamSelectableList.component';
import { NoOwnerFoundProps } from './NoOwnerFound.interface'; import { NoOwnerFoundProps } from './NoOwnerFound.interface';
export const NoOwnerFound: React.FC<NoOwnerFoundProps> = ({ export const NoOwnerFound: React.FC<NoOwnerFoundProps> = ({
showDashPlaceholder,
isCompactView, isCompactView,
showLabel = true, showLabel = true,
placeHolder, placeHolder,
@ -32,6 +34,22 @@ export const NoOwnerFound: React.FC<NoOwnerFoundProps> = ({
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const ownerPlaceholder = useMemo(() => {
const defaultPlaceholder = showDashPlaceholder
? NO_DATA_PLACEHOLDER
: t('label.no-entity', { entity: t('label.owner-plural') });
if (placeHolder) {
if (showLabel) {
return defaultPlaceholder;
}
return placeHolder;
}
return defaultPlaceholder;
}, [placeHolder, showLabel, showDashPlaceholder]);
return ( return (
<div <div
className={classNames( className={classNames(
@ -86,11 +104,7 @@ export const NoOwnerFound: React.FC<NoOwnerFoundProps> = ({
{!isCompactView && ( {!isCompactView && (
<div className="no-owner-text text-sm font-medium"> <div className="no-owner-text text-sm font-medium">
{placeHolder {ownerPlaceholder}
? showLabel
? t('label.no-entity', { entity: placeHolder })
: placeHolder
: t('label.no-entity', { entity: t('label.owner-plural') })}
</div> </div>
)} )}
</div> </div>

View File

@ -27,6 +27,7 @@ import './owner-label.less';
import { OwnerLabelProps } from './OwnerLabel.interface'; import { OwnerLabelProps } from './OwnerLabel.interface';
export const OwnerLabel = ({ export const OwnerLabel = ({
showDashPlaceholder,
owners = [], owners = [],
showLabel = true, showLabel = true,
className, className,
@ -102,8 +103,8 @@ export const OwnerLabel = ({
.slice(maxVisibleOwners); .slice(maxVisibleOwners);
const renderMultipleType = useMemo(() => { const renderMultipleType = useMemo(() => {
return ( return (
<div className="flex-wrap w-max-full d-flex relative items-center"> <div className="w-max-full d-flex relative items-center">
<div className="flex w-full gap-2 flex-wrap relative"> <div className="flex w-full gap-2 relative">
{showMultipleTypeTeam.map((owner, index) => ( {showMultipleTypeTeam.map((owner, index) => (
<div className="w-max-full" key={owner.id}> <div className="w-max-full" key={owner.id}>
<OwnerItem <OwnerItem
@ -197,6 +198,7 @@ export const OwnerLabel = ({
multiple={multiple} multiple={multiple}
owners={owners} owners={owners}
placeHolder={placeHolder} placeHolder={placeHolder}
showDashPlaceholder={showDashPlaceholder}
showLabel={showLabel} showLabel={showLabel}
tooltipText={tooltipText} tooltipText={tooltipText}
onUpdate={onUpdate} onUpdate={onUpdate}
@ -211,7 +213,7 @@ export const OwnerLabel = ({
return ( return (
<div <div
className={classNames({ className={classNames({
'owner-label-container w-full d-flex flex-col items-start flex-start': 'owner-label-container d-flex flex-col items-start flex-start':
!isCompactView, !isCompactView,
'd-flex owner-label-heading gap-2 items-center': isCompactView, 'd-flex owner-label-heading gap-2 items-center': isCompactView,
})} })}
@ -261,7 +263,6 @@ export const OwnerLabel = ({
</div> </div>
{showMoreButton && !isCompactView && ( {showMoreButton && !isCompactView && (
<div className="m-l-xs">
<OwnerReveal <OwnerReveal
avatarSize={isCompactView ? 24 : avatarSize} avatarSize={isCompactView ? 24 : avatarSize}
isCompactView={isCompactView} isCompactView={isCompactView}
@ -272,7 +273,6 @@ export const OwnerLabel = ({
setShowAllOwners={setShowAllOwners} setShowAllOwners={setShowAllOwners}
showAllOwners={showAllOwners} showAllOwners={showAllOwners}
/> />
</div>
)} )}
</div> </div>
{isCompactView && onUpdate && ( {isCompactView && onUpdate && (

View File

@ -14,6 +14,7 @@ import { ReactNode } from 'react';
import { EntityReference } from '../../../generated/tests/testCase'; import { EntityReference } from '../../../generated/tests/testCase';
export interface OwnerLabelProps { export interface OwnerLabelProps {
showDashPlaceholder?: boolean;
owners?: EntityReference[]; owners?: EntityReference[];
showLabel?: boolean; showLabel?: boolean;
className?: string; className?: string;

View File

@ -104,7 +104,7 @@ export const ExtraInfoLabel = ({
<span className="extra-info-label-heading">{label}</span> <span className="extra-info-label-heading">{label}</span>
)} )}
<div className={classNames('font-medium extra-info-value')}> <div className={classNames('font-medium extra-info-value')}>
{value} {value ?? NO_DATA_PLACEHOLDER}
</div> </div>
</Typography.Text> </Typography.Text>
</div> </div>