1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-08-04 13:48:56 +02:00

feat: better tooltip links (#2891)

https://linear.app/unleash/issue/2-576/improve-how-text-that-has-tooltip-should-look-so-they-are-not


![image](https://user-images.githubusercontent.com/14320932/212140467-46d4f7f9-b5c1-40ea-9748-4a6ccc7950bb.png)


![image](https://user-images.githubusercontent.com/14320932/212140316-d6e88bc0-c26e-436b-855f-5f6e8697aed8.png)

- Adapts the existing `HtmlTooltip` component to support setting
`maxHeight` and `maxWidth` properties;
- Introduces a new common component: `TooltipLink`;
- Adapts SA (tokens), features (tags), variants (overrides, payloads)
and project access (role and role description);
- Role description in project access now uses this component instead of
the old jankier popover component;
This commit is contained in:
Nuno Góis 2023-01-16 12:04:52 +00:00 committed by GitHub
parent 005e5b1d15
commit 4286103850
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 150 additions and 170 deletions

View File

@ -1,22 +1,16 @@
import { VFC } from 'react'; import { VFC } from 'react';
import { Link, styled, Typography } from '@mui/material'; import { styled, Typography } from '@mui/material';
import { TextCell } from 'component/common/Table/cells/TextCell/TextCell'; import { TextCell } from 'component/common/Table/cells/TextCell/TextCell';
import { HtmlTooltip } from 'component/common/HtmlTooltip/HtmlTooltip';
import { Highlighter } from 'component/common/Highlighter/Highlighter'; import { Highlighter } from 'component/common/Highlighter/Highlighter';
import { useSearchHighlightContext } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext'; import { useSearchHighlightContext } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
import { IServiceAccount } from 'interfaces/service-account'; import { IServiceAccount } from 'interfaces/service-account';
import { LinkCell } from 'component/common/Table/cells/LinkCell/LinkCell'; import { LinkCell } from 'component/common/Table/cells/LinkCell/LinkCell';
import { TooltipLink } from 'component/common/TooltipLink/TooltipLink';
const StyledItem = styled(Typography)(({ theme }) => ({ const StyledItem = styled(Typography)(({ theme }) => ({
fontSize: theme.fontSizes.smallerBody, fontSize: theme.fontSizes.smallerBody,
})); }));
const StyledLink = styled(Link, {
shouldForwardProp: prop => prop !== 'highlighted',
})<{ highlighted?: boolean }>(({ theme, highlighted }) => ({
backgroundColor: highlighted ? theme.palette.highlight : 'transparent',
}));
interface IServiceAccountTokensCellProps { interface IServiceAccountTokensCellProps {
serviceAccount: IServiceAccount; serviceAccount: IServiceAccount;
value: string; value: string;
@ -35,8 +29,8 @@ export const ServiceAccountTokensCell: VFC<IServiceAccountTokensCellProps> = ({
return ( return (
<TextCell> <TextCell>
<HtmlTooltip <TooltipLink
title={ tooltip={
<> <>
{serviceAccount.tokens?.map(({ id, description }) => ( {serviceAccount.tokens?.map(({ id, description }) => (
<StyledItem key={id}> <StyledItem key={id}>
@ -47,19 +41,15 @@ export const ServiceAccountTokensCell: VFC<IServiceAccountTokensCellProps> = ({
))} ))}
</> </>
} }
highlighted={
searchQuery.length > 0 &&
value.toLowerCase().includes(searchQuery.toLowerCase())
}
> >
<StyledLink {serviceAccount.tokens?.length === 1
underline="always" ? '1 token'
highlighted={ : `${serviceAccount.tokens?.length} tokens`}
searchQuery.length > 0 && </TooltipLink>
value.toLowerCase().includes(searchQuery.toLowerCase())
}
>
{serviceAccount.tokens?.length === 1
? '1 token'
: `${serviceAccount.tokens?.length} tokens`}
</StyledLink>
</HtmlTooltip>
</TextCell> </TextCell>
); );
}; };

View File

@ -1,29 +1,44 @@
import { styled, Tooltip, tooltipClasses, TooltipProps } from '@mui/material'; import { styled, Tooltip, tooltipClasses, TooltipProps } from '@mui/material';
import { SpacingArgument } from '@mui/system/createTheme/createSpacing';
const StyledHtmlTooltip = styled(({ className, ...props }: TooltipProps) => ( const StyledHtmlTooltip = styled(
<Tooltip {...props} classes={{ popper: className }} /> ({ className, maxWidth, maxHeight, ...props }: IHtmlTooltipProps) => (
))(({ theme }) => ({ <Tooltip {...props} classes={{ popper: className }} />
maxWidth: theme.spacing(37.5), ),
[`& .${tooltipClasses.tooltip}`]: { {
display: 'flex', shouldForwardProp: prop => prop !== 'maxWidth' && prop !== 'maxHeight',
flexDirection: 'column', }
backgroundColor: theme.palette.background.paper, )<{ maxWidth?: SpacingArgument; maxHeight?: SpacingArgument }>(
padding: theme.spacing(1, 1.5), ({ theme, maxWidth, maxHeight }) => ({
borderRadius: theme.shape.borderRadiusMedium, maxWidth: maxWidth || theme.spacing(37.5),
boxShadow: theme.shadows[2], [`& .${tooltipClasses.tooltip}`]: {
color: theme.palette.text.primary, display: 'flex',
fontWeight: theme.fontWeight.medium, flexDirection: 'column',
maxWidth: 'inherit', backgroundColor: theme.palette.background.paper,
border: `1px solid ${theme.palette.lightBorder}`, padding: theme.spacing(1, 1.5),
}, borderRadius: theme.shape.borderRadiusMedium,
[`& .${tooltipClasses.arrow}`]: { boxShadow: theme.shadows[2],
'&:before': { color: theme.palette.text.primary,
fontWeight: theme.fontWeight.medium,
maxWidth: 'inherit',
border: `1px solid ${theme.palette.lightBorder}`, border: `1px solid ${theme.palette.lightBorder}`,
maxHeight: maxHeight || theme.spacing(37.5),
overflow: 'auto',
}, },
color: theme.palette.background.paper, [`& .${tooltipClasses.arrow}`]: {
}, '&:before': {
})); border: `1px solid ${theme.palette.lightBorder}`,
},
color: theme.palette.background.paper,
},
})
);
export const HtmlTooltip = (props: TooltipProps) => ( export interface IHtmlTooltipProps extends TooltipProps {
maxWidth?: SpacingArgument;
maxHeight?: SpacingArgument;
}
export const HtmlTooltip = (props: IHtmlTooltipProps) => (
<StyledHtmlTooltip {...props}>{props.children}</StyledHtmlTooltip> <StyledHtmlTooltip {...props}>{props.children}</StyledHtmlTooltip>
); );

View File

@ -1,21 +1,15 @@
import { VFC } from 'react'; import { VFC } from 'react';
import { FeatureSchema } from 'openapi'; import { FeatureSchema } from 'openapi';
import { Link, styled, Typography } from '@mui/material'; import { styled, Typography } from '@mui/material';
import { TextCell } from '../TextCell/TextCell'; import { TextCell } from '../TextCell/TextCell';
import { HtmlTooltip } from 'component/common/HtmlTooltip/HtmlTooltip';
import { Highlighter } from 'component/common/Highlighter/Highlighter'; import { Highlighter } from 'component/common/Highlighter/Highlighter';
import { useSearchHighlightContext } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext'; import { useSearchHighlightContext } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
import { TooltipLink } from 'component/common/TooltipLink/TooltipLink';
const StyledTag = styled(Typography)(({ theme }) => ({ const StyledTag = styled(Typography)(({ theme }) => ({
fontSize: theme.fontSizes.smallerBody, fontSize: theme.fontSizes.smallerBody,
})); }));
const StyledLink = styled(Link, {
shouldForwardProp: prop => prop !== 'highlighted',
})<{ highlighted?: boolean }>(({ theme, highlighted }) => ({
backgroundColor: highlighted ? theme.palette.highlight : 'transparent',
}));
interface IFeatureTagCellProps { interface IFeatureTagCellProps {
row: { row: {
original: FeatureSchema; original: FeatureSchema;
@ -31,8 +25,12 @@ export const FeatureTagCell: VFC<IFeatureTagCellProps> = ({ row, value }) => {
return ( return (
<TextCell> <TextCell>
<HtmlTooltip <TooltipLink
title={ highlighted={
searchQuery.length > 0 &&
value.toLowerCase().includes(searchQuery.toLowerCase())
}
tooltip={
<> <>
{row.original.tags?.map(tag => ( {row.original.tags?.map(tag => (
<StyledTag key={tag.type + tag.value}> <StyledTag key={tag.type + tag.value}>
@ -44,18 +42,10 @@ export const FeatureTagCell: VFC<IFeatureTagCellProps> = ({ row, value }) => {
</> </>
} }
> >
<StyledLink {row.original.tags?.length === 1
underline="always" ? '1 tag'
highlighted={ : `${row.original.tags?.length} tags`}
searchQuery.length > 0 && </TooltipLink>
value.toLowerCase().includes(searchQuery.toLowerCase())
}
>
{row.original.tags?.length === 1
? '1 tag'
: `${row.original.tags?.length} tags`}
</StyledLink>
</HtmlTooltip>
</TextCell> </TextCell>
); );
}; };

View File

@ -0,0 +1,34 @@
import { ReactNode } from 'react';
import { Link, LinkProps, styled, TooltipProps } from '@mui/material';
import { HtmlTooltip, IHtmlTooltipProps } from '../HtmlTooltip/HtmlTooltip';
const StyledLink = styled(Link, {
shouldForwardProp: prop => prop !== 'highlighted',
})<{ highlighted?: boolean }>(({ theme, highlighted }) => ({
backgroundColor: highlighted ? theme.palette.highlight : 'transparent',
color: theme.palette.text.primary,
textDecorationColor: theme.palette.text.disabled,
textDecorationStyle: 'dashed',
textUnderlineOffset: theme.spacing(0.5),
}));
interface ITooltipLinkProps extends LinkProps {
tooltip: ReactNode;
highlighted?: boolean;
tooltipProps?: Omit<IHtmlTooltipProps, 'title' | 'children'>;
children: ReactNode;
}
export const TooltipLink = ({
tooltip,
highlighted,
tooltipProps,
children,
...props
}: ITooltipLinkProps) => (
<HtmlTooltip title={tooltip} {...tooltipProps}>
<StyledLink highlighted={highlighted} {...props}>
{children}
</StyledLink>
</HtmlTooltip>
);

View File

@ -1,20 +1,14 @@
import { Link, styled, Typography } from '@mui/material'; import { styled, Typography } from '@mui/material';
import { Highlighter } from 'component/common/Highlighter/Highlighter'; import { Highlighter } from 'component/common/Highlighter/Highlighter';
import { HtmlTooltip } from 'component/common/HtmlTooltip/HtmlTooltip';
import { TextCell } from 'component/common/Table/cells/TextCell/TextCell'; import { TextCell } from 'component/common/Table/cells/TextCell/TextCell';
import { useSearchHighlightContext } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext'; import { useSearchHighlightContext } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
import { TooltipLink } from 'component/common/TooltipLink/TooltipLink';
import { IOverride } from 'interfaces/featureToggle'; import { IOverride } from 'interfaces/featureToggle';
const StyledItem = styled(Typography)(({ theme }) => ({ const StyledItem = styled(Typography)(({ theme }) => ({
fontSize: theme.fontSizes.smallerBody, fontSize: theme.fontSizes.smallerBody,
})); }));
const StyledLink = styled(Link, {
shouldForwardProp: prop => prop !== 'highlighted',
})<{ highlighted?: boolean }>(({ theme, highlighted }) => ({
backgroundColor: highlighted ? theme.palette.highlight : 'transparent',
}));
interface IOverridesCellProps { interface IOverridesCellProps {
value?: IOverride[]; value?: IOverride[];
} }
@ -29,8 +23,8 @@ export const OverridesCell = ({ value: overrides }: IOverridesCellProps) => {
return ( return (
<TextCell> <TextCell>
<HtmlTooltip <TooltipLink
title={ tooltip={
<> <>
{overrides.map((override, index) => ( {overrides.map((override, index) => (
<StyledItem key={override.contextName + index}> <StyledItem key={override.contextName + index}>
@ -41,23 +35,19 @@ export const OverridesCell = ({ value: overrides }: IOverridesCellProps) => {
))} ))}
</> </>
} }
highlighted={
searchQuery.length > 0 &&
overrides
?.map(overrideToString)
.join('\n')
.toLowerCase()
.includes(searchQuery.toLowerCase())
}
> >
<StyledLink {overrides.length === 1
underline="always" ? '1 override'
highlighted={ : `${overrides.length} overrides`}
searchQuery.length > 0 && </TooltipLink>
overrides
?.map(overrideToString)
.join('\n')
.toLowerCase()
.includes(searchQuery.toLowerCase())
}
>
{overrides.length === 1
? '1 override'
: `${overrides.length} overrides`}
</StyledLink>
</HtmlTooltip>
</TextCell> </TextCell>
); );
}; };

View File

@ -1,8 +1,8 @@
import { Link, styled, Typography } from '@mui/material'; import { styled, Typography } from '@mui/material';
import { Highlighter } from 'component/common/Highlighter/Highlighter'; import { Highlighter } from 'component/common/Highlighter/Highlighter';
import { HtmlTooltip } from 'component/common/HtmlTooltip/HtmlTooltip';
import { TextCell } from 'component/common/Table/cells/TextCell/TextCell'; import { TextCell } from 'component/common/Table/cells/TextCell/TextCell';
import { useSearchHighlightContext } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext'; import { useSearchHighlightContext } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
import { TooltipLink } from 'component/common/TooltipLink/TooltipLink';
import { IPayload } from 'interfaces/featureToggle'; import { IPayload } from 'interfaces/featureToggle';
const StyledItem = styled(Typography)(({ theme }) => ({ const StyledItem = styled(Typography)(({ theme }) => ({
@ -10,12 +10,6 @@ const StyledItem = styled(Typography)(({ theme }) => ({
whiteSpace: 'pre-wrap', whiteSpace: 'pre-wrap',
})); }));
const StyledLink = styled(Link, {
shouldForwardProp: prop => prop !== 'highlighted',
})<{ highlighted?: boolean }>(({ theme, highlighted }) => ({
backgroundColor: highlighted ? theme.palette.highlight : 'transparent',
}));
interface IPayloadCellProps { interface IPayloadCellProps {
value?: IPayload; value?: IPayload;
} }
@ -35,8 +29,8 @@ export const PayloadCell = ({ value: payload }: IPayloadCellProps) => {
return ( return (
<TextCell> <TextCell>
<HtmlTooltip <TooltipLink
title={ tooltip={
<> <>
<StyledItem> <StyledItem>
<Highlighter search={searchQuery}> <Highlighter search={searchQuery}>
@ -45,19 +39,15 @@ export const PayloadCell = ({ value: payload }: IPayloadCellProps) => {
</StyledItem> </StyledItem>
</> </>
} }
highlighted={
searchQuery.length > 0 &&
payload.value
.toLowerCase()
.includes(searchQuery.toLowerCase())
}
> >
<StyledLink {payload.type}
underline="always" </TooltipLink>
highlighted={
searchQuery.length > 0 &&
payload.value
.toLowerCase()
.includes(searchQuery.toLowerCase())
}
>
{payload.type}
</StyledLink>
</HtmlTooltip>
</TextCell> </TextCell>
); );
}; };

View File

@ -8,12 +8,19 @@ export const ProjectRoleDescriptionEnvironmentPermissions = ({
permissions, permissions,
}: IProjectRoleDescriptionEnvironmentPermissionsProps) => ( }: IProjectRoleDescriptionEnvironmentPermissionsProps) => (
<> <>
{permissions {[
.filter((permission: any) => permission.environment === environment) ...new Set(
.map((permission: any) => permission.displayName) permissions
.filter(
(permission: any) =>
permission.environment === environment
)
.map((permission: any) => permission.displayName)
),
]
.sort() .sort()
.map((permission: any) => ( .map((permission: any) => (
<p key={permission}>{permission}</p> <p key={`${environment}-${permission}`}>{permission}</p>
))} ))}
</> </>
); );

View File

@ -1,19 +1,7 @@
import { Link, Popover, styled } from '@mui/material';
import { TextCell } from 'component/common/Table/cells/TextCell/TextCell'; import { TextCell } from 'component/common/Table/cells/TextCell/TextCell';
import React from 'react';
import { VFC } from 'react'; import { VFC } from 'react';
import { ProjectRoleDescription } from 'component/project/ProjectAccess/ProjectAccessAssign/ProjectRoleDescription/ProjectRoleDescription'; import { ProjectRoleDescription } from 'component/project/ProjectAccess/ProjectAccessAssign/ProjectRoleDescription/ProjectRoleDescription';
import { TooltipLink } from 'component/common/TooltipLink/TooltipLink';
const StyledLink = styled(Link)(() => ({
textDecoration: 'none',
'&:hover, &:focus': {
textDecoration: 'underline',
},
}));
const StyledPopover = styled(Popover)(() => ({
pointerEvents: 'none',
}));
interface IProjectAccessRoleCellProps { interface IProjectAccessRoleCellProps {
roleId: number; roleId: number;
@ -28,49 +16,25 @@ export const ProjectAccessRoleCell: VFC<IProjectAccessRoleCellProps> = ({
value, value,
emptyText, emptyText,
}) => { }) => {
const [anchorEl, setAnchorEl] = React.useState<HTMLElement | null>(null);
const onPopoverOpen = (event: React.MouseEvent<HTMLElement>) => {
setAnchorEl(event.currentTarget);
};
const onPopoverClose = () => {
setAnchorEl(null);
};
if (!value) return <TextCell>{emptyText}</TextCell>; if (!value) return <TextCell>{emptyText}</TextCell>;
return ( return (
<> <TextCell>
<TextCell> <TooltipLink
<StyledLink tooltip={
onMouseEnter={event => { <ProjectRoleDescription
onPopoverOpen(event); roleId={roleId}
}} projectId={projectId}
onMouseLeave={onPopoverClose} popover
> />
{value} }
</StyledLink> tooltipProps={{
</TextCell> maxWidth: 500,
<StyledPopover maxHeight: 600,
open={Boolean(anchorEl)}
anchorEl={anchorEl}
onClose={onPopoverClose}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'left',
}}
transformOrigin={{
vertical: 'top',
horizontal: 'left',
}} }}
> >
<ProjectRoleDescription {value}
roleId={roleId} </TooltipLink>
projectId={projectId} </TextCell>
popover
/>
</StyledPopover>
</>
); );
}; };