feat: Create reusable EntityAvatar component for consistent icon rendering (#23609)

- Created EntityAvatar component to centralize entity icon rendering logic
- Updated domain and data product detail pages to use EntityAvatar
- Updated useCellRenderer to use EntityAvatar for listing pages
- Supports custom icons via style.iconURL and custom colors via style.color
- Handles URL-based icons, icon names from ICON_MAP, and default icons

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Satish <satish@Satishs-MacBook-Pro.local>
Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
satish 2025-09-29 14:36:00 +05:30 committed by GitHub
parent 2b80a885a0
commit 7ffb6df57a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 143 additions and 127 deletions

View File

@ -21,12 +21,10 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import { ReactComponent as EditIcon } from '../../../assets/svg/edit-new.svg';
import { ReactComponent as DataProductIcon } from '../../../assets/svg/ic-data-product.svg';
import { ReactComponent as DeleteIcon } from '../../../assets/svg/ic-delete.svg';
import { ReactComponent as VersionIcon } from '../../../assets/svg/ic-version.svg';
import { ReactComponent as IconDropdown } from '../../../assets/svg/menu.svg';
import { ReactComponent as StyleIcon } from '../../../assets/svg/style.svg';
import { DE_ACTIVE_COLOR } from '../../../constants/constants';
import { CustomizeEntityType } from '../../../constants/Customize.constants';
import { EntityField } from '../../../constants/Feeds.constants';
import { usePermissionProvider } from '../../../context/PermissionProvider/PermissionProvider';
@ -65,6 +63,7 @@ import {
import { showErrorToast } from '../../../utils/ToastUtils';
import { useRequiredParams } from '../../../utils/useRequiredParams';
import { CustomPropertyTable } from '../../common/CustomPropertyTable/CustomPropertyTable';
import { EntityAvatar } from '../../common/EntityAvatar/EntityAvatar';
import { ManageButtonItemLabel } from '../../common/ManageButtonContentItem/ManageButtonContentItem.component';
import ResizablePanels from '../../common/ResizablePanels/ResizablePanels';
import TabsLabel from '../../common/TabsLabel/TabsLabel.component';
@ -510,23 +509,13 @@ const DataProductsDetailsPage = ({
entityType={EntityType.DATA_PRODUCT}
handleFollowingClick={handleFollowingClick}
icon={
dataProduct.style?.iconURL ? (
<img
className="align-middle"
data-testid="icon"
height={36}
src={dataProduct.style.iconURL}
width={32}
/>
) : (
<DataProductIcon
className="align-middle"
color={DE_ACTIVE_COLOR}
height={36}
name="folder"
width={32}
/>
)
<EntityAvatar
entity={{
...dataProduct,
entityType: 'dataProduct',
}}
size={36}
/>
}
isFollowing={isFollowing}
isFollowingLoading={isFollowingLoading}

View File

@ -32,8 +32,6 @@ import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import { ReactComponent as EditIcon } from '../../../assets/svg/edit-new.svg';
import { ReactComponent as DeleteIcon } from '../../../assets/svg/ic-delete.svg';
import { ReactComponent as DomainIcon } from '../../../assets/svg/ic-domain.svg';
import { ReactComponent as SubDomainIcon } from '../../../assets/svg/ic-subdomain.svg';
import { ReactComponent as VersionIcon } from '../../../assets/svg/ic-version.svg';
import { ReactComponent as IconDropdown } from '../../../assets/svg/menu.svg';
import { ReactComponent as StyleIcon } from '../../../assets/svg/style.svg';
@ -43,7 +41,7 @@ import { AssetsTabRef } from '../../../components/Glossary/GlossaryTerms/tabs/As
import { AssetsOfEntity } from '../../../components/Glossary/GlossaryTerms/tabs/AssetsTabs.interface';
import EntityNameModal from '../../../components/Modals/EntityNameModal/EntityNameModal.component';
import { FQN_SEPARATOR_CHAR } from '../../../constants/char.constants';
import { DE_ACTIVE_COLOR, ERROR_MESSAGE } from '../../../constants/constants';
import { ERROR_MESSAGE } from '../../../constants/constants';
import { EntityField } from '../../../constants/Feeds.constants';
import { usePermissionProvider } from '../../../context/PermissionProvider/PermissionProvider';
import {
@ -97,6 +95,7 @@ import { showErrorToast, showSuccessToast } from '../../../utils/ToastUtils';
import { useRequiredParams } from '../../../utils/useRequiredParams';
import { useFormDrawerWithRef } from '../../common/atoms/drawer';
import DeleteWidgetModal from '../../common/DeleteWidget/DeleteWidgetModal';
import { EntityAvatar } from '../../common/EntityAvatar/EntityAvatar';
import { AlignRightIconButton } from '../../common/IconButtons/EditIconButton';
import Loader from '../../common/Loader/Loader';
import { GenericProvider } from '../../Customization/GenericProvider/GenericProvider';
@ -681,36 +680,14 @@ const DomainDetailsPage = ({
}, [domainFqn, fetchSubDomainsCount]);
const iconData = useMemo(() => {
if (domain.style?.iconURL) {
return (
<img
alt="domain-icon"
className="align-middle"
data-testid="icon"
height={36}
src={domain.style.iconURL}
width={32}
/>
);
} else if (isSubDomain) {
return (
<SubDomainIcon
className="align-middle"
color={DE_ACTIVE_COLOR}
height={36}
name="folder"
width={32}
/>
);
}
return (
<DomainIcon
className="align-middle"
color={DE_ACTIVE_COLOR}
height={36}
name="folder"
width={32}
<EntityAvatar
entity={{
...domain,
entityType: 'domain',
parent: isSubDomain ? { type: 'domain' } : undefined,
}}
size={36}
/>
);
}, [domain, isSubDomain]);

View File

@ -0,0 +1,123 @@
/*
* Copyright 2024 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 { Avatar, useTheme } from '@mui/material';
import { FC } from 'react';
import { ReactComponent as SubDomainIcon } from '../../../assets/svg/ic-subdomain.svg';
import {
getDefaultIconForEntityType,
ICON_MAP,
} from '../../../utils/IconUtils';
export interface EntityAvatarProps {
entity: {
name?: string;
displayName?: string;
entityType?: string;
style?: {
color?: string;
iconURL?: string;
};
parent?: {
type?: string;
};
};
size?: number;
className?: string;
}
/**
* A reusable component for rendering entity avatars with custom icons
* Supports URL-based icons, icon names from ICON_MAP, and default icons
*/
export const EntityAvatar: FC<EntityAvatarProps> = ({
entity,
size = 40,
className = 'entity-avatar',
}) => {
const theme = useTheme();
const bgColor = entity.style?.color || theme.palette.allShades.brand[600];
// Check if it's a sub-domain
const isSubDomain = entity.parent?.type === 'domain';
// Check if it's a URL (for Avatar src prop)
const isUrl =
entity.style?.iconURL &&
(entity.style.iconURL.startsWith('http') ||
entity.style.iconURL.startsWith('/'));
if (isUrl) {
// For URLs, use Avatar's src prop
return (
<Avatar
alt={entity.name || entity.displayName}
className={className}
src={entity.style?.iconURL}
sx={{
width: size,
height: size,
backgroundColor: bgColor,
color: theme.palette.allShades.white,
'& .MuiAvatar-img': {
width: size * 0.6,
height: size * 0.6,
},
}}
/>
);
}
// For icon names, render the icon component
const IconComponent = entity.style?.iconURL
? ICON_MAP[entity.style.iconURL]
: null;
if (IconComponent) {
return (
<Avatar
alt={entity.name || entity.displayName}
className={className}
sx={{
width: size,
height: size,
backgroundColor: bgColor,
color: theme.palette.allShades.white,
}}>
<IconComponent size={size * 0.6} style={{ strokeWidth: 1.5 }} />
</Avatar>
);
}
// Default icons when no iconURL is provided
let DefaultIcon;
if (isSubDomain) {
DefaultIcon = SubDomainIcon;
} else {
DefaultIcon = getDefaultIconForEntityType(entity.entityType);
}
return (
<Avatar
alt={entity.name || entity.displayName}
className={className}
sx={{
width: size,
height: size,
backgroundColor: bgColor,
color: theme.palette.allShades.white,
}}>
<DefaultIcon size={size * 0.6} style={{ strokeWidth: 1.5 }} />
</Avatar>
);
};

View File

@ -11,15 +11,12 @@
* limitations under the License.
*/
import { Avatar, AvatarGroup, Box, Typography, useTheme } from '@mui/material';
import { AvatarGroup, Box, Typography, useTheme } from '@mui/material';
import { ReactNode, useMemo } from 'react';
import { EntityType } from '../../../../enums/entity.enum';
import { EntityReference } from '../../../../generated/entity/type';
import { getEntityName } from '../../../../utils/EntityUtils';
import {
getDefaultIconForEntityType,
ICON_MAP,
} from '../../../../utils/IconUtils';
import { EntityAvatar } from '../../EntityAvatar/EntityAvatar';
import { ProfilePicture } from '../ProfilePicture';
import { CellRenderer, ColumnConfig } from '../shared/types';
import TagsCell from './TagsCell';
@ -45,79 +42,9 @@ export const useCellRenderer = <
entityName: (entity: any) => {
const entityName = getEntityName(entity);
// Generic entity icon (exact copy from useTableRow)
const getEntityIcon = () => {
const bgColor =
entity.style?.color || theme.palette.allShades.brand[600];
// Check if it's a URL (for Avatar src prop)
const isUrl =
entity.style?.iconURL &&
(entity.style.iconURL.startsWith('http') ||
entity.style.iconURL.startsWith('/'));
if (isUrl) {
// For URLs, use Avatar's src prop
return (
<Avatar
alt={entity.name || entity.displayName}
className="entity-avatar"
src={entity.style?.iconURL}
sx={{
width: 40,
height: 40,
backgroundColor: bgColor,
color: theme.palette.allShades.white,
'& .MuiAvatar-img': {
width: 24,
height: 24,
},
}}
/>
);
}
// For icon names, render the icon component
const IconComponent = entity.style?.iconURL
? ICON_MAP[entity.style.iconURL]
: null;
if (IconComponent) {
return (
<Avatar
alt={entity.name || entity.displayName}
className="entity-avatar"
sx={{
width: 40,
height: 40,
backgroundColor: bgColor,
color: theme.palette.allShades.white,
}}>
<IconComponent size={24} style={{ strokeWidth: 1.5 }} />
</Avatar>
);
}
// Default icons when no iconURL is provided
const DefaultIcon = getDefaultIconForEntityType(entity.entityType);
return (
<Avatar
alt={entity.name || entity.displayName}
className="entity-avatar"
sx={{
width: 40,
height: 40,
backgroundColor: bgColor,
color: theme.palette.allShades.white,
}}>
<DefaultIcon size={24} style={{ strokeWidth: 1.5 }} />
</Avatar>
);
};
return (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
{getEntityIcon()}
<EntityAvatar entity={entity} size={40} />
<Box>
<Typography
sx={{