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:
parent
e63503e832
commit
e2b90ae91d
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
});
|
95
frontend/src/component/feature/FeatureView/Collaborators.tsx
Normal file
95
frontend/src/component/feature/FeatureView/Collaborators.tsx
Normal 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>
|
||||
);
|
||||
};
|
@ -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 />} />
|
||||
|
@ -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>
|
||||
|
@ -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 {
|
||||
|
@ -331,7 +331,7 @@ const flags: IFlags = {
|
||||
false,
|
||||
),
|
||||
featureCollaborators: parseEnvVarBoolean(
|
||||
process.env.UNEASH_EXPERIMENTAL_FEATURE_COLLABORATORS,
|
||||
process.env.UNLEASH_EXPERIMENTAL_FEATURE_COLLABORATORS,
|
||||
false,
|
||||
),
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user