UI: Update tooltip/link structure & functionality in Software tables (#13893)
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
- Restored the ability to click on and select/copy text from software bundle tooltips while
|
||||
maintaining the abilities to click the software's name to get more details and to click anywhere
|
||||
else in the row to view all hosts with that software installed.
|
||||
@@ -3,6 +3,7 @@ import React from "react";
|
||||
|
||||
import { Link } from "react-router";
|
||||
import classnames from "classnames";
|
||||
import TooltipWrapper from "components/TooltipWrapper";
|
||||
|
||||
interface ILinkCellProps {
|
||||
value: string | JSX.Element;
|
||||
@@ -10,7 +11,7 @@ interface ILinkCellProps {
|
||||
className?: string;
|
||||
customOnClick?: (e: React.MouseEvent) => void;
|
||||
/** allows viewing overflow for tooltip */
|
||||
withTooltip?: boolean;
|
||||
tooltipContent?: string;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
@@ -21,21 +22,32 @@ const LinkCell = ({
|
||||
path,
|
||||
className,
|
||||
customOnClick,
|
||||
withTooltip,
|
||||
title,
|
||||
tooltipContent,
|
||||
}: ILinkCellProps): JSX.Element => {
|
||||
const cellClasses = classnames(
|
||||
baseClass,
|
||||
className,
|
||||
withTooltip && "link-cell-tooltip"
|
||||
);
|
||||
const cellClasses = classnames(baseClass, className);
|
||||
|
||||
const onClick = (e: React.MouseEvent): void => {
|
||||
customOnClick && customOnClick(e);
|
||||
};
|
||||
|
||||
return (
|
||||
<Link className={cellClasses} to={path} onClick={onClick} title={title}>
|
||||
return tooltipContent ? (
|
||||
<TooltipWrapper
|
||||
position="top"
|
||||
className="link-cell-tooltip-wrapper"
|
||||
tipContent={tooltipContent}
|
||||
>
|
||||
<Link className={cellClasses} to={path} onClick={onClick} title={title}>
|
||||
{value}
|
||||
</Link>
|
||||
</TooltipWrapper>
|
||||
) : (
|
||||
<Link
|
||||
className={cellClasses}
|
||||
to={path}
|
||||
onClick={customOnClick}
|
||||
title={title}
|
||||
>
|
||||
{value}
|
||||
</Link>
|
||||
);
|
||||
|
||||
@@ -241,21 +241,32 @@ $shadow-transition-width: 10px;
|
||||
}
|
||||
}
|
||||
// css to properly style link-cell with tooltip
|
||||
.link-cell-tooltip {
|
||||
.link-cell-tooltip-wrapper {
|
||||
overflow: visible; // fixes tooltip overflow cut off by cell
|
||||
white-space: nowrap; // single line
|
||||
margin: 0; // padding applied to .link-cell for larger clickable area
|
||||
.component__tooltip-wrapper {
|
||||
display: block;
|
||||
white-space: nowrap; // single line
|
||||
margin: 0; // padding applied to .link-cell for larger clickable area
|
||||
.component__tooltip-wrapper__element {
|
||||
&__element {
|
||||
display: block;
|
||||
white-space: nowrap; // single line
|
||||
text-overflow: ellipsis; // truncates text
|
||||
overflow: hidden;
|
||||
&__underline {
|
||||
width: 100%;
|
||||
|
||||
.component__tooltip-wrapper__underline {
|
||||
max-width: 100%; // fixes underline overflowing past truncated text
|
||||
&::after {
|
||||
bottom: 9px; // compensate for padding to make larger clickable area
|
||||
}
|
||||
}
|
||||
// TODO – this naming is now confusing, as this .link-cell is not the outermost layer of
|
||||
// the cell – it's a NameCell
|
||||
.link-cell {
|
||||
padding: 10px 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__tip-text {
|
||||
cursor: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import React from "react";
|
||||
import * as DOMPurify from "dompurify";
|
||||
|
||||
interface ITooltipWrapperProps {
|
||||
children: string;
|
||||
children: string | JSX.Element;
|
||||
tipContent: string;
|
||||
position?: "top" | "bottom";
|
||||
isDelayed?: boolean;
|
||||
@@ -33,11 +33,17 @@ const TooltipWrapper = ({
|
||||
<div className={classname} data-position={position}>
|
||||
<div className={`${baseClass}__element`}>
|
||||
{children}
|
||||
<div className={`${baseClass}__underline`} data-text={children} />
|
||||
<div
|
||||
className={`${baseClass}__element__underline`}
|
||||
data-text={children}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={tipClass}
|
||||
dangerouslySetInnerHTML={{ __html: sanitizedTipContent }}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -18,26 +18,31 @@
|
||||
position: static;
|
||||
display: inline; // treat like a span but allow other tags as children
|
||||
white-space: nowrap;
|
||||
}
|
||||
&__underline {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
|
||||
&::before {
|
||||
content: attr(data-text);
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
&::after {
|
||||
content: "";
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
&__underline {
|
||||
position: absolute;
|
||||
bottom: -2px;
|
||||
top: 0;
|
||||
left: 0;
|
||||
border-bottom: 1px dashed $ui-fleet-black-50;
|
||||
bottom: 0;
|
||||
|
||||
&::before {
|
||||
content: attr(data-text);
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
&::after {
|
||||
content: "";
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
bottom: -2px;
|
||||
left: 0;
|
||||
border-bottom: 1px dashed $ui-fleet-black-50;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
position: relative;
|
||||
z-index: 99;
|
||||
}
|
||||
}
|
||||
&__tip-text {
|
||||
|
||||
@@ -14,6 +14,7 @@ import TooltipWrapper from "components/TooltipWrapper";
|
||||
import ViewAllHostsLink from "components/ViewAllHostsLink";
|
||||
import { DEFAULT_EMPTY_CELL_VALUE } from "utilities/constants";
|
||||
import { COLORS } from "styles/var/colors";
|
||||
import { getSoftwareBundleTooltipMarkup } from "utilities/helpers";
|
||||
|
||||
interface IHeaderProps {
|
||||
column: {
|
||||
@@ -218,8 +219,10 @@ export const generateSoftwareTableHeaders = ({
|
||||
<LinkCell
|
||||
path={PATHS.SOFTWARE_DETAILS(id.toString())}
|
||||
customOnClick={onClickSoftware}
|
||||
value={bundle ? renderBundleTooltip(name, bundle) : name}
|
||||
withTooltip={!!bundle}
|
||||
value={name}
|
||||
tooltipContent={
|
||||
bundle ? getSoftwareBundleTooltipMarkup(bundle) : undefined
|
||||
}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -6,7 +6,10 @@ import ReactTooltip from "react-tooltip";
|
||||
import { formatSoftwareType, ISoftware } from "interfaces/software";
|
||||
import { IVulnerability } from "interfaces/vulnerability";
|
||||
import PATHS from "router/paths";
|
||||
import { formatFloatAsPercentage } from "utilities/helpers";
|
||||
import {
|
||||
formatFloatAsPercentage,
|
||||
getSoftwareBundleTooltipMarkup,
|
||||
} from "utilities/helpers";
|
||||
import { DEFAULT_EMPTY_CELL_VALUE } from "utilities/constants";
|
||||
|
||||
import HeaderCell from "components/TableContainer/DataTable/HeaderCell";
|
||||
@@ -65,23 +68,6 @@ const condenseVulnerabilities = (
|
||||
: condensed;
|
||||
};
|
||||
|
||||
const renderBundleTooltip = (name: string, bundle: string) => (
|
||||
<span className="name-container">
|
||||
<TooltipWrapper
|
||||
position="top"
|
||||
tipContent={`
|
||||
<span>
|
||||
<b>Bundle identifier: </b>
|
||||
<br />
|
||||
${bundle}
|
||||
</span>
|
||||
`}
|
||||
>
|
||||
{name}
|
||||
</TooltipWrapper>
|
||||
</span>
|
||||
);
|
||||
|
||||
const getMaxProbability = (vulns: IVulnerability[]) =>
|
||||
vulns.reduce(
|
||||
(max, { epss_probability }) => Math.max(max, epss_probability || 0),
|
||||
@@ -217,8 +203,10 @@ const generateTableHeaders = (
|
||||
<LinkCell
|
||||
path={PATHS.SOFTWARE_DETAILS(id.toString())}
|
||||
customOnClick={onClickSoftware}
|
||||
value={bundle ? renderBundleTooltip(name, bundle) : name}
|
||||
withTooltip={!!bundle}
|
||||
value={name}
|
||||
tooltipContent={
|
||||
bundle ? getSoftwareBundleTooltipMarkup(bundle) : undefined
|
||||
}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -900,6 +900,16 @@ export const getNextLocationPath = ({
|
||||
return queryString ? `/${nextLocation}?${queryString}` : `/${nextLocation}`;
|
||||
};
|
||||
|
||||
export const getSoftwareBundleTooltipMarkup = (bundle: string) => {
|
||||
return `
|
||||
<span>
|
||||
<b>Bundle identifier: </b>
|
||||
<br />
|
||||
${bundle}
|
||||
</span>
|
||||
`;
|
||||
};
|
||||
|
||||
export default {
|
||||
addGravatarUrlToResource,
|
||||
formatConfigDataForServer,
|
||||
|
||||
Reference in New Issue
Block a user