From 7ffb6df57a4aed0dd9397efd535aa5e579e6b838 Mon Sep 17 00:00:00 2001 From: satish Date: Mon, 29 Sep 2025 14:36:00 +0530 Subject: [PATCH] feat: Create reusable EntityAvatar component for consistent icon rendering (#23609) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 Co-authored-by: Claude --- .../DataProductsDetailsPage.component.tsx | 27 ++-- .../DomainDetailsPage.component.tsx | 41 ++---- .../common/EntityAvatar/EntityAvatar.tsx | 123 ++++++++++++++++++ .../common/atoms/table/useCellRenderer.tsx | 79 +---------- 4 files changed, 143 insertions(+), 127 deletions(-) create mode 100644 openmetadata-ui/src/main/resources/ui/src/components/common/EntityAvatar/EntityAvatar.tsx diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataProducts/DataProductsDetailsPage/DataProductsDetailsPage.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataProducts/DataProductsDetailsPage/DataProductsDetailsPage.component.tsx index 33761b58d84..4783d9ad1c7 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataProducts/DataProductsDetailsPage/DataProductsDetailsPage.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataProducts/DataProductsDetailsPage/DataProductsDetailsPage.component.tsx @@ -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 ? ( - - ) : ( - - ) + } isFollowing={isFollowing} isFollowingLoading={isFollowingLoading} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Domain/DomainDetailsPage/DomainDetailsPage.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Domain/DomainDetailsPage/DomainDetailsPage.component.tsx index 51c6cea9818..b30d2f0369a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Domain/DomainDetailsPage/DomainDetailsPage.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Domain/DomainDetailsPage/DomainDetailsPage.component.tsx @@ -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 ( - domain-icon - ); - } else if (isSubDomain) { - return ( - - ); - } - return ( - ); }, [domain, isSubDomain]); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/EntityAvatar/EntityAvatar.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/EntityAvatar/EntityAvatar.tsx new file mode 100644 index 00000000000..ee3497628f5 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/EntityAvatar/EntityAvatar.tsx @@ -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 = ({ + 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 ( + + ); + } + + // For icon names, render the icon component + const IconComponent = entity.style?.iconURL + ? ICON_MAP[entity.style.iconURL] + : null; + + if (IconComponent) { + return ( + + + + ); + } + + // Default icons when no iconURL is provided + let DefaultIcon; + if (isSubDomain) { + DefaultIcon = SubDomainIcon; + } else { + DefaultIcon = getDefaultIconForEntityType(entity.entityType); + } + + return ( + + + + ); +}; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/atoms/table/useCellRenderer.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/atoms/table/useCellRenderer.tsx index 2d6695f177c..3b316facb0a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/atoms/table/useCellRenderer.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/atoms/table/useCellRenderer.tsx @@ -11,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 ( - - ); - } - - // For icon names, render the icon component - const IconComponent = entity.style?.iconURL - ? ICON_MAP[entity.style.iconURL] - : null; - if (IconComponent) { - return ( - - - - ); - } - - // Default icons when no iconURL is provided - const DefaultIcon = getDefaultIconForEntityType(entity.entityType); - - return ( - - - - ); - }; - return ( - {getEntityIcon()} +