1
0
mirror of https://github.com/Unleash/unleash.git synced 2024-12-28 00:06:53 +01:00

fix: add workaround for tooltip (#7649)

This PR adds the UI part of feature flag collaborators. Collaborators are hidden on windows smaller than size XL because we're not sure how to deal with them in those cases yet.
This commit is contained in:
Thomas Heartman 2024-07-24 11:33:29 +02:00 committed by GitHub
parent e63503e832
commit e2b90ae91d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 185 additions and 35 deletions

View File

@ -19,13 +19,14 @@ const StyledAvatars = styled('div')(({ theme }) => ({
marginLeft: theme.spacing(1),
}));
const StyledAvatar = styled(UserAvatar)(({ theme }) => ({
outline: `${theme.spacing(0.25)} solid ${theme.palette.background.paper}`,
marginLeft: theme.spacing(-1),
'&:hover': {
outlineColor: theme.palette.primary.main,
},
}));
const StyledAvatar = (component: typeof UserAvatar) =>
styled(component)(({ theme }) => ({
outline: `${theme.spacing(0.25)} solid ${theme.palette.background.paper}`,
marginLeft: theme.spacing(-1),
'&:hover': {
outlineColor: theme.palette.primary.main,
},
}));
const StyledHeader = styled('h3')(({ theme }) => ({
margin: theme.spacing(0, 0, 1),
@ -42,13 +43,31 @@ interface IGroupCardAvatarsProps {
}[];
header?: ReactNode;
avatarLimit?: number;
AvatarComponent?: typeof UserAvatar;
}
export const GroupCardAvatars = ({
AvatarComponent,
...props
}: IGroupCardAvatarsProps) => {
const Avatar = StyledAvatar(AvatarComponent ?? UserAvatar);
return <GroupCardAvatarsInner AvatarComponent={Avatar} {...props} />;
};
type GroupCardAvatarsInnerProps = Omit<
IGroupCardAvatarsProps,
'AvatarComponent'
> & {
AvatarComponent: typeof UserAvatar;
};
const GroupCardAvatarsInner = ({
users = [],
header = null,
avatarLimit = 9,
}: IGroupCardAvatarsProps) => {
AvatarComponent,
}: GroupCardAvatarsInnerProps) => {
const shownUsers = useMemo(
() =>
users
@ -94,7 +113,7 @@ export const GroupCardAvatars = ({
/>
<StyledAvatars>
{shownUsers.map((user) => (
<StyledAvatar
<AvatarComponent
key={objectId(user)}
user={{ ...user, id: objectId(user) }}
onMouseEnter={(event) => {
@ -107,9 +126,9 @@ export const GroupCardAvatars = ({
<ConditionallyRender
condition={users.length > avatarLimit}
show={
<StyledAvatar>
<AvatarComponent>
+{users.length - shownUsers.length}
</StyledAvatar>
</AvatarComponent>
}
/>
<GroupPopover

View File

@ -9,21 +9,15 @@ import type { IUser } from 'interfaces/user';
import type { FC } from 'react';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
const StyledAvatar = styled(Avatar, {
shouldForwardProp: (prop) => prop !== 'avatarWidth',
})<{ avatarWidth?: (theme: Theme) => string }>(({ theme, avatarWidth }) => {
const width = avatarWidth ? avatarWidth(theme) : theme.spacing(3.5);
return {
width,
height: width,
margin: 'auto',
backgroundColor: theme.palette.secondary.light,
color: theme.palette.text.primary,
fontSize: theme.fontSizes.smallerBody,
fontWeight: theme.fontWeight.bold,
};
});
const StyledAvatar = styled(Avatar)(({ theme }) => ({
width: theme.spacing(3.5),
height: theme.spacing(3.5),
margin: 'auto',
backgroundColor: theme.palette.secondary.light,
color: theme.palette.text.primary,
fontSize: theme.fontSizes.smallerBody,
fontWeight: theme.fontWeight.bold,
}));
export interface IUserAvatarProps extends AvatarProps {
user?: Partial<
@ -35,7 +29,6 @@ export interface IUserAvatarProps extends AvatarProps {
onMouseLeave?: () => void;
className?: string;
sx?: SxProps<Theme>;
avatarWidth?: (theme: Theme) => string;
hideTitle?: boolean;
}

View File

@ -0,0 +1,14 @@
import { render } from '@testing-library/react';
import { Collaborators } from './Collaborators';
test('renders nothing if collaborators is undefined', () => {
const { container } = render(<Collaborators collaborators={undefined} />);
expect(container).toBeEmptyDOMElement();
});
test('renders nothing if users is empty', () => {
const { container } = render(<Collaborators collaborators={[]} />);
expect(container).toBeEmptyDOMElement();
});

View File

@ -0,0 +1,95 @@
import { styled } from '@mui/material';
import { GroupCardAvatars } from 'component/admin/groups/GroupsList/GroupCard/GroupCardAvatars/NewGroupCardAvatars';
import { HtmlTooltip } from 'component/common/HtmlTooltip/HtmlTooltip';
import { UserAvatar } from 'component/common/UserAvatar/UserAvatar';
import type { Collaborator } from 'interfaces/featureToggle';
import type { FC } from 'react';
import { Link } from 'react-router-dom';
const StyledAvatar = styled(UserAvatar)(({ theme }) => ({
width: theme.spacing(3),
height: theme.spacing(3),
}));
const LastModifiedByContainer = styled('div')(({ theme }) => ({
display: 'grid',
gridTemplateAreas: `
'description description'
'avatar link'
`,
rowGap: theme.spacing(0.5),
columnGap: theme.spacing(1),
alignItems: 'center',
height: 'min-content',
}));
const GridDescription = styled('span')({ gridArea: 'description' });
const GridTooltip = styled(HtmlTooltip)({ gridArea: 'avatar' });
const GridLink = styled(Link)({ gridArea: 'link' });
const LastModifiedBy: FC<Collaborator> = ({ id, name, imageUrl }) => {
return (
<LastModifiedByContainer>
<GridDescription>Last modified by</GridDescription>
<GridTooltip arrow describeChild title={name}>
<span>
<StyledAvatar user={{ id, name, imageUrl }} hideTitle />
</span>
</GridTooltip>
<GridLink to='logs'>view change</GridLink>
</LastModifiedByContainer>
);
};
const CollaboratorListContainer = styled('div')(({ theme }) => ({
display: 'flex',
flexFlow: 'column',
gap: theme.spacing(0.5),
alignItems: 'flex-start',
height: 'min-content',
}));
const CollaboratorList: FC<{ collaborators: Collaborator[] }> = ({
collaborators,
}) => {
return (
<CollaboratorListContainer>
<span className='description'>Collaborators</span>
<GroupCardAvatars
users={collaborators}
avatarLimit={8}
AvatarComponent={StyledAvatar}
/>
</CollaboratorListContainer>
);
};
const Container = styled('article')(({ theme }) => ({
display: 'flex',
flexDirection: 'row',
gap: theme.spacing(10),
alignItems: 'center',
justifyContent: 'space-between',
[theme.breakpoints.down('xl')]: {
display: 'none',
},
}));
type Props = {
collaborators: Collaborator[] | undefined;
};
export const Collaborators: FC<Props> = ({ collaborators }) => {
if (!collaborators || collaborators.length === 0) {
return null;
}
const lastModifiedBy = collaborators[0];
return (
<Container>
<LastModifiedBy {...lastModifiedBy} />
<CollaboratorList collaborators={collaborators} />
</Container>
);
};

View File

@ -50,6 +50,7 @@ import copy from 'copy-to-clipboard';
import useToast from 'hooks/useToast';
import { useUiFlag } from 'hooks/useUiFlag';
import type { IFeatureToggle } from 'interfaces/featureToggle';
import { Collaborators } from './Collaborators';
const StyledHeader = styled('div')(({ theme }) => ({
backgroundColor: theme.palette.background.paper,
@ -113,8 +114,12 @@ const StyledSeparator = styled('div')(({ theme }) => ({
height: '1px',
}));
const StyledTabContainer = styled('div')(({ theme }) => ({
padding: theme.spacing(0, 4),
const StyledTabRow = styled('div')(({ theme }) => ({
display: 'flex',
flexFlow: 'row nowrap',
gap: theme.spacing(4),
paddingInline: theme.spacing(4),
justifyContent: 'space-between',
}));
const StyledTabButton = styled(Tab)(({ theme }) => ({
@ -155,6 +160,7 @@ export const FeatureView = () => {
const [openStaleDialog, setOpenStaleDialog] = useState(false);
const [isFeatureNameCopied, setIsFeatureNameCopied] = useState(false);
const smallScreen = useMediaQuery(`(max-width:${500}px)`);
const showCollaborators = useUiFlag('featureCollaborators');
const { feature, loading, error, status } = useFeature(
projectId,
@ -355,7 +361,7 @@ export const FeatureView = () => {
</StyledToolbarContainer>
</StyledInnerContainer>
<StyledSeparator />
<StyledTabContainer>
<StyledTabRow>
<Tabs
value={activeTab.path}
indicatorColor='primary'
@ -371,7 +377,15 @@ export const FeatureView = () => {
/>
))}
</Tabs>
</StyledTabContainer>
<ConditionallyRender
condition={showCollaborators}
show={
<Collaborators
collaborators={feature.collaborators?.users}
/>
}
/>
</StyledTabRow>
</StyledHeader>
<Routes>
<Route path='metrics' element={<FeatureMetrics />} />

View File

@ -1,4 +1,4 @@
import { type Theme, styled } from '@mui/material';
import { styled } from '@mui/material';
import type { FC } from 'react';
import { ScreenReaderOnly } from 'component/common/ScreenReaderOnly/ScreenReaderOnly';
import { HtmlTooltip } from 'component/common/HtmlTooltip/HtmlTooltip';
@ -35,6 +35,11 @@ const StyledSecondaryText = styled('p')(({ theme }) => ({
color: theme.palette.text.secondary,
}));
const StyledAvatar = styled(UserAvatar)(({ theme }) => ({
width: theme.spacing(3),
height: theme.spacing(3),
}));
export const AvatarCell =
(onAvatarClick: (userId: number) => void): FC<AvatarCellProps> =>
({ row: { original } }) => {
@ -67,14 +72,13 @@ export const AvatarCell =
</span>
</ScreenReaderOnly>
<UserAvatar
<StyledAvatar
hideTitle
user={{
id: original.createdBy.id,
name: original.createdBy.name,
imageUrl: original.createdBy.imageUrl,
}}
avatarWidth={(theme: Theme) => theme.spacing(3)}
/>
</StyledAvatarButton>
</HtmlTooltip>

View File

@ -38,6 +38,16 @@ export type Lifecycle = {
enteredStageAt: string;
};
export type Collaborator = {
id: number;
name: string;
imageUrl: string;
};
export type CollaboratorData = {
users: Collaborator[];
};
/**
* @deprecated use FeatureSchema from openapi
*/
@ -65,6 +75,7 @@ export interface IFeatureToggle {
name: string;
imageUrl: string;
};
collaborators?: CollaboratorData;
}
export interface IDependency {

View File

@ -331,7 +331,7 @@ const flags: IFlags = {
false,
),
featureCollaborators: parseEnvVarBoolean(
process.env.UNEASH_EXPERIMENTAL_FEATURE_COLLABORATORS,
process.env.UNLEASH_EXPERIMENTAL_FEATURE_COLLABORATORS,
false,
),
};