mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-10-15 02:38:42 +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 { useTranslation } from 'react-i18next';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { ReactComponent as EditIcon } from '../../../assets/svg/edit-new.svg';
|
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 DeleteIcon } from '../../../assets/svg/ic-delete.svg';
|
||||||
import { ReactComponent as VersionIcon } from '../../../assets/svg/ic-version.svg';
|
import { ReactComponent as VersionIcon } from '../../../assets/svg/ic-version.svg';
|
||||||
import { ReactComponent as IconDropdown } from '../../../assets/svg/menu.svg';
|
import { ReactComponent as IconDropdown } from '../../../assets/svg/menu.svg';
|
||||||
import { ReactComponent as StyleIcon } from '../../../assets/svg/style.svg';
|
import { ReactComponent as StyleIcon } from '../../../assets/svg/style.svg';
|
||||||
import { DE_ACTIVE_COLOR } from '../../../constants/constants';
|
|
||||||
import { CustomizeEntityType } from '../../../constants/Customize.constants';
|
import { CustomizeEntityType } from '../../../constants/Customize.constants';
|
||||||
import { EntityField } from '../../../constants/Feeds.constants';
|
import { EntityField } from '../../../constants/Feeds.constants';
|
||||||
import { usePermissionProvider } from '../../../context/PermissionProvider/PermissionProvider';
|
import { usePermissionProvider } from '../../../context/PermissionProvider/PermissionProvider';
|
||||||
@ -65,6 +63,7 @@ import {
|
|||||||
import { showErrorToast } from '../../../utils/ToastUtils';
|
import { showErrorToast } from '../../../utils/ToastUtils';
|
||||||
import { useRequiredParams } from '../../../utils/useRequiredParams';
|
import { useRequiredParams } from '../../../utils/useRequiredParams';
|
||||||
import { CustomPropertyTable } from '../../common/CustomPropertyTable/CustomPropertyTable';
|
import { CustomPropertyTable } from '../../common/CustomPropertyTable/CustomPropertyTable';
|
||||||
|
import { EntityAvatar } from '../../common/EntityAvatar/EntityAvatar';
|
||||||
import { ManageButtonItemLabel } from '../../common/ManageButtonContentItem/ManageButtonContentItem.component';
|
import { ManageButtonItemLabel } from '../../common/ManageButtonContentItem/ManageButtonContentItem.component';
|
||||||
import ResizablePanels from '../../common/ResizablePanels/ResizablePanels';
|
import ResizablePanels from '../../common/ResizablePanels/ResizablePanels';
|
||||||
import TabsLabel from '../../common/TabsLabel/TabsLabel.component';
|
import TabsLabel from '../../common/TabsLabel/TabsLabel.component';
|
||||||
@ -510,23 +509,13 @@ const DataProductsDetailsPage = ({
|
|||||||
entityType={EntityType.DATA_PRODUCT}
|
entityType={EntityType.DATA_PRODUCT}
|
||||||
handleFollowingClick={handleFollowingClick}
|
handleFollowingClick={handleFollowingClick}
|
||||||
icon={
|
icon={
|
||||||
dataProduct.style?.iconURL ? (
|
<EntityAvatar
|
||||||
<img
|
entity={{
|
||||||
className="align-middle"
|
...dataProduct,
|
||||||
data-testid="icon"
|
entityType: 'dataProduct',
|
||||||
height={36}
|
}}
|
||||||
src={dataProduct.style.iconURL}
|
size={36}
|
||||||
width={32}
|
/>
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<DataProductIcon
|
|
||||||
className="align-middle"
|
|
||||||
color={DE_ACTIVE_COLOR}
|
|
||||||
height={36}
|
|
||||||
name="folder"
|
|
||||||
width={32}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
isFollowing={isFollowing}
|
isFollowing={isFollowing}
|
||||||
isFollowingLoading={isFollowingLoading}
|
isFollowingLoading={isFollowingLoading}
|
||||||
|
@ -32,8 +32,6 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { ReactComponent as EditIcon } from '../../../assets/svg/edit-new.svg';
|
import { ReactComponent as EditIcon } from '../../../assets/svg/edit-new.svg';
|
||||||
import { ReactComponent as DeleteIcon } from '../../../assets/svg/ic-delete.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 VersionIcon } from '../../../assets/svg/ic-version.svg';
|
||||||
import { ReactComponent as IconDropdown } from '../../../assets/svg/menu.svg';
|
import { ReactComponent as IconDropdown } from '../../../assets/svg/menu.svg';
|
||||||
import { ReactComponent as StyleIcon } from '../../../assets/svg/style.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 { AssetsOfEntity } from '../../../components/Glossary/GlossaryTerms/tabs/AssetsTabs.interface';
|
||||||
import EntityNameModal from '../../../components/Modals/EntityNameModal/EntityNameModal.component';
|
import EntityNameModal from '../../../components/Modals/EntityNameModal/EntityNameModal.component';
|
||||||
import { FQN_SEPARATOR_CHAR } from '../../../constants/char.constants';
|
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 { EntityField } from '../../../constants/Feeds.constants';
|
||||||
import { usePermissionProvider } from '../../../context/PermissionProvider/PermissionProvider';
|
import { usePermissionProvider } from '../../../context/PermissionProvider/PermissionProvider';
|
||||||
import {
|
import {
|
||||||
@ -97,6 +95,7 @@ import { showErrorToast, showSuccessToast } from '../../../utils/ToastUtils';
|
|||||||
import { useRequiredParams } from '../../../utils/useRequiredParams';
|
import { useRequiredParams } from '../../../utils/useRequiredParams';
|
||||||
import { useFormDrawerWithRef } from '../../common/atoms/drawer';
|
import { useFormDrawerWithRef } from '../../common/atoms/drawer';
|
||||||
import DeleteWidgetModal from '../../common/DeleteWidget/DeleteWidgetModal';
|
import DeleteWidgetModal from '../../common/DeleteWidget/DeleteWidgetModal';
|
||||||
|
import { EntityAvatar } from '../../common/EntityAvatar/EntityAvatar';
|
||||||
import { AlignRightIconButton } from '../../common/IconButtons/EditIconButton';
|
import { AlignRightIconButton } from '../../common/IconButtons/EditIconButton';
|
||||||
import Loader from '../../common/Loader/Loader';
|
import Loader from '../../common/Loader/Loader';
|
||||||
import { GenericProvider } from '../../Customization/GenericProvider/GenericProvider';
|
import { GenericProvider } from '../../Customization/GenericProvider/GenericProvider';
|
||||||
@ -681,36 +680,14 @@ const DomainDetailsPage = ({
|
|||||||
}, [domainFqn, fetchSubDomainsCount]);
|
}, [domainFqn, fetchSubDomainsCount]);
|
||||||
|
|
||||||
const iconData = useMemo(() => {
|
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 (
|
return (
|
||||||
<DomainIcon
|
<EntityAvatar
|
||||||
className="align-middle"
|
entity={{
|
||||||
color={DE_ACTIVE_COLOR}
|
...domain,
|
||||||
height={36}
|
entityType: 'domain',
|
||||||
name="folder"
|
parent: isSubDomain ? { type: 'domain' } : undefined,
|
||||||
width={32}
|
}}
|
||||||
|
size={36}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}, [domain, isSubDomain]);
|
}, [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.
|
* 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 { ReactNode, useMemo } from 'react';
|
||||||
import { EntityType } from '../../../../enums/entity.enum';
|
import { EntityType } from '../../../../enums/entity.enum';
|
||||||
import { EntityReference } from '../../../../generated/entity/type';
|
import { EntityReference } from '../../../../generated/entity/type';
|
||||||
import { getEntityName } from '../../../../utils/EntityUtils';
|
import { getEntityName } from '../../../../utils/EntityUtils';
|
||||||
import {
|
import { EntityAvatar } from '../../EntityAvatar/EntityAvatar';
|
||||||
getDefaultIconForEntityType,
|
|
||||||
ICON_MAP,
|
|
||||||
} from '../../../../utils/IconUtils';
|
|
||||||
import { ProfilePicture } from '../ProfilePicture';
|
import { ProfilePicture } from '../ProfilePicture';
|
||||||
import { CellRenderer, ColumnConfig } from '../shared/types';
|
import { CellRenderer, ColumnConfig } from '../shared/types';
|
||||||
import TagsCell from './TagsCell';
|
import TagsCell from './TagsCell';
|
||||||
@ -45,79 +42,9 @@ export const useCellRenderer = <
|
|||||||
entityName: (entity: any) => {
|
entityName: (entity: any) => {
|
||||||
const entityName = getEntityName(entity);
|
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 (
|
return (
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
|
||||||
{getEntityIcon()}
|
<EntityAvatar entity={entity} size={40} />
|
||||||
<Box>
|
<Box>
|
||||||
<Typography
|
<Typography
|
||||||
sx={{
|
sx={{
|
||||||
|
Loading…
x
Reference in New Issue
Block a user