feat(ui/ingestion): implement hover state for stacked avatars in owners column (#13703)

This commit is contained in:
purnimagarg1 2025-06-10 03:55:42 +05:30 committed by GitHub
parent 8889181b31
commit 16a3211a96
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 186 additions and 16 deletions

View File

@ -3,12 +3,14 @@ import React, { useState } from 'react';
import { AvatarImage, AvatarImageWrapper, AvatarText, Container } from '@components/components/Avatar/components';
import { AvatarProps } from '@components/components/Avatar/types';
import getAvatarColor, { getNameInitials } from '@components/components/Avatar/utils';
import { Icon } from '@components/components/Icon';
export const avatarDefaults: AvatarProps = {
name: 'User name',
size: 'default',
showInPill: false,
isOutlined: false,
isGroup: false,
};
export const Avatar = ({
@ -18,23 +20,29 @@ export const Avatar = ({
onClick,
showInPill = avatarDefaults.showInPill,
isOutlined = avatarDefaults.isOutlined,
isGroup = avatarDefaults.isGroup,
}: AvatarProps) => {
const [hasError, setHasError] = useState(false);
return (
<Container onClick={onClick} $hasOnClick={!!onClick} $showInPill={showInPill}>
<AvatarImageWrapper
$color={getAvatarColor(name)}
$size={size}
$isOutlined={isOutlined}
$hasImage={!!imageUrl}
>
{!hasError && imageUrl ? (
<AvatarImage src={imageUrl} onError={() => setHasError(true)} />
) : (
<>{getNameInitials(name)} </>
)}
</AvatarImageWrapper>
{(!isGroup || imageUrl) && (
<AvatarImageWrapper
$color={getAvatarColor(name)}
$size={size}
$isOutlined={isOutlined}
$hasImage={!!imageUrl}
>
{!hasError && imageUrl ? (
<AvatarImage src={imageUrl} onError={() => setHasError(true)} />
) : (
!isGroup && getNameInitials(name)
)}
</AvatarImageWrapper>
)}
{isGroup && !imageUrl && (
<Icon icon="UsersThree" source="phosphor" variant="filled" color="gray" size="lg" />
)}
{showInPill && <AvatarText $size={size}>{name}</AvatarText>}
</Container>
);

View File

@ -7,4 +7,5 @@ export interface AvatarProps {
size?: AvatarSizeOptions;
showInPill?: boolean;
isOutlined?: boolean;
isGroup?: boolean;
}

View File

@ -0,0 +1,87 @@
import { Badge, StructuredPopover, Text } from '@components';
import React from 'react';
import styled from 'styled-components';
import { AvatarStack } from '@components/components/AvatarStack/AvatarStack';
import HoverSectionContent from '@components/components/AvatarStack/HoverSectionContent';
import { AvatarStackProps } from '@components/components/AvatarStack/types';
import EntityRegistry from '@app/entityV2/EntityRegistry';
import StopPropagationWrapper from '@app/sharedV2/StopPropagationWrapper';
import { EntityType } from '@types';
const HeaderContainer = styled.div`
display: flex;
gap: 4px;
`;
interface Props extends AvatarStackProps {
entityRegistry: EntityRegistry;
}
const AvatarStackWithHover = ({
avatars,
size = 'default',
showRemainingNumber = true,
maxToShow = 4,
entityRegistry,
}: Props) => {
const users = avatars.filter((avatar) => avatar.type === EntityType.CorpUser);
const groups = avatars.filter((avatar) => avatar.type === EntityType.CorpGroup);
const renderTitle = (headerText, count) => (
<HeaderContainer>
<Text size="sm" color="gray" weight="bold">
{headerText}
</Text>
<Badge count={count} size="xs" />
</HeaderContainer>
);
return (
<StopPropagationWrapper>
<StructuredPopover
width={280}
title="Owners"
sections={[
...(users.length > 0
? [
{
title: renderTitle('Users', users.length),
content: (
<HoverSectionContent
avatars={users}
entityRegistry={entityRegistry}
size={size}
/>
),
},
]
: []),
...(groups.length > 0
? [
{
title: renderTitle('Groups', groups.length),
content: (
<HoverSectionContent
avatars={groups}
entityRegistry={entityRegistry}
size={size}
isGroup
/>
),
},
]
: []),
]}
>
<div>
<AvatarStack avatars={avatars} showRemainingNumber={showRemainingNumber} maxToShow={maxToShow} />
</div>
</StructuredPopover>
</StopPropagationWrapper>
);
};
export default AvatarStackWithHover;

View File

@ -0,0 +1,65 @@
import { Avatar, Button } from '@components';
import React, { useState } from 'react';
import { Link } from 'react-router-dom';
import styled from 'styled-components';
import { AvatarItemProps } from '@components/components/AvatarStack/types';
import { AvatarSizeOptions } from '@components/theme/config';
import EntityRegistry from '@app/entityV2/EntityRegistry';
const PillsContainer = styled.div`
display: flex;
gap: 4px;
flex-wrap: wrap;
`;
interface Props {
avatars: AvatarItemProps[];
entityRegistry: EntityRegistry;
size?: AvatarSizeOptions;
maxVisible?: number;
isGroup?: boolean;
}
const HoverSectionContent = ({ avatars, entityRegistry, size, maxVisible = 4, isGroup }: Props) => {
const [expanded, setExpanded] = useState(false);
const visibleAvatars = expanded ? avatars : avatars.slice(0, maxVisible);
const hasMore = avatars.length > maxVisible;
return (
<div>
<PillsContainer>
{visibleAvatars.map((user) => {
const userAvatar = (
<Avatar
showInPill
size={size}
isOutlined
imageUrl={user.imageUrl}
name={user.name}
isGroup={isGroup}
/>
);
return (
<>
{user.type && user.urn ? (
<Link to={entityRegistry.getEntityUrl(user.type, user.urn)}>{userAvatar}</Link>
) : (
{ userAvatar }
)}
</>
);
})}
</PillsContainer>
{hasMore && (
<Button variant="text" size="sm" color="gray" onClick={() => setExpanded((prev) => !prev)}>
{expanded ? 'View less' : 'View more'}
</Button>
)}
</div>
);
};
export default HoverSectionContent;

View File

@ -1,8 +1,12 @@
import { AvatarSizeOptions } from '@src/alchemy-components/theme/config';
import { EntityType } from '@types';
export interface AvatarItemProps {
name: string;
imageUrl?: string | null;
type?: EntityType;
urn?: string;
}
export type AvatarStackProps = {

View File

@ -1,5 +1,5 @@
export interface SectionType {
title: string;
title: string | React.ReactNode;
titleSuffix?: string | React.ReactNode;
content: string | React.ReactNode;
}

View File

@ -5,7 +5,7 @@ import React from 'react';
import { Link } from 'react-router-dom';
import styled from 'styled-components/macro';
import { AvatarStack } from '@components/components/AvatarStack/AvatarStack';
import AvatarStackWithHover from '@components/components/AvatarStack/AvatarStackWithHover';
import EntityRegistry from '@app/entityV2/EntityRegistry';
import { EXECUTION_REQUEST_STATUS_RUNNING } from '@app/ingestV2/executions/constants';
@ -14,7 +14,7 @@ import useGetSourceLogoUrl from '@app/ingestV2/source/builder/useGetSourceLogoUr
import { HoverEntityTooltip } from '@app/recommendations/renderer/component/HoverEntityTooltip';
import { capitalizeFirstLetter } from '@app/shared/textUtil';
import { Owner } from '@types';
import { EntityType, Owner } from '@types';
const PreviewImage = styled(Image)`
max-height: 20px;
@ -119,6 +119,8 @@ export function OwnerColumn({ owners, entityRegistry }: { owners: Owner[]; entit
return {
name: entityRegistry.getDisplayName(owner.owner.type, owner.owner),
imageUrl: owner.owner.editableProperties?.pictureLink,
type: owner.owner.type,
urn: owner.owner.urn,
};
});
const singleOwner = owners.length === 1 ? owners[0].owner : undefined;
@ -139,11 +141,14 @@ export function OwnerColumn({ owners, entityRegistry }: { owners: Owner[]; entit
name={entityRegistry.getDisplayName(singleOwner.type, singleOwner)}
imageUrl={singleOwner.editableProperties?.pictureLink}
showInPill
isGroup={singleOwner.type === EntityType.CorpGroup}
/>
</Link>
</HoverEntityTooltip>
)}
{owners.length > 1 && <AvatarStack avatars={ownerAvatars} showRemainingNumber />}
{owners.length > 1 && (
<AvatarStackWithHover avatars={ownerAvatars} showRemainingNumber entityRegistry={entityRegistry} />
)}
</>
);
}