mirror of
https://github.com/Unleash/unleash.git
synced 2025-07-26 13:48:33 +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   - 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:
parent
005e5b1d15
commit
4286103850
@ -1,22 +1,16 @@
|
||||
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 { HtmlTooltip } from 'component/common/HtmlTooltip/HtmlTooltip';
|
||||
import { Highlighter } from 'component/common/Highlighter/Highlighter';
|
||||
import { useSearchHighlightContext } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
|
||||
import { IServiceAccount } from 'interfaces/service-account';
|
||||
import { LinkCell } from 'component/common/Table/cells/LinkCell/LinkCell';
|
||||
import { TooltipLink } from 'component/common/TooltipLink/TooltipLink';
|
||||
|
||||
const StyledItem = styled(Typography)(({ theme }) => ({
|
||||
fontSize: theme.fontSizes.smallerBody,
|
||||
}));
|
||||
|
||||
const StyledLink = styled(Link, {
|
||||
shouldForwardProp: prop => prop !== 'highlighted',
|
||||
})<{ highlighted?: boolean }>(({ theme, highlighted }) => ({
|
||||
backgroundColor: highlighted ? theme.palette.highlight : 'transparent',
|
||||
}));
|
||||
|
||||
interface IServiceAccountTokensCellProps {
|
||||
serviceAccount: IServiceAccount;
|
||||
value: string;
|
||||
@ -35,8 +29,8 @@ export const ServiceAccountTokensCell: VFC<IServiceAccountTokensCellProps> = ({
|
||||
|
||||
return (
|
||||
<TextCell>
|
||||
<HtmlTooltip
|
||||
title={
|
||||
<TooltipLink
|
||||
tooltip={
|
||||
<>
|
||||
{serviceAccount.tokens?.map(({ id, description }) => (
|
||||
<StyledItem key={id}>
|
||||
@ -47,19 +41,15 @@ export const ServiceAccountTokensCell: VFC<IServiceAccountTokensCellProps> = ({
|
||||
))}
|
||||
</>
|
||||
}
|
||||
highlighted={
|
||||
searchQuery.length > 0 &&
|
||||
value.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
}
|
||||
>
|
||||
<StyledLink
|
||||
underline="always"
|
||||
highlighted={
|
||||
searchQuery.length > 0 &&
|
||||
value.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
}
|
||||
>
|
||||
{serviceAccount.tokens?.length === 1
|
||||
? '1 token'
|
||||
: `${serviceAccount.tokens?.length} tokens`}
|
||||
</StyledLink>
|
||||
</HtmlTooltip>
|
||||
{serviceAccount.tokens?.length === 1
|
||||
? '1 token'
|
||||
: `${serviceAccount.tokens?.length} tokens`}
|
||||
</TooltipLink>
|
||||
</TextCell>
|
||||
);
|
||||
};
|
||||
|
@ -1,29 +1,44 @@
|
||||
import { styled, Tooltip, tooltipClasses, TooltipProps } from '@mui/material';
|
||||
import { SpacingArgument } from '@mui/system/createTheme/createSpacing';
|
||||
|
||||
const StyledHtmlTooltip = styled(({ className, ...props }: TooltipProps) => (
|
||||
<Tooltip {...props} classes={{ popper: className }} />
|
||||
))(({ theme }) => ({
|
||||
maxWidth: theme.spacing(37.5),
|
||||
[`& .${tooltipClasses.tooltip}`]: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
padding: theme.spacing(1, 1.5),
|
||||
borderRadius: theme.shape.borderRadiusMedium,
|
||||
boxShadow: theme.shadows[2],
|
||||
color: theme.palette.text.primary,
|
||||
fontWeight: theme.fontWeight.medium,
|
||||
maxWidth: 'inherit',
|
||||
border: `1px solid ${theme.palette.lightBorder}`,
|
||||
},
|
||||
[`& .${tooltipClasses.arrow}`]: {
|
||||
'&:before': {
|
||||
const StyledHtmlTooltip = styled(
|
||||
({ className, maxWidth, maxHeight, ...props }: IHtmlTooltipProps) => (
|
||||
<Tooltip {...props} classes={{ popper: className }} />
|
||||
),
|
||||
{
|
||||
shouldForwardProp: prop => prop !== 'maxWidth' && prop !== 'maxHeight',
|
||||
}
|
||||
)<{ maxWidth?: SpacingArgument; maxHeight?: SpacingArgument }>(
|
||||
({ theme, maxWidth, maxHeight }) => ({
|
||||
maxWidth: maxWidth || theme.spacing(37.5),
|
||||
[`& .${tooltipClasses.tooltip}`]: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
padding: theme.spacing(1, 1.5),
|
||||
borderRadius: theme.shape.borderRadiusMedium,
|
||||
boxShadow: theme.shadows[2],
|
||||
color: theme.palette.text.primary,
|
||||
fontWeight: theme.fontWeight.medium,
|
||||
maxWidth: 'inherit',
|
||||
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>
|
||||
);
|
||||
|
@ -1,21 +1,15 @@
|
||||
import { VFC } from 'react';
|
||||
import { FeatureSchema } from 'openapi';
|
||||
import { Link, styled, Typography } from '@mui/material';
|
||||
import { styled, Typography } from '@mui/material';
|
||||
import { TextCell } from '../TextCell/TextCell';
|
||||
import { HtmlTooltip } from 'component/common/HtmlTooltip/HtmlTooltip';
|
||||
import { Highlighter } from 'component/common/Highlighter/Highlighter';
|
||||
import { useSearchHighlightContext } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
|
||||
import { TooltipLink } from 'component/common/TooltipLink/TooltipLink';
|
||||
|
||||
const StyledTag = styled(Typography)(({ theme }) => ({
|
||||
fontSize: theme.fontSizes.smallerBody,
|
||||
}));
|
||||
|
||||
const StyledLink = styled(Link, {
|
||||
shouldForwardProp: prop => prop !== 'highlighted',
|
||||
})<{ highlighted?: boolean }>(({ theme, highlighted }) => ({
|
||||
backgroundColor: highlighted ? theme.palette.highlight : 'transparent',
|
||||
}));
|
||||
|
||||
interface IFeatureTagCellProps {
|
||||
row: {
|
||||
original: FeatureSchema;
|
||||
@ -31,8 +25,12 @@ export const FeatureTagCell: VFC<IFeatureTagCellProps> = ({ row, value }) => {
|
||||
|
||||
return (
|
||||
<TextCell>
|
||||
<HtmlTooltip
|
||||
title={
|
||||
<TooltipLink
|
||||
highlighted={
|
||||
searchQuery.length > 0 &&
|
||||
value.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
}
|
||||
tooltip={
|
||||
<>
|
||||
{row.original.tags?.map(tag => (
|
||||
<StyledTag key={tag.type + tag.value}>
|
||||
@ -44,18 +42,10 @@ export const FeatureTagCell: VFC<IFeatureTagCellProps> = ({ row, value }) => {
|
||||
</>
|
||||
}
|
||||
>
|
||||
<StyledLink
|
||||
underline="always"
|
||||
highlighted={
|
||||
searchQuery.length > 0 &&
|
||||
value.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
}
|
||||
>
|
||||
{row.original.tags?.length === 1
|
||||
? '1 tag'
|
||||
: `${row.original.tags?.length} tags`}
|
||||
</StyledLink>
|
||||
</HtmlTooltip>
|
||||
{row.original.tags?.length === 1
|
||||
? '1 tag'
|
||||
: `${row.original.tags?.length} tags`}
|
||||
</TooltipLink>
|
||||
</TextCell>
|
||||
);
|
||||
};
|
||||
|
34
frontend/src/component/common/TooltipLink/TooltipLink.tsx
Normal file
34
frontend/src/component/common/TooltipLink/TooltipLink.tsx
Normal 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>
|
||||
);
|
@ -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 { HtmlTooltip } from 'component/common/HtmlTooltip/HtmlTooltip';
|
||||
import { TextCell } from 'component/common/Table/cells/TextCell/TextCell';
|
||||
import { useSearchHighlightContext } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
|
||||
import { TooltipLink } from 'component/common/TooltipLink/TooltipLink';
|
||||
import { IOverride } from 'interfaces/featureToggle';
|
||||
|
||||
const StyledItem = styled(Typography)(({ theme }) => ({
|
||||
fontSize: theme.fontSizes.smallerBody,
|
||||
}));
|
||||
|
||||
const StyledLink = styled(Link, {
|
||||
shouldForwardProp: prop => prop !== 'highlighted',
|
||||
})<{ highlighted?: boolean }>(({ theme, highlighted }) => ({
|
||||
backgroundColor: highlighted ? theme.palette.highlight : 'transparent',
|
||||
}));
|
||||
|
||||
interface IOverridesCellProps {
|
||||
value?: IOverride[];
|
||||
}
|
||||
@ -29,8 +23,8 @@ export const OverridesCell = ({ value: overrides }: IOverridesCellProps) => {
|
||||
|
||||
return (
|
||||
<TextCell>
|
||||
<HtmlTooltip
|
||||
title={
|
||||
<TooltipLink
|
||||
tooltip={
|
||||
<>
|
||||
{overrides.map((override, 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
|
||||
underline="always"
|
||||
highlighted={
|
||||
searchQuery.length > 0 &&
|
||||
overrides
|
||||
?.map(overrideToString)
|
||||
.join('\n')
|
||||
.toLowerCase()
|
||||
.includes(searchQuery.toLowerCase())
|
||||
}
|
||||
>
|
||||
{overrides.length === 1
|
||||
? '1 override'
|
||||
: `${overrides.length} overrides`}
|
||||
</StyledLink>
|
||||
</HtmlTooltip>
|
||||
{overrides.length === 1
|
||||
? '1 override'
|
||||
: `${overrides.length} overrides`}
|
||||
</TooltipLink>
|
||||
</TextCell>
|
||||
);
|
||||
};
|
||||
|
@ -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 { HtmlTooltip } from 'component/common/HtmlTooltip/HtmlTooltip';
|
||||
import { TextCell } from 'component/common/Table/cells/TextCell/TextCell';
|
||||
import { useSearchHighlightContext } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
|
||||
import { TooltipLink } from 'component/common/TooltipLink/TooltipLink';
|
||||
import { IPayload } from 'interfaces/featureToggle';
|
||||
|
||||
const StyledItem = styled(Typography)(({ theme }) => ({
|
||||
@ -10,12 +10,6 @@ const StyledItem = styled(Typography)(({ theme }) => ({
|
||||
whiteSpace: 'pre-wrap',
|
||||
}));
|
||||
|
||||
const StyledLink = styled(Link, {
|
||||
shouldForwardProp: prop => prop !== 'highlighted',
|
||||
})<{ highlighted?: boolean }>(({ theme, highlighted }) => ({
|
||||
backgroundColor: highlighted ? theme.palette.highlight : 'transparent',
|
||||
}));
|
||||
|
||||
interface IPayloadCellProps {
|
||||
value?: IPayload;
|
||||
}
|
||||
@ -35,8 +29,8 @@ export const PayloadCell = ({ value: payload }: IPayloadCellProps) => {
|
||||
|
||||
return (
|
||||
<TextCell>
|
||||
<HtmlTooltip
|
||||
title={
|
||||
<TooltipLink
|
||||
tooltip={
|
||||
<>
|
||||
<StyledItem>
|
||||
<Highlighter search={searchQuery}>
|
||||
@ -45,19 +39,15 @@ export const PayloadCell = ({ value: payload }: IPayloadCellProps) => {
|
||||
</StyledItem>
|
||||
</>
|
||||
}
|
||||
highlighted={
|
||||
searchQuery.length > 0 &&
|
||||
payload.value
|
||||
.toLowerCase()
|
||||
.includes(searchQuery.toLowerCase())
|
||||
}
|
||||
>
|
||||
<StyledLink
|
||||
underline="always"
|
||||
highlighted={
|
||||
searchQuery.length > 0 &&
|
||||
payload.value
|
||||
.toLowerCase()
|
||||
.includes(searchQuery.toLowerCase())
|
||||
}
|
||||
>
|
||||
{payload.type}
|
||||
</StyledLink>
|
||||
</HtmlTooltip>
|
||||
{payload.type}
|
||||
</TooltipLink>
|
||||
</TextCell>
|
||||
);
|
||||
};
|
||||
|
@ -8,12 +8,19 @@ export const ProjectRoleDescriptionEnvironmentPermissions = ({
|
||||
permissions,
|
||||
}: IProjectRoleDescriptionEnvironmentPermissionsProps) => (
|
||||
<>
|
||||
{permissions
|
||||
.filter((permission: any) => permission.environment === environment)
|
||||
.map((permission: any) => permission.displayName)
|
||||
{[
|
||||
...new Set(
|
||||
permissions
|
||||
.filter(
|
||||
(permission: any) =>
|
||||
permission.environment === environment
|
||||
)
|
||||
.map((permission: any) => permission.displayName)
|
||||
),
|
||||
]
|
||||
.sort()
|
||||
.map((permission: any) => (
|
||||
<p key={permission}>{permission}</p>
|
||||
<p key={`${environment}-${permission}`}>{permission}</p>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
|
@ -1,19 +1,7 @@
|
||||
import { Link, Popover, styled } from '@mui/material';
|
||||
import { TextCell } from 'component/common/Table/cells/TextCell/TextCell';
|
||||
import React from 'react';
|
||||
import { VFC } from 'react';
|
||||
import { ProjectRoleDescription } from 'component/project/ProjectAccess/ProjectAccessAssign/ProjectRoleDescription/ProjectRoleDescription';
|
||||
|
||||
const StyledLink = styled(Link)(() => ({
|
||||
textDecoration: 'none',
|
||||
'&:hover, &:focus': {
|
||||
textDecoration: 'underline',
|
||||
},
|
||||
}));
|
||||
|
||||
const StyledPopover = styled(Popover)(() => ({
|
||||
pointerEvents: 'none',
|
||||
}));
|
||||
import { TooltipLink } from 'component/common/TooltipLink/TooltipLink';
|
||||
|
||||
interface IProjectAccessRoleCellProps {
|
||||
roleId: number;
|
||||
@ -28,49 +16,25 @@ export const ProjectAccessRoleCell: VFC<IProjectAccessRoleCellProps> = ({
|
||||
value,
|
||||
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>;
|
||||
|
||||
return (
|
||||
<>
|
||||
<TextCell>
|
||||
<StyledLink
|
||||
onMouseEnter={event => {
|
||||
onPopoverOpen(event);
|
||||
}}
|
||||
onMouseLeave={onPopoverClose}
|
||||
>
|
||||
{value}
|
||||
</StyledLink>
|
||||
</TextCell>
|
||||
<StyledPopover
|
||||
open={Boolean(anchorEl)}
|
||||
anchorEl={anchorEl}
|
||||
onClose={onPopoverClose}
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'left',
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'left',
|
||||
<TextCell>
|
||||
<TooltipLink
|
||||
tooltip={
|
||||
<ProjectRoleDescription
|
||||
roleId={roleId}
|
||||
projectId={projectId}
|
||||
popover
|
||||
/>
|
||||
}
|
||||
tooltipProps={{
|
||||
maxWidth: 500,
|
||||
maxHeight: 600,
|
||||
}}
|
||||
>
|
||||
<ProjectRoleDescription
|
||||
roleId={roleId}
|
||||
projectId={projectId}
|
||||
popover
|
||||
/>
|
||||
</StyledPopover>
|
||||
</>
|
||||
{value}
|
||||
</TooltipLink>
|
||||
</TextCell>
|
||||
);
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user