mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-09-30 19:36:41 +00:00
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:
parent
73eb1fd0d9
commit
ba61af7465
@ -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();
|
||||||
});
|
});
|
||||||
|
@ -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(
|
||||||
|
@ -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 (
|
||||||
|
@ -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 (
|
||||||
|
@ -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>
|
||||||
|
@ -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({
|
||||||
|
@ -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;
|
||||||
|
@ -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]
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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',
|
||||||
}
|
}
|
||||||
: {};
|
: {};
|
||||||
|
|
||||||
|
@ -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 "
|
||||||
|
@ -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;
|
||||||
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
@ -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;
|
||||||
|
@ -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>
|
||||||
|
@ -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 && (
|
||||||
|
@ -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;
|
||||||
|
@ -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>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user