mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-10-14 10:18:23 +00:00
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:
parent
2b80a885a0
commit
7ffb6df57a
@ -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}
|
||||
|
@ -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]);
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
@ -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={{
|
||||
|
Loading…
x
Reference in New Issue
Block a user