Refactor Tooltip Wrapper (#13845)

This commit is contained in:
Jacob Shandling 2023-11-07 13:15:49 -08:00 committed by GitHub
parent b112505bf1
commit bf8504a028
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
71 changed files with 735 additions and 544 deletions

View file

@ -239,7 +239,7 @@ const DownloadInstallers = ({
Include 
<TooltipWrapper
tipContent={
"<p>Include Fleet Desktop if yourre adding workstations.</p>"
<p>Include Fleet Desktop if you&apos;re adding workstations.</p>
}
>
Fleet Desktop

View file

@ -538,7 +538,7 @@ const PlatformWrapper = ({
Include&nbsp;
<TooltipWrapper
tipContent={
"Include Fleet Desktop if yourre adding workstations."
"Include Fleet Desktop if you're adding workstations."
}
>
Fleet Desktop

View file

@ -1,6 +1,5 @@
import React from "react";
import { render, screen } from "@testing-library/react";
import { renderWithSetup } from "test/test-utils";
import { fireEvent, render, screen } from "@testing-library/react";
import LastUpdatedText from ".";
@ -27,11 +26,9 @@ describe("Last updated text", () => {
});
it("renders tooltip on hover", async () => {
const { user } = renderWithSetup(
<LastUpdatedText whatToRetrieve="software" />
);
render(<LastUpdatedText whatToRetrieve="software" />);
await user.hover(screen.getByText("Updated never"));
await fireEvent.mouseEnter(screen.getByText("Updated never"));
expect(screen.getByText(/to retrieve software/i)).toBeInTheDocument();
});

View file

@ -27,7 +27,12 @@ const LastUpdatedText = ({
return (
<span className={baseClass}>
<TooltipWrapper
tipContent={`Fleet periodically queries all hosts <br />to retrieve ${whatToRetrieve}.`}
tipContent={
<>
Fleet periodically queries all hosts <br />
to retrieve {whatToRetrieve}.
</>
}
>
{`Updated ${lastUpdatedAt}`}
</TooltipWrapper>

View file

@ -399,7 +399,13 @@ const SelectTargets = ({
{onlinePercentage()}
%&nbsp;
<TooltipWrapper
tipContent={`Hosts are online if they<br /> have recently checked <br />into Fleet.`}
tipContent={
<>
Hosts are online if they <br />
have recently checked <br />
into Fleet.
</>
}
>
online
</TooltipWrapper>

View file

@ -49,30 +49,62 @@ const LogDestinationIndicator = ({
const tooltipText = () => {
switch (logDestination) {
case "filesystem":
return `Each time a query runs, the data is sent to <br />
return (
<>
Each time a query runs, the data is sent to <br />
/var/log/osquery/osqueryd.snapshots.log <br />
in each host&apos;s filesystem.`;
in each host&apos;s filesystem.
</>
);
case "firehose":
return `Each time a query runs, the data is sent to <br />
Amazon Kinesis Data Firehose.`;
return (
<>
Each time a query runs, the data is sent to <br />
Amazon Kinesis Data Firehose.`
</>
);
case "kinesis":
return `Each time a query runs, the data is sent to <br />
Amazon Kinesis Data Streams.`;
return (
<>
Each time a query runs, the data is sent to <br />
Amazon Kinesis Data Streams.
</>
);
case "lambda":
return `
Each time a query runs, the data <br />is sent to AWS Lambda.
`;
return (
<>
Each time a query runs, the data <br />
is sent to AWS Lambda.
</>
);
case "pubsub":
return `Each time a query runs, the data is <br />sent to Google Cloud Pub/Sub.`;
return (
<>
Each time a query runs, the data is <br /> sent to Google Cloud Pub
/ Sub.`
</>
);
case "kafta":
return `Each time a query runs, the data <br />is sent to Apache Kafka.`;
return (
<>
Each time a query runs, the data <br /> is sent to Apache Kafka.
</>
);
case "stdout":
return `Each time a query runs, the data is sent to <br />
standard output (stdout) on the Fleet server.`;
return (
<>
Each time a query runs, the data is sent to <br />
standard output(stdout) on the Fleet server.
</>
);
case "":
return "Please configure a log destination.";
return <>Please configure a log destination.</>;
default:
return "No additional information is available about this log destination.";
return (
<>
No additional information is available about this log destination.
</>
);
}
};

View file

@ -56,8 +56,12 @@ const PlatformCompatibility = ({
<span className={baseClass}>
<b>
<TooltipWrapper
tipContent="Estimated compatiblity based on <br /> the tables used in the query."
isDelayed
tipContent={
<>
Estimated compatiblity based on <br />
the tables used in the query.
</>
}
>
Compatible with:
</TooltipWrapper>
@ -73,8 +77,12 @@ const PlatformCompatibility = ({
<span className={baseClass}>
<b>
<TooltipWrapper
tipContent="Estimated compatiblity based on <br /> the tables used in the query."
isDelayed
tipContent={
<>
Estimated compatiblity based on <br /> the tables used in the
query.
</>
}
>
Compatible with:
</TooltipWrapper>

View file

@ -1,20 +1,17 @@
import React from "react";
import classnames from "classnames";
import TooltipWrapper from "components/TooltipWrapper";
interface IHeaderCellProps {
value: string | JSX.Element; // either a string or a TooltipWrapper
isSortedDesc?: boolean;
disableSortBy?: boolean;
isLastColumn?: boolean;
}
const HeaderCell = ({
value,
isSortedDesc,
disableSortBy,
isLastColumn = false,
}: IHeaderCellProps): JSX.Element => {
let sortArrowClass = "";
if (isSortedDesc === undefined) {
@ -25,23 +22,8 @@ const HeaderCell = ({
sortArrowClass = "ascending";
}
let lastColumnHeaderWithTooltipClass = "";
if (
typeof value !== "string" &&
value.type === TooltipWrapper &&
isLastColumn
) {
lastColumnHeaderWithTooltipClass = "last-col-header-with-tip";
}
return (
<div
className={classnames(
"header-cell",
sortArrowClass,
lastColumnHeaderWithTooltipClass
)}
>
<div className={classnames("header-cell", sortArrowClass)}>
<span>{value}</span>
{!disableSortBy && (
<div className="sort-arrows">

View file

@ -11,7 +11,7 @@ interface ILinkCellProps {
className?: string;
customOnClick?: (e: React.MouseEvent) => void;
/** allows viewing overflow for tooltip */
tooltipContent?: string;
tooltipContent?: string | React.ReactNode;
title?: string;
}
@ -33,7 +33,7 @@ const LinkCell = ({
return tooltipContent ? (
<TooltipWrapper
position="top"
position="top-start"
className="link-cell-tooltip-wrapper"
tipContent={tooltipContent}
>

View file

@ -248,17 +248,10 @@ $shadow-transition-width: 10px;
white-space: nowrap; // single line
text-overflow: ellipsis; // truncates text
overflow: hidden;
&__underline {
width: 100%;
&::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;
padding: 0;
}
}

View file

@ -1,7 +1,7 @@
# Tooltips Notes
This tooltip component was created to allow any content to be shown as a tooltip. You can place any
HTML inside of the `tipContent` prop. Also, very important, the `TooltipWrapper` is designed **ONLY**
JSX inside of the `tipContent` prop. Also, very important, the `TooltipWrapper` is designed **ONLY**
to wrap text so make sure to use static text or text returned from a function.
## Use cases
@ -24,11 +24,13 @@ You can even make the tooltip more dynamic HTML:
```jsx
<TooltipWrapper
tipContent={`
The &quot;snapshot&quot; key includes the query&apos;s results.
tipContent={
<>
The "snapshot" key includes the query's results.
<br />
These will be unique to your query.
`}
</>
}
>
The data sent to your configured log destination will look similar
to the following JSON:
@ -38,7 +40,7 @@ You can even make the tooltip more dynamic HTML:
**Within a form input element**
Inside a form input element, you only need to specify a `tooltip` prop for the input. This can be
text or HTML as mentioned before.
any JSX as mentioned before.
```jsx
<InputField
@ -53,9 +55,11 @@ text or HTML as mentioned before.
"Must include 12 characters, at least 1 number (e.g. 0 - 9), and at least 1 symbol (e.g. &*#)",
]}
blockAutoComplete
tooltip={`\
This password is temporary. This user will be asked to set a new password after logging in to the Fleet UI.<br /><br />
This user will not be asked to set a new password after logging in to fleetctl or the Fleet API.
`}
tooltip={
<>
This password is temporary. This user will be asked to set a new password after logging in to the Fleet UI.<br /><br />
This user will not be asked to set a new password after logging in to fleetctl or the Fleet API.
</>
}
/>
```

View file

@ -6,8 +6,8 @@ import TooltipWrapper from ".";
import "../../index.scss";
interface ITooltipWrapperProps {
children: string;
tipContent: string;
children: React.ReactNode;
tipContent: React.ReactNode;
}
export default {
@ -18,7 +18,20 @@ export default {
},
argTypes: {
position: {
options: ["top", "bottom"],
options: [
"top",
"top-start",
"top-end",
"right",
"right-start",
"right-end",
"bottom",
"bottom-start",
"bottom-end",
"left",
"left-start",
"left-end",
],
control: "radio",
},
},
@ -32,6 +45,10 @@ const Template: Story<ITooltipWrapperProps> = (props) => (
<br />
<br />
<TooltipWrapper {...props}>Example text</TooltipWrapper>
<br />
<br />
<br />
<br />
</>
);

View file

@ -1,52 +1,78 @@
import classnames from "classnames";
import React from "react";
import { Tooltip as ReactTooltip5, PlacesType } from "react-tooltip-5";
import * as DOMPurify from "dompurify";
import { uniqueId } from "lodash";
interface ITooltipWrapperProps {
children: string | JSX.Element;
tipContent: string;
/** Default: bottom */
position?: "top" | "bottom";
interface ITooltipWrapper {
children: React.ReactNode;
// default is bottom-start
position?: PlacesType;
isDelayed?: boolean;
underline?: boolean;
// Below two props used here to maintain the API of the old TooltipWrapper
// A clearer system would be to use the 3 below commented props, which describe exactly where they
// will apply, `element` being the element this tooltip will wrap. Associated logic is commented
// out, but ready to be used.
className?: string;
tooltipClass?: string;
// wrapperCustomClass?: string;
// elementCustomClass?: string;
// tipCustomClass?: string;
clickable?: boolean;
tipContent: React.ReactNode;
}
const baseClass = "component__tooltip-wrapper";
const TooltipWrapper = ({
// wrapperCustomClass,
// elementCustomClass,
// tipCustomClass,
children,
tipContent,
position = "bottom",
position = "bottom-start",
isDelayed,
underline = true,
className,
tooltipClass,
}: ITooltipWrapperProps): JSX.Element => {
const classname = classnames(baseClass, className);
const tipClass = classnames(`${baseClass}__tip-text`, tooltipClass, {
"delayed-tip": isDelayed,
clickable = true,
}: ITooltipWrapper) => {
const wrapperClassNames = classnames(baseClass, className, {
// [`${baseClass}__${wrapperCustomClass}`]: !!wrapperCustomClass,
});
const sanitizedTipContent = DOMPurify.sanitize(tipContent);
const elementClassNames = classnames(`${baseClass}__element`, {
// [`${baseClass}__${elementCustomClass}`]: !!elementCustomClass,
[`${baseClass}__underline`]: underline,
});
const tipClassNames = classnames(`${baseClass}__tip-text`, tooltipClass, {
// [`${baseClass}__${tipCustomClass}`]: !!tipCustomClass,
});
const tipId = uniqueId();
return (
<div className={classname} data-position={position}>
<div className={`${baseClass}__element`}>
<span className={wrapperClassNames}>
<div className={elementClassNames} data-tooltip-id={tipId}>
{children}
<div
className={`${baseClass}__element__underline`}
data-text={children}
/>
</div>
<div
className={tipClass}
dangerouslySetInnerHTML={{ __html: sanitizedTipContent }}
onClick={(e) => {
e.stopPropagation();
}}
/>
</div>
<ReactTooltip5
className={tipClassNames}
id={tipId}
delayShow={isDelayed ? 500 : undefined}
delayHide={isDelayed ? 500 : undefined}
noArrow
place={position}
opacity={1}
disableStyleInjection
clickable={clickable}
offset={5}
>
{tipContent}
</ReactTooltip5>
</span>
);
};

View file

@ -1,92 +1,30 @@
.component__tooltip-wrapper {
display: inline-flex;
position: relative;
&:hover {
.component__tooltip-wrapper__tip-text {
visibility: visible;
opacity: 1;
}
.delayed-tip {
transition: 300ms all;
transition-delay: 300ms;
}
&__underline {
position: relative;
width: fit-content;
// compensate for bottom border and padding to maintain centering
top: 2px;
border-bottom: 1px dashed $ui-fleet-black-50;
padding-bottom: 1px;
}
&__element {
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%;
position: absolute;
bottom: -2px;
left: 0;
border-bottom: 1px dashed $ui-fleet-black-50;
}
}
a {
position: relative;
z-index: 99;
}
}
&__tip-text {
width: max-content;
max-width: 360px;
padding: 6px;
color: $core-white;
background-color: $core-fleet-blue;
background-color: $tooltip-bg;
font-weight: $regular;
font-size: $xx-small;
border-radius: 4px;
position: absolute;
top: calc(100% + 6px);
left: 0;
box-sizing: border-box;
z-index: 99; // not more than the site nav
visibility: hidden;
opacity: 0;
transition: opacity 0.3s ease;
line-height: 1.375;
white-space: initial;
// invisible block to cover space so
// hover state can continue from text to bubble
&::before {
content: "";
width: 100%;
height: 6px;
position: absolute;
top: -6px;
left: 0;
}
p {
margin: 0;
}
}
&[data-position="top"] {
.component__tooltip-wrapper__tip-text {
top: auto;
bottom: 100%;
&::before {
display: none;
}
}
}
}

View file

@ -1,5 +1,5 @@
import React from "react";
import { render, screen } from "@testing-library/react";
import { render, screen, fireEvent } from "@testing-library/react";
import { renderWithSetup } from "test/test-utils";
import RevealButton from "./RevealButton";
@ -85,7 +85,7 @@ describe("Reveal button", () => {
/>
);
await user.hover(screen.getByText(SHOW_TEXT));
await fireEvent.mouseEnter(screen.getByText(SHOW_TEXT));
expect(screen.getByText(TOOLTIP_HTML)).toBeInTheDocument();
});

View file

@ -14,7 +14,7 @@ export interface IFormFieldProps {
label: Array<any> | JSX.Element | string;
name: string;
type: string;
tooltip?: string;
tooltip?: React.ReactNode;
}
const FormField = ({

View file

@ -57,11 +57,14 @@ class UserSettingsForm extends Component {
hint={renderEmailHint()}
disabled={!smtpConfigured}
tooltip={
"\
Editing your email address requires that SMTP or SES is configured in order to send a validation email.\
<br /><br /> \
Users with Admin role can configure SMTP in <strong>Settings &gt; Organization settings</strong>.\
"
<>
Editing your email address requires that SMTP or SES is
configured in order to send a validation email.
<br />
<br />
Users with Admin role can configure SMTP in{" "}
<strong>Settings &gt; Organization settings</strong>.
</>
}
/>
</div>

View file

@ -19,7 +19,7 @@ export interface ICheckboxProps {
wrapperClassName?: string;
indeterminate?: boolean;
parseTarget?: boolean;
tooltip?: string;
tooltipContent?: React.ReactNode;
isLeftLabel?: boolean;
}
@ -35,7 +35,7 @@ const Checkbox = (props: ICheckboxProps) => {
wrapperClassName,
indeterminate,
parseTarget,
tooltip,
tooltipContent,
isLeftLabel,
} = props;
@ -78,9 +78,9 @@ const Checkbox = (props: ICheckboxProps) => {
type="checkbox"
/>
<span className={checkBoxTickClass} />
{tooltip ? (
{tooltipContent ? (
<span className={`${baseClass}__label-tooltip tooltip`}>
<TooltipWrapper tipContent={tooltip}>
<TooltipWrapper tipContent={tooltipContent}>
{children as string}
</TooltipWrapper>
</span>

View file

@ -45,7 +45,7 @@ class InputFieldWithIcon extends InputField {
data-has-tooltip={!!tooltip}
>
{tooltip && !error ? (
<TooltipWrapper position="top" tipContent={tooltip}>
<TooltipWrapper position="top-start" tipContent={tooltip}>
{label}
</TooltipWrapper>
) : (

View file

@ -1,6 +1,6 @@
import React from "react";
import { noop } from "lodash";
import { render, screen } from "@testing-library/react";
import { render, screen, fireEvent } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import Radio from "./Radio";
@ -76,7 +76,7 @@ describe("Radio - component", () => {
expect(radioComponent).toHaveClass("disabled");
});
it("render a tooltip from the tooltip prop", () => {
it("render a tooltip from the tooltip prop", async () => {
render(
<Radio
disabled
@ -88,6 +88,7 @@ describe("Radio - component", () => {
/>
);
await fireEvent.mouseEnter(screen.getByText("Radio Label"));
const tooltip = screen.getByText("A Test Radio Tooltip");
expect(tooltip).toBeInTheDocument();
});

View file

@ -14,7 +14,7 @@ export interface IRadioProps {
name?: string;
className?: string;
disabled?: boolean;
tooltip?: string;
tooltip?: React.ReactNode;
testId?: string;
}

View file

@ -152,7 +152,17 @@ const generateTableHeaders = (
Header: () => {
return (
<div>
<TooltipWrapper tipContent="This is the average performance<br />impact across all hosts where<br />this query was scheduled.">
<TooltipWrapper
tipContent={
<>
This is the average performance
<br />
impact across all hosts where
<br />
this query was scheduled.
</>
}
>
Performance impact
</TooltipWrapper>
</div>

View file

@ -101,8 +101,13 @@ const QuertResultsHeading = ({
<span>
({`${percentResponded}% `}
<TooltipWrapper
tipContent={`
Hosts that respond may<br /> return results, errors, or <br />no results`}
tipContent={
<>
Hosts that respond may
<br /> return results, errors, or <br />
no results
</>
}
>
responded
</TooltipWrapper>
@ -120,7 +125,12 @@ const QuertResultsHeading = ({
{!isQueryFinished && (
<div className={`${baseClass}__tooltip`}>
<TooltipWrapper
tipContent={`The hosts distributed interval can <br/>impact live query response times.`}
tipContent={
<>
The hosts distributed interval can <br />
impact live query response times.
</>
}
>
Taking longer than 15 seconds?
</TooltipWrapper>

View file

@ -1,6 +1,6 @@
import React from "react";
import { noop } from "lodash";
import { render, screen } from "@testing-library/react";
import { render, screen, fireEvent } from "@testing-library/react";
import createMockOsqueryTable from "__mocks__/osqueryTableMock";
import QuerySidePanel from "./QuerySidePanel";
@ -10,7 +10,7 @@ describe("QuerySidePanel - component", () => {
render(
<QuerySidePanel
selectedOsqueryTable={createMockOsqueryTable()}
onOsqueryTableSelect={(tableName: string) => noop}
onOsqueryTableSelect={() => noop}
onClose={noop}
/>
);
@ -23,7 +23,7 @@ describe("QuerySidePanel - component", () => {
const { container } = render(
<QuerySidePanel
selectedOsqueryTable={createMockOsqueryTable()}
onOsqueryTableSelect={(tableName: string) => noop}
onOsqueryTableSelect={() => noop}
onClose={noop}
/>
);
@ -42,7 +42,7 @@ describe("QuerySidePanel - component", () => {
const { container } = render(
<QuerySidePanel
selectedOsqueryTable={createMockOsqueryTable()}
onOsqueryTableSelect={(tableName: string) => noop}
onOsqueryTableSelect={() => noop}
onClose={noop}
/>
);
@ -51,14 +51,15 @@ describe("QuerySidePanel - component", () => {
expect(platformList.length).toBe(11); // 2 columns are set to hidden
});
it("renders the platform specific column tooltip", () => {
it("renders the platform specific column tooltip", async () => {
render(
<QuerySidePanel
selectedOsqueryTable={createMockOsqueryTable()}
onOsqueryTableSelect={(tableName: string) => noop}
onOsqueryTableSelect={() => noop}
onClose={noop}
/>
);
await fireEvent.mouseEnter(screen.getByText("email"));
const tooltip = screen.getByText(/only available on chrome/i);
expect(tooltip).toBeInTheDocument();
@ -68,7 +69,7 @@ describe("QuerySidePanel - component", () => {
render(
<QuerySidePanel
selectedOsqueryTable={createMockOsqueryTable()}
onOsqueryTableSelect={(tableName: string) => noop}
onOsqueryTableSelect={() => noop}
onClose={noop}
/>
);
@ -87,7 +88,7 @@ describe("QuerySidePanel - component", () => {
selectedOsqueryTable={createMockOsqueryTable({
notes: "This table is being used for testing.",
})}
onOsqueryTableSelect={(tableName: string) => noop}
onOsqueryTableSelect={() => noop}
onClose={noop}
/>
);
@ -102,7 +103,7 @@ describe("QuerySidePanel - component", () => {
render(
<QuerySidePanel
selectedOsqueryTable={createMockOsqueryTable()}
onOsqueryTableSelect={(tableName: string) => noop}
onOsqueryTableSelect={() => noop}
onClose={noop}
/>
);

View file

@ -2,9 +2,9 @@ import React from "react";
import classnames from "classnames";
import { ColumnType, IQueryTableColumn } from "interfaces/osquery_table";
import { PLATFORM_DISPLAY_NAMES } from "utilities/constants";
import TooltipWrapper from "components/TooltipWrapper";
import { buildQueryStringFromParams } from "utilities/url";
import { OsqueryPlatform } from "interfaces/platform";
interface IColumnListItemProps {
column: IQueryTableColumn;
@ -24,22 +24,11 @@ const FOOTNOTES = {
* current tooltip only supports strings. we can change this when it support ReactNodes
* in the future.
*/
const createTooltipHtml = (
const renderTooltip = (
column: IQueryTableColumn,
selectedTableName: string
) => {
const toolTipHtml = [];
const descriptionHtml = `<span class="${baseClass}__column-description">${column.description}</span>`;
toolTipHtml.push(descriptionHtml);
if (column.required) {
toolTipHtml.push(
`<span class="${baseClass}__footnote">${FOOTNOTES.required}</span>`
);
}
if (column.requires_user_context) {
const renderUserContextFootnote = () => {
const queryString = buildQueryStringFromParams({
utm_source: "fleet-ui",
utm_table: `table-${selectedTableName}`,
@ -51,37 +40,47 @@ const createTooltipHtml = (
`${baseClass}__footnote-link`
);
toolTipHtml.push(
`<a href="${href}" target="__blank" class="${classNames}">${FOOTNOTES.requires_user_context}</a>`
return (
<a href={href} target="__blank" className={classNames}>
${FOOTNOTES.requires_user_context}
</a>
);
}
};
if (column.platforms?.length === 1) {
const platform = column.platforms[0];
toolTipHtml.push(
`<span class="${baseClass}__footnote">${FOOTNOTES.platform} ${platform}</span>`
const renderPlatformFootnotes = (columnPlatforms: OsqueryPlatform[]) => {
let platformsCopy;
switch (columnPlatforms.length) {
case 1:
platformsCopy = columnPlatforms[0];
break;
case 2:
platformsCopy = `${columnPlatforms[0]} and ${columnPlatforms[1]}`;
break;
case 3:
platformsCopy = `${columnPlatforms[0]}, ${columnPlatforms[1]}, and ${columnPlatforms[2]}`;
break;
default:
platformsCopy = columnPlatforms.join(", ");
}
return (
<span className={`${baseClass}__footnote`}>
{FOOTNOTES.platform} {platformsCopy}
</span>
);
}
};
if (column.platforms?.length === 2) {
const platform1 = PLATFORM_DISPLAY_NAMES[column.platforms[0]];
const platform2 = PLATFORM_DISPLAY_NAMES[column.platforms[1]];
toolTipHtml.push(
`<span class="${baseClass}__footnote">${FOOTNOTES.platform} ${platform1} and ${platform2}.</span>`
);
}
if (column.platforms?.length === 3) {
const platform1 = PLATFORM_DISPLAY_NAMES[column.platforms[0]];
const platform2 = PLATFORM_DISPLAY_NAMES[column.platforms[1]];
const platform3 = PLATFORM_DISPLAY_NAMES[column.platforms[2]];
toolTipHtml.push(
`<span class="${baseClass}__footnote">${FOOTNOTES.platform} ${platform1}, ${platform2}, and ${platform3}.</span>`
);
}
const tooltip = toolTipHtml.join("");
return tooltip;
return (
<>
<span className={`${baseClass}__column-description`}>
{column.description}
</span>
{column.required && (
<span className={`${baseClass}__footnote`}>{FOOTNOTES.required}</span>
)}
{column.requires_user_context && renderUserContextFootnote()}
{column.platforms && renderPlatformFootnotes(column.platforms)}
</>
);
};
const hasFootnotes = (column: IQueryTableColumn) => {
@ -113,7 +112,7 @@ const ColumnListItem = ({
<div className={`${baseClass}__name-wrapper`}>
<span className={columnNameClasses}>
<TooltipWrapper
tipContent={createTooltipHtml(column, selectedTableName)}
tipContent={renderTooltip(column, selectedTableName)}
className={`${baseClass}__tooltip`}
>
{column.name}

View file

@ -52,7 +52,7 @@
margin: $pad-small 0;
display: block;
&:last-child {
&:last-of-type {
margin-bottom: 0;
}
}

View file

@ -4,7 +4,6 @@ export type IDataColumn = Column & {
title?: string;
disableHidden?: boolean;
disableSortBy?: boolean;
isLastColumn?: boolean;
filterValue?: any;
preFilteredRows?: any;
setFilter?: any;

View file

@ -1,6 +1,6 @@
import React from "react";
import { render, screen } from "@testing-library/react";
import { fireEvent, render, screen } from "@testing-library/react";
import { renderWithSetup } from "test/test-utils";
import paths from "router/paths";
import SummaryTile from "./SummaryTile";
@ -102,7 +102,7 @@ describe("SummaryTile - component", () => {
/>
);
await user.hover(screen.getByText("Windows hosts"));
await fireEvent.mouseEnter(screen.getByText("Windows hosts"));
expect(screen.getByText("Hosts on any Windows device")).toBeInTheDocument();
});

View file

@ -2,12 +2,12 @@ import React from "react";
import { Link } from "react-router";
import { kebabCase } from "lodash";
import TooltipWrapper from "components/TooltipWrapper";
import Icon from "components/Icon";
import { IconNames } from "components/icons";
import PremiumFeatureIconWithTooltip from "components/PremiumFeatureIconWithTooltip";
import classnames from "classnames";
import { Colors } from "styles/var/colors";
import TooltipWrapper from "components/TooltipWrapper";
interface ISummaryTileProps {
count: number;
@ -52,7 +52,6 @@ const SummaryTile = ({
const classes = classnames(`${baseClass}__tile`, `${kebabCase(title)}-tile`, {
[`${baseClass}__not-supported`]: notSupported,
});
const tile = (
<>
<div className={circledIcon ? `${baseClass}__circled-icon` : ""}>

View file

@ -54,7 +54,7 @@ export const generateStatusTableHeaders = (teamId?: number): IDataColumn[] => [
accessor: "status",
Cell: (cellProps: IStringCellProps) => (
<TooltipWrapper
position="top"
position="top-start"
tipContent={MDM_STATUS_TOOLTIP[cellProps.cell.value]}
>
{cellProps.cell.value}

View file

@ -41,9 +41,9 @@ const generateMunkiIssuesTableHeaders = (teamId?: number): IDataColumn[] => [
Header: (): JSX.Element => {
const titleWithToolTip = (
<TooltipWrapper
tipContent={`
Issues reported the last time Munki ran on each host.
`}
tipContent={
<>Issues reported the last time Munki ran on each host.</>
}
>
Issue
</TooltipWrapper>

View file

@ -201,7 +201,7 @@ const AppleBusinessManagerSection = ({
<div className={`${baseClass}__section-information`}>
<h4>
<TooltipWrapper
position="top"
position="top-start"
tipContent="macOS hosts will be added to this team when theyre first unboxed."
>
Team

View file

@ -226,12 +226,13 @@ const IntegrationForm = ({
parseTarget
value={projectKey}
tooltip={
"\
To find the Jira project key, head to your project in <br /> \
Jira. Your project key is located in the URL. For example, in <br /> \
jira.example.com/projects/JRAEXAMPLE, <br /> \
JRAEXAMPLE is your project key. \
"
<>
To find the Jira project key, head to your project in <br />
Jira. Your project key is located in the URL. For example, in{" "}
<br />
jira.example.com/projects/JRAEXAMPLE, <br />
JRAEXAMPLE is your project key.
</>
}
/>
) : (
@ -244,11 +245,15 @@ const IntegrationForm = ({
parseTarget
value={groupId === 0 ? null : groupId}
tooltip={
"\
To find the Zendesk group ID, select <b>Admin > <br /> \
People > Groups</b>. Find the group and select it. <br /> \
The group ID will appear in the search field. \
"
<>
To find the Zendesk group ID, select{" "}
<b>
Admin &gt; <br />
People &gt; Groups
</b>
. Find the group and select it. <br />
The group ID will appear in the search field.
</>
}
/>
)}

View file

@ -115,7 +115,13 @@ const Advanced = ({
value={domain}
parseTarget
tooltip={
'<p>If you need to specify a HELO domain, <br />you can do it here <em className="hint hint--brand">(Default: <strong>Blank</strong>)</em></p>'
<p>
If you need to specify a HELO domain, <br />
you can do it here{" "}
<em className="hint hint--brand">
(Default: <strong>Blank</strong>)
</em>
</p>
}
/>
<Checkbox
@ -123,8 +129,15 @@ const Advanced = ({
name="verifySSLCerts"
value={verifySSLCerts}
parseTarget
tooltip={
'<p>Turn this off (not recommended) <br />if you use a self-signed certificate <em className="hint hint--brand"><br />(Default: <strong>On</strong>)</em></p>'
tooltipContent={
<p>
Turn this off (not recommended) <br />
if you use a self-signed certificate{" "}
<em className="hint hint--brand">
<br />
(Default: <strong>On</strong>)
</em>
</p>
}
>
Verify SSL certs
@ -134,8 +147,15 @@ const Advanced = ({
name="enableStartTLS"
value={enableStartTLS}
parseTarget
tooltip={
'<p>Detects if STARTTLS is enabled <br />in your SMTP server and starts <br />to use it. <em className="hint hint--brand">(Default: <strong>On</strong>)</em></p>'
tooltipContent={
<p>
Detects if STARTTLS is enabled <br />
in your SMTP server and starts <br />
to use it.{" "}
<em className="hint hint--brand">
(Default: <strong>On</strong>)
</em>
</p>
}
>
Enable STARTTLS
@ -145,8 +165,15 @@ const Advanced = ({
name="enableHostExpiry"
value={enableHostExpiry}
parseTarget
tooltip={
'<p>When enabled, allows automatic cleanup <br />of hosts that have not communicated with Fleet <br />in some number of days. <em className="hint hint--brand">(Default: <strong>Off</strong>)</em></p>'
tooltipContent={
<p>
When enabled, allows automatic cleanup <br />
of hosts that have not communicated with Fleet <br />
in some number of days.{" "}
<em className="hint hint--brand">
(Default: <strong>Off</strong>)
</em>
</p>
}
>
Host expiry
@ -161,8 +188,11 @@ const Advanced = ({
parseTarget
onBlur={validateForm}
error={formErrors.host_expiry_window}
tooltip={
"<p>If a host has not communicated with Fleet in the specified number of days, it will be removed.</p>"
tooltipContent={
<p>
If a host has not communicated with Fleet in the specified
number of days, it will be removed.
</p>
}
/>
<Checkbox
@ -170,8 +200,15 @@ const Advanced = ({
name="disableLiveQuery"
value={disableLiveQuery}
parseTarget
tooltip={
'<p>When enabled, disables the ability to run live queries <br />(ad hoc queries executed via the UI or fleetctl). <em className="hint hint--brand">(Default: <strong>Off</strong>)</em></p>'
tooltipContent={
<p>
When enabled, disables the ability to run live queries{" "}
<br />
(ad hoc queries executed via the UI or fleetctl).{" "}
<em className="hint hint--brand">
(Default: <strong>Off</strong>)
</em>
</p>
}
>
Disable live queries
@ -181,15 +218,23 @@ const Advanced = ({
name="disableQueryReports"
value={disableQueryReports}
parseTarget
// TODO - update to JSX once tooltip wrapper refactor is merged
// TODO - once refactor is merged, have this and bove tooltips disappear more
// quickly to get out of users' way
tooltip={
'<p>Disabling query reports will decrease database usage, <br />\
but will prevent you from accessing query results in<br /> \
Fleet and will delete existing reports. This can also be<br />\
disabled on a per-query basis by enabling "Discard <br />\
data". <em>(Default: <b>Off</b>)</em></p>'
tooltipContent={
<>
<p>
Disabling query reports will decrease database usage,{" "}
<br />\ but will prevent you from accessing query results
in
<br /> \ Fleet and will delete existing reports. This can
also be
<br />\ disabled on a per-query basis by enabling
&quot;Discard <br />\ data&quot;.{" "}
<em>
(Default: <b>Off</b>)
</em>
</p>
</>
}
>
Disable query reports

View file

@ -181,9 +181,10 @@ const HostStatusWebhook = ({
onBlur={validateForm}
error={formErrors.destination_url}
tooltip={
"\
<p>Provide a URL to deliver <br/>the webhook request to.</p>\
"
<p>
Provide a URL to deliver <br />
the webhook request to.
</p>
}
/>
</div>
@ -197,9 +198,13 @@ const HostStatusWebhook = ({
parseTarget
onBlur={validateForm}
tooltip={
"\
<p>Select the minimum percentage of hosts that<br/>must fail to check into Fleet in order to trigger<br/>the webhook request.</p>\
"
<p>
Select the minimum percentage of hosts that
<br />
must fail to check into Fleet in order to trigger
<br />
the webhook request.
</p>
}
/>
</div>
@ -213,9 +218,15 @@ const HostStatusWebhook = ({
parseTarget
onBlur={validateForm}
tooltip={
"\
<p>Select the minimum number of days that the<br/>configured <b>Percentage of hosts</b> must fail to<br/>check into Fleet in order to trigger the<br/>webhook request.</p>\
"
<p>
Select the minimum number of days that the
<br />
configured <b>Percentage of hosts</b> must fail to
<br />
check into Fleet in order to trigger the
<br />
webhook request.
</p>
}
/>
</div>

View file

@ -248,11 +248,21 @@ const Smtp = ({
value={smtpAuthenticationType}
parseTarget
tooltip={
"\
<p>If your mail server requires authentication, you need to specify the authentication type here.</p> \
<p><strong>No Authentication</strong> - Select this if your SMTP is open.</p> \
<p><strong>Username & Password</strong> - Select this if your SMTP server requires authentication with a username and password.</p>\
"
<>
<p>
If your mail server requires authentication, you need to
specify the authentication type here.
</p>
<p>
<strong>No Authentication</strong> - Select this if your SMTP
is open.
</p>
<p>
<strong>Username & Password</strong> - Select this if your
SMTP server requires authentication with a username and
password.
</p>
</>
}
/>
{renderSmtpSection()}

View file

@ -171,7 +171,8 @@ const Sso = ({
parseTarget
onBlur={validateForm}
error={formErrors.idp_image_url}
tooltip="An optional link to an image such <br/>as a logo for the identity provider."
tooltip={`An optional link to an image such
as a logo for the identity provider.`}
/>
</div>
<div className={`${baseClass}__inputs`}>
@ -184,7 +185,8 @@ const Sso = ({
parseTarget
onBlur={validateForm}
error={formErrors.metadata}
tooltip="Metadata provided by the identity provider. Either<br/> metadata or a metadata url must be provided."
tooltip={`Metadata provided by the identity provider. Either
metadata or a metadata url must be provided.`}
/>
</div>
<div className={`${baseClass}__inputs`}>

View file

@ -125,12 +125,16 @@ const generateTableHeaders = (
if (cellProps.cell.value === "GitOps") {
return (
<TooltipWrapper
position="top"
tipContent={`
The GitOps role is only available on the command-line<br/>
when creating an API-only user. This user has no<br/>
access to the UI.
`}
position="top-start"
tipContent={
<>
The GitOps role is only available on the command-line
<br />
when creating an API-only user. This user has no
<br />
access to the UI.
</>
}
>
GitOps
</TooltipWrapper>
@ -139,12 +143,16 @@ const generateTableHeaders = (
if (cellProps.cell.value === "Observer+") {
return (
<TooltipWrapper
position="top"
tipContent={`
Users with the Observer+ role have access to all of<br/>
the same functions as an Observer, with the added<br/>
ability to run any live query against all hosts.
`}
position="top-start"
tipContent={
<>
Users with the Observer+ role have access to all of
<br />
the same functions as an Observer, with the added
<br />
ability to run any live query against all hosts.
</>
}
>
{cellProps.cell.value}
</TooltipWrapper>

View file

@ -5,7 +5,6 @@ import PATHS from "router/paths";
import { NotificationContext } from "context/notification";
import { ITeam } from "interfaces/team";
import { IUserFormErrors, UserRole } from "interfaces/user";
import { IRole } from "interfaces/role";
import Button from "components/buttons/Button";
import validatePresence from "components/forms/validators/validate_presence";
@ -398,11 +397,14 @@ const UserForm = ({
value={formData.email || ""}
disabled={!isNewUser && !(smtpConfigured || sesConfigured)}
tooltip={
"\
Editing an email address requires that SMTP or SES is configured in order to send a validation email. \
<br /><br /> \
Users with Admin role can configure SMTP in <strong>Settings &gt; Organization settings</strong>. \
"
<>
Editing an email address requires that SMTP or SES is configured in
order to send a validation email.
<br />
<br />
Users with Admin role can configure SMTP in{" "}
<strong>Settings &gt; Organization settings</strong>.
</>
}
/>
{!isNewUser &&
@ -434,11 +436,16 @@ const UserForm = ({
value={canUseSso && formData.sso_enabled}
disabled={!canUseSso}
wrapperClassName={`${baseClass}__invite-admin`}
tooltip={`
Enabling single sign-on for a user requires that SSO is first enabled for the organization.
<br /><br />
Users with Admin role can configure SSO in <strong>Settings &gt; Organization settings</strong>.
`}
tooltipContent={
<>
Enabling single sign-on for a user requires that SSO is first
enabled for the organization.
<br />
<br />
Users with Admin role can configure SSO in{" "}
<strong>Settings &gt; Organization settings</strong>.
</>
}
>
Enable single sign-on
</Checkbox>
@ -470,14 +477,18 @@ const UserForm = ({
name={"newUserType"}
onChange={onRadioChange("newUserType")}
tooltip={
smtpConfigured || sesConfigured
? ""
: `
The &quot;Invite user&quot; feature requires that SMTP or SES
is configured in order to send invitation emails.
<br /><br />
SMTP can be configured in Settings &gt; Organization settings.
`
smtpConfigured || sesConfigured ? (
""
) : (
<>
The &quot;Invite user&quot; feature requires that SMTP
or SES is configured in order to send invitation emails.
<br />
<br />
SMTP can be configured in Settings &gt; Organization
settings.
</>
)
}
/>
</>
@ -506,10 +517,16 @@ const UserForm = ({
"Must include 12 characters, at least 1 number (e.g. 0 - 9), and at least 1 symbol (e.g. &*#)",
]}
blockAutoComplete
tooltip={`\
This password is temporary. This user will be asked to set a new password after logging in to the Fleet UI.<br /><br />\
This user will not be asked to set a new password after logging in to fleetctl or the Fleet API.\
`}
tooltip={
<>
This password is temporary. This user will be asked to
set a new password after logging in to the Fleet UI.
<br />
<br />
This user will not be asked to set a new password after
logging in to fleetctl or the Fleet API.
</>
}
/>
</div>
</>

View file

@ -131,12 +131,16 @@ const generateTableHeaders = (
if (cellProps.cell.value === "GitOps") {
return (
<TooltipWrapper
position="top"
tipContent={`
The GitOps role is only available on the command-line<br/>
when creating an API-only user. This user has no<br/>
access to the UI.
`}
position="top-start"
tipContent={
<>
The GitOps role is only available on the command-line
<br />
when creating an API-only user. This user has no
<br />
access to the UI.
</>
}
>
GitOps
</TooltipWrapper>
@ -145,12 +149,16 @@ const generateTableHeaders = (
if (cellProps.cell.value === "Observer+") {
return (
<TooltipWrapper
position="top"
tipContent={`
Users with the Observer+ role have access to all of<br/>
the same functions as an Observer, with the added<br/>
ability to run any live query against all hosts.
`}
position="top-start"
tipContent={
<>
Users with the Observer+ role have access to all of
<br />
the same functions as an Observer, with the added
<br />
ability to run any live query against all hosts.
</>
}
>
{cellProps.cell.value}
</TooltipWrapper>

View file

@ -47,7 +47,6 @@ interface IHeaderProps {
column: {
title: string;
isSortedDesc: boolean;
isLastColumn?: boolean;
};
getToggleAllRowsSelectedProps: () => IGetToggleAllRowsSelectedProps;
toggleAllRowsSelected: () => void;
@ -149,7 +148,6 @@ const allHostTableHeaders: IDataColumn[] = [
<HeaderCell
value={cellProps.column.title}
isSortedDesc={cellProps.column.isSortedDesc}
isLastColumn={cellProps.column.isLastColumn}
/>
),
accessor: "display_name",
@ -208,7 +206,6 @@ const allHostTableHeaders: IDataColumn[] = [
<HeaderCell
value={cellProps.column.title}
isSortedDesc={cellProps.column.isSortedDesc}
isLastColumn={cellProps.column.isLastColumn}
/>
),
accessor: "hostname",
@ -220,7 +217,6 @@ const allHostTableHeaders: IDataColumn[] = [
<HeaderCell
value={cellProps.column.title}
isSortedDesc={cellProps.column.isSortedDesc}
isLastColumn={cellProps.column.isLastColumn}
/>
),
accessor: "computer_name",
@ -232,7 +228,6 @@ const allHostTableHeaders: IDataColumn[] = [
<HeaderCell
value={cellProps.column.title}
isSortedDesc={cellProps.column.isSortedDesc}
isLastColumn={cellProps.column.isLastColumn}
/>
),
accessor: "team_name",
@ -245,11 +240,13 @@ const allHostTableHeaders: IDataColumn[] = [
Header: (cellProps: IHeaderProps): JSX.Element => {
const titleWithToolTip = (
<TooltipWrapper
tipContent={`
Online hosts will respond to a live query. Offline<br/>
hosts wont respond to a live query because<br/>
they may be shut down, asleep, or not<br/>
connected to the internet.`}
tipContent={
<>
Online hosts will respond to a live query. Offline hosts wont
respond to a live query because they may be shut down, asleep, or
not connected to the internet.
</>
}
className="status-header"
>
Status
@ -259,7 +256,6 @@ const allHostTableHeaders: IDataColumn[] = [
<HeaderCell
value={cellProps.rows.length === 1 ? "Status" : titleWithToolTip}
disableSortBy
isLastColumn={cellProps.column.isLastColumn}
/>
);
},
@ -291,7 +287,6 @@ const allHostTableHeaders: IDataColumn[] = [
<HeaderCell
value={cellProps.column.title}
isSortedDesc={cellProps.column.isSortedDesc}
isLastColumn={cellProps.column.isLastColumn}
/>
),
accessor: "gigs_disk_space_available",
@ -321,7 +316,6 @@ const allHostTableHeaders: IDataColumn[] = [
<HeaderCell
value={cellProps.column.title}
isSortedDesc={cellProps.column.isSortedDesc}
isLastColumn={cellProps.column.isLastColumn}
/>
),
accessor: "os_version",
@ -382,7 +376,6 @@ const allHostTableHeaders: IDataColumn[] = [
<HeaderCell
value={cellProps.column.title}
isSortedDesc={cellProps.column.isSortedDesc}
isLastColumn={cellProps.column.isLastColumn}
/>
),
accessor: "primary_ip",
@ -393,21 +386,18 @@ const allHostTableHeaders: IDataColumn[] = [
Header: (cellProps: IHeaderProps): JSX.Element => {
const titleWithToolTip = (
<TooltipWrapper
tipContent={`
Settings can be updated remotely on hosts with MDM turned<br/>
on. To filter by MDM status, head to the Dashboard page.
`}
tipContent={
<>
Settings can be updated remotely on hosts with MDM turned
<br />
on. To filter by MDM status, head to the Dashboard page.
</>
}
>
MDM status
</TooltipWrapper>
);
return (
<HeaderCell
value={titleWithToolTip}
isLastColumn={cellProps.column.isLastColumn}
disableSortBy
/>
);
return <HeaderCell value={titleWithToolTip} disableSortBy />;
},
disableSortBy: true,
accessor: "mdm.enrollment_status",
@ -427,21 +417,18 @@ const allHostTableHeaders: IDataColumn[] = [
Header: (cellProps: IHeaderProps): JSX.Element => {
const titleWithToolTip = (
<TooltipWrapper
tipContent={`
The MDM server that updates settings on the host. To<br/>
filter by MDM server URL, head to the Dashboard page.
`}
tipContent={
<>
The MDM server that updates settings on the host. To
<br />
filter by MDM server URL, head to the Dashboard page.
</>
}
>
MDM server URL
</TooltipWrapper>
);
return (
<HeaderCell
value={titleWithToolTip}
isLastColumn={cellProps.column.isLastColumn}
disableSortBy
/>
);
return <HeaderCell value={titleWithToolTip} disableSortBy />;
},
disableSortBy: true,
accessor: "mdm.server_url",
@ -462,7 +449,6 @@ const allHostTableHeaders: IDataColumn[] = [
<HeaderCell
value={cellProps.column.title}
isSortedDesc={cellProps.column.isSortedDesc}
isLastColumn={cellProps.column.isLastColumn}
/>
),
accessor: "public_ip",
@ -506,9 +492,12 @@ const allHostTableHeaders: IDataColumn[] = [
Header: (cellProps: IHeaderProps): JSX.Element => {
const titleWithToolTip = (
<TooltipWrapper
tipContent={`
The last time the host<br/> reported vitals.
`}
tipContent={
<>
The last time the host
<br /> reported vitals.
</>
}
>
Last fetched
</TooltipWrapper>
@ -517,7 +506,6 @@ const allHostTableHeaders: IDataColumn[] = [
<HeaderCell
value={titleWithToolTip}
isSortedDesc={cellProps.column.isSortedDesc}
isLastColumn={cellProps.column.isLastColumn}
/>
);
},
@ -534,9 +522,12 @@ const allHostTableHeaders: IDataColumn[] = [
Header: (cellProps: IHeaderProps): JSX.Element => {
const titleWithToolTip = (
<TooltipWrapper
tipContent={`
The last time the <br/>host was online.
`}
tipContent={
<>
The last time the <br />
host was online.
</>
}
>
Last seen
</TooltipWrapper>
@ -545,7 +536,6 @@ const allHostTableHeaders: IDataColumn[] = [
<HeaderCell
value={titleWithToolTip}
isSortedDesc={cellProps.column.isSortedDesc}
isLastColumn={cellProps.column.isLastColumn}
/>
);
},
@ -563,7 +553,6 @@ const allHostTableHeaders: IDataColumn[] = [
<HeaderCell
value={cellProps.column.title}
isSortedDesc={cellProps.column.isSortedDesc}
isLastColumn={cellProps.column.isLastColumn}
/>
),
accessor: "uuid",
@ -577,7 +566,6 @@ const allHostTableHeaders: IDataColumn[] = [
<HeaderCell
value={cellProps.column.title}
isSortedDesc={cellProps.column.isSortedDesc}
isLastColumn={cellProps.column.isLastColumn}
/>
),
accessor: "uptime",
@ -610,7 +598,6 @@ const allHostTableHeaders: IDataColumn[] = [
<HeaderCell
value={cellProps.column.title}
isSortedDesc={cellProps.column.isSortedDesc}
isLastColumn={cellProps.column.isLastColumn}
/>
),
accessor: "memory",
@ -624,7 +611,6 @@ const allHostTableHeaders: IDataColumn[] = [
<HeaderCell
value={cellProps.column.title}
isSortedDesc={cellProps.column.isSortedDesc}
isLastColumn={cellProps.column.isLastColumn}
/>
),
accessor: "primary_mac",
@ -636,7 +622,6 @@ const allHostTableHeaders: IDataColumn[] = [
<HeaderCell
value={cellProps.column.title}
isSortedDesc={cellProps.column.isSortedDesc}
isLastColumn={cellProps.column.isLastColumn}
/>
),
accessor: "hardware_serial",
@ -648,7 +633,6 @@ const allHostTableHeaders: IDataColumn[] = [
<HeaderCell
value={cellProps.column.title}
isSortedDesc={cellProps.column.isSortedDesc}
isLastColumn={cellProps.column.isLastColumn}
/>
),
accessor: "hardware_model",

View file

@ -1412,12 +1412,6 @@ const ManageHostsPage = ({
isOnlyObserver || (!isOnGlobalTeam && !isTeamMaintainerOrTeamAdmin),
});
// Update last column
tableColumns.forEach((dataColumn) => {
dataColumn.isLastColumn = false;
});
tableColumns[tableColumns.length - 1].isLastColumn = true;
const emptyState = () => {
const emptyHosts: IEmptyTableProps = {
header: "No hosts match the current criteria",

View file

@ -200,24 +200,6 @@
overflow-x: scroll;
}
&__table {
thead {
tr {
th {
.last-col-header-with-tip {
min-width: 90px;
.component__tooltip-wrapper__tip-text {
left: -126px;
}
}
.status-header {
.component__tooltip-wrapper__tip-text {
left: -220px;
}
}
}
}
}
tbody {
.issues {
&__cell {

View file

@ -103,7 +103,6 @@ const About = ({
<span className="info-grid__header">MDM status</span>
<span className="info-grid__data">
<TooltipWrapper
position="bottom"
tipContent={MDM_STATUS_TOOLTIP[mdm.enrollment_status]}
>
{mdm.enrollment_status}

View file

@ -48,7 +48,6 @@ const AgentOptions = ({
{isChromeOS ? (
<TooltipWrapper
tipContent={CHROMEOS_AGENT_OPTIONS_TOOLTIP_MESSAGE}
position="bottom"
className="section__header"
>
Agent options

View file

@ -176,7 +176,7 @@ const HostSummary = ({
return (
<div className="info-flex__item info-flex__item--title">
<span className="info-flex__header">Disk encryption</span>
<TooltipWrapper tipContent={tooltipMessage} position="bottom">
<TooltipWrapper tipContent={tooltipMessage}>
{statusText}
</TooltipWrapper>
</div>

View file

@ -60,9 +60,9 @@ export const munkiIssuesTableHeaders: IDataColumn[] = [
Header: (headerProps: IHeaderProps): JSX.Element => {
const titleWithToolTip = (
<TooltipWrapper
tipContent={`
Issues reported the last time Munki ran on each host.
`}
tipContent={
<>Issues reported the last time Munki ran on each host.</>
}
>
Issue
</TooltipWrapper>
@ -95,9 +95,7 @@ export const munkiIssuesTableHeaders: IDataColumn[] = [
Header: (headerProps: IHeaderProps): JSX.Element => {
const titleWithToolTip = (
<TooltipWrapper
tipContent={`
The first time Munki reported this issue.
`}
tipContent={<>The first time Munki reported this issue.</>}
>
Time
</TooltipWrapper>

View file

@ -79,7 +79,16 @@ const generatePackTableHeaders = (): IDataColumn[] => {
{
Header: () => {
return (
<TooltipWrapper tipContent="The last time the query ran<br/>since the last time osquery <br/>started on this host.">
<TooltipWrapper
tipContent={
<>
The last time the query ran
<br />
since the last time osquery <br />
started on this host.
</>
}
>
Last run
</TooltipWrapper>
);
@ -93,7 +102,14 @@ const generatePackTableHeaders = (): IDataColumn[] => {
{
Header: () => {
return (
<TooltipWrapper tipContent="This is the performance <br />impact on this host.">
<TooltipWrapper
tipContent={
<>
This is the performance <br />
impact on this host.
</>
}
>
Performance impact
</TooltipWrapper>
);

View file

@ -76,7 +76,14 @@ const generateTableHeaders = (): IDataColumn[] => {
{
Header: () => {
return (
<TooltipWrapper tipContent="This is the performance <br />impact on this host.">
<TooltipWrapper
tipContent={
<>
This is the performance <br />
impact on this host.
</>
}
>
Performance impact
</TooltipWrapper>
);

View file

@ -14,7 +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";
import { getSoftwareBundleTooltipJSX } from "utilities/helpers";
interface IHeaderProps {
column: {
@ -106,14 +106,13 @@ const condenseVulnerabilities = (vulns: string[]): string[] => {
const renderBundleTooltip = (name: string, bundle: string) => (
<span className="name-container">
<TooltipWrapper
position="top"
tipContent={`
position="top-start"
tipContent={
<span>
<b>Bundle identifier: </b>
<br />
${bundle}
<br />${bundle}
</span>
`}
}
>
{name}
</TooltipWrapper>
@ -221,7 +220,7 @@ export const generateSoftwareTableHeaders = ({
customOnClick={onClickSoftware}
value={name}
tooltipContent={
bundle ? getSoftwareBundleTooltipMarkup(bundle) : undefined
bundle ? getSoftwareBundleTooltipJSX(bundle) : undefined
}
/>
);
@ -356,7 +355,14 @@ export const generateSoftwareTableHeaders = ({
title: "File path",
Header: () => {
return (
<TooltipWrapper tipContent="This is where the software is <br />located on this host.">
<TooltipWrapper
tipContent={
<>
This is where the software is <br />
located on this host.
</>
}
>
File path
</TooltipWrapper>
);

View file

@ -49,7 +49,17 @@ const generateUsersTableHeaders = (): IDataColumn[] => {
{
Header: () => {
return (
<TooltipWrapper tipContent="The command line shell, such as bash,<br />that this user is equipped with by<br />default when they log in to the system.">
<TooltipWrapper
tipContent={
<>
The command line shell, such as bash,
<br />
that this user is equipped with by
<br />
default when they log in to the system.
</>
}
>
Shell
</TooltipWrapper>
);

View file

@ -767,9 +767,9 @@ const ManagePolicyPage = ({
globalPoliciesCount
)}
caretPosition={"before"}
tooltipHtml={
'"All teams" policies are checked <br/> for this teams hosts.'
}
tooltipHtml={`"All teams" policies are checked ${(
<br />
)} for this team's hosts.`}
onClick={toggleShowInheritedPolicies}
/>
)}

View file

@ -130,7 +130,7 @@ describe("PolicyForm - component", () => {
);
});
it("disables run button with tooltip for globally disabled queries", async () => {
it("disables run button with tooltip when live queries are globally disabled", async () => {
const render = createCustomRenderer({
context: {
policy: {

View file

@ -460,9 +460,11 @@ const PolicyForm = ({
>
<TooltipWrapper
tipContent={
"<p>If automations are turned on, this<br/> information is included.</p>"
<p>
If automations are turned on, this
<br /> information is included.
</p>
}
isDelayed
>
Critical:
</TooltipWrapper>

View file

@ -153,9 +153,11 @@ const SaveNewPolicyModal = ({
>
<TooltipWrapper
tipContent={
"<p>If automations are turned on, this<br/> information is included.</p>"
<p>
If automations are turned on, this
<br /> information is included.
</p>
}
isDelayed
>
Critical:
</TooltipWrapper>

View file

@ -39,7 +39,12 @@ const PreviewDataModal = ({
<div className={`${baseClass}__preview-modal`}>
<p>
<TooltipWrapper
tipContent={`The &quot;snapshot&quot; key includes the query&apos;s results. These will be unique to your query.`}
tipContent={
<>
The &quot;snapshot&quot; key includes the query&apos;s results.
These will be unique to your query.
</>
}
>
The data sent to your configured log destination will look similar
to the following JSON:

View file

@ -197,9 +197,12 @@ const generateTableHeaders = ({
return (
<div>
<TooltipWrapper
tipContent={`
This is the average performance impact across <br />
all hosts where this query was scheduled.`}
tipContent={
<>
This is the average performance impact across <br />
all hosts where this query was scheduled.
</>
}
>
Performance impact
</TooltipWrapper>

View file

@ -255,8 +255,15 @@ const QueryDetailsPage = ({
<div className={`${baseClass}__settings`}>
<div className={`${baseClass}__automations`}>
<TooltipWrapper
// TODO - change to JSX after tooltip refactor
tipContent={`Query automations let you send data to your log <br />destination on a schedule. When automations are <b>on</b>, <br />data is sent according to a querys frequency.`}
tipContent={
<>
Query automations let you send data to your log <br />
destination on a schedule. When automations are <b>
on
</b>, <br />
data is sent according to a query&apos;s frequency.
</>
}
>
Automations:
</TooltipWrapper>

View file

@ -38,10 +38,6 @@
display: flex;
gap: $pad-large;
font-size: $x-small;
// TODO - remove once refactored tooltip wrapper is merged
.component__tooltip-wrapper__element__underline::after {
bottom: 0px;
}
}
&__automations,

View file

@ -67,18 +67,50 @@ const NoResults = ({
// In order of empty page priority
if (disabledCaching) {
const tipContent = () => {
// TODO - change to JSX with refactor tooltipwrapper merge
if (disabledCachingGlobally) {
return `<div>The following setting prevents saving this query's results in Fleet:</div>\
<div>&nbsp; Query reports are globally disabled in organization settings.</div>`;
return (
<>
{" "}
<div>
The following setting prevents saving this query&apos;s results
in Fleet:
</div>
\
<div>
&nbsp; Query reports are globally disabled in organization
settings.
</div>
</>
);
}
if (discardDataEnabled) {
return `<div>The following setting prevents saving this query's results in Fleet:</div>\
<div>&nbsp; This query has <b>Discard data</b> enabled.</div>`;
return (
<>
<div>
The following setting prevents saving this query&apos;s results
in Fleet:
</div>
\
<div>
&nbsp; This query has <b>Discard data</b> enabled.
</div>
</>
);
}
if (!loggingSnapshot) {
return `<div>The following setting prevents saving this query's results in Fleet:</div>\
<div>&nbsp; The logging setting for this query is not <b>Snapshot</b>.</div>`;
return (
<>
<div>
The following setting prevents saving this query&apos;s results
in Fleet:
</div>
\
<div>
&nbsp; The logging setting for this query is not{" "}
<b>Snapshot</b>.
</div>
</>
);
}
return "Unknown";
};

View file

@ -114,10 +114,16 @@ const QueryReport = ({
return (
<div className={`${baseClass}__count `}>
<TooltipWrapper
tipContent={`Fleet has retained a sample of early results for
reference. Reporting is paused until existing data is deleted. <br/><br/>
You can reset this report by updating the query's SQL, or by
temporarily enabling the <b>discard data</b> setting and disabling it again.`}
tipContent={
<>
Fleet has retained a sample of early results for reference.
Reporting is paused until existing data is deleted. <br />
<br />
You can reset this report by updating the query&apos;s SQL, or
by temporarily enabling the <b>discard data</b> setting and
disabling it again.
</>
}
>
{`${count} result${count === 1 ? "" : "s"}`}
</TooltipWrapper>

View file

@ -30,10 +30,6 @@ describe("DiscardDataOption component", () => {
expect(screen.getByText(/Discard data/)).toBeInTheDocument();
expect(screen.getByText(/This setting is ignored/)).toBeInTheDocument();
await fireEvent.mouseOver(screen.getByText(/globally disabled/));
expect(screen.getByText(/A Fleet administrator/)).toBeInTheDocument();
});
it('Restores normal help text when disabled and then "Edit anyway" is clicked', async () => {

View file

@ -32,12 +32,16 @@ const DiscardDataOption = ({
<>
This setting is ignored because query reports in Fleet have been{" "}
<TooltipWrapper
// TODO - use JSX once new tooltipwrapper is merged
tipContent={
"A Fleet administrator can enable query reports under <br />\
<b>Organization settings > Advanced options > Disable query reports</b>."
<>
A Fleet administrator can enable query reports under <br />
<b>
Organization settings &gt; Advanced options &gt; Disable query
reports
</b>
.
</>
}
position="bottom"
>
{"globally disabled."}
</TooltipWrapper>{" "}

View file

@ -8,7 +8,7 @@ import { IVulnerability } from "interfaces/vulnerability";
import PATHS from "router/paths";
import {
formatFloatAsPercentage,
getSoftwareBundleTooltipMarkup,
getSoftwareBundleTooltipJSX,
} from "utilities/helpers";
import { DEFAULT_EMPTY_CELL_VALUE } from "utilities/constants";
@ -79,13 +79,15 @@ const generateEPSSColumnHeader = (isSandboxMode = false) => {
Header: (headerProps: IHeaderProps): JSX.Element => {
const titleWithToolTip = (
<TooltipWrapper
tipContent={`
The probability that this software will be exploited
<br />
in the next 30 days (EPSS probability). This data is
<br />
reported by FIRST.org.
`}
tipContent={
<>
The probability that this software will be exploited
<br />
in the next 30 days (EPSS probability). This data is
<br />
reported by FIRST.org.
</>
}
>
Probability of exploit
</TooltipWrapper>
@ -205,7 +207,7 @@ const generateTableHeaders = (
customOnClick={onClickSoftware}
value={name}
tooltipContent={
bundle ? getSoftwareBundleTooltipMarkup(bundle) : undefined
bundle ? getSoftwareBundleTooltipJSX(bundle) : undefined
}
/>
);

View file

@ -92,10 +92,14 @@ const generateVulnTableHeaders = (
Header: (headerProps: IHeaderProps): JSX.Element => {
const titleWithToolTip = (
<TooltipWrapper
tipContent={`
The probability that this vulnerability will be exploited in the next 30 days (EPSS probability).<br />
This data is reported by FIRST.org.
`}
tipContent={
<>
The probability that this vulnerability will be exploited in the
next 30 days (EPSS probability).
<br />
This data is reported by FIRST.org.
</>
}
>
Probability of exploit
</TooltipWrapper>
@ -121,10 +125,15 @@ const generateVulnTableHeaders = (
Header: (headerProps: IHeaderProps): JSX.Element => {
const titleWithToolTip = (
<TooltipWrapper
tipContent={`
The worst case impact across different environments (CVSS base score).<br />
This data is reported by the National Vulnerability Database (NVD).
`}
tipContent={
<>
The worst case impact across different environments (CVSS base
score).
<br />
This data is reported by the National Vulnerability Database
(NVD).
</>
}
>
Severity
</TooltipWrapper>
@ -151,10 +160,13 @@ const generateVulnTableHeaders = (
Header: (headerProps: IHeaderProps): JSX.Element => {
const titleWithToolTip = (
<TooltipWrapper
tipContent={`
The vulnerability has been actively exploited in the wild. This data is reported by
the Cybersecurity and Infrustructure Security Agency (CISA).
`}
tipContent={
<>
The vulnerability has been actively exploited in the wild. This
data is reported by the Cybersecurity and Infrustructure
Security Agency (CISA).
</>
}
>
Known exploit
</TooltipWrapper>
@ -181,7 +193,12 @@ const generateVulnTableHeaders = (
Header: (headerProps: IHeaderProps): JSX.Element => {
const titleWithToolTip = (
<TooltipWrapper
tipContent={`The date this vulnerability was published in the National Vulnerability Database (NVD).`}
tipContent={
<>
The date this vulnerability was published in the National
Vulnerability Database (NVD).
</>
}
>
Published
</TooltipWrapper>

View file

@ -24,6 +24,7 @@ $ui-shadow: #e9e9e9;
$ui-vibrant-blue-50: rgba(106, 103, 254, 0.5);
$ui-vibrant-blue-25: #d9d9fe;
$ui-vibrant-blue-10: #f1f0ff; // rgba(241, 240, 255, 1)
$tooltip-bg: #3e4771;
// Notifications & status & specific messages
$ui-offline: #8b8fa2;

View file

@ -2,6 +2,14 @@ import "@testing-library/jest-dom";
import mockServer from "./mock-server";
// Needed for testing react-tooltip-5
window.CSS.supports = jest.fn();
global.ResizeObserver = jest.fn().mockImplementation(() => ({
observe: jest.fn(),
unobserve: jest.fn(),
disconnect: jest.fn(),
}));
// Mock server setup
beforeAll(() => mockServer.listen());
afterEach(() => mockServer.resetHandlers());

View file

@ -1,3 +1,4 @@
import React from "react";
import {
isEmpty,
flatMap,
@ -34,8 +35,8 @@ import {
ISelectedTargetsForApi,
IPackTargets,
} from "interfaces/target";
import { ITeam, ITeamSummary } from "interfaces/team";
import { IUser, UserRole } from "interfaces/user";
import { ITeam } from "interfaces/team";
import { UserRole } from "interfaces/user";
import stringUtils from "utilities/strings";
import sortUtils from "utilities/sort";
@ -92,40 +93,6 @@ const labelSlug = (label: ILabel): string => {
return `labels/${id}`;
};
const statusKey = [
{
id: "new",
count: 0,
description: "Hosts that have been enrolled to Fleet in the last 24 hours.",
display_text: "New",
title_description: "(added in last 24hrs)",
type: "status",
},
{
id: "online",
count: 0,
description: "Hosts that have recently checked-in to Fleet.",
display_text: "Online",
type: "status",
},
{
id: "missing",
count: 0,
description: "Hosts that have not been online in 30 days or more.",
display_text: "Missing",
slug: "missing",
statusLabelKey: "missing_count",
type: "status",
},
{
id: "offline",
count: 0,
description: "Hosts that have not checked-in to Fleet recently.",
display_text: "Offline",
type: "status",
},
];
const isLabel = (target: ISelectTargetsEntity) => {
return "label_type" in target;
};
@ -801,7 +768,7 @@ export const syntaxHighlight = (json: any): string => {
/* eslint-disable no-useless-escape */
return jsonStr.replace(
/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g,
function (match) {
(match) => {
let cls = "number";
if (/^"/.test(match)) {
if (/:$/.test(match)) {
@ -900,15 +867,13 @@ export const getNextLocationPath = ({
return queryString ? `/${nextLocation}?${queryString}` : `/${nextLocation}`;
};
export const getSoftwareBundleTooltipMarkup = (bundle: string) => {
return `
<span>
<b>Bundle identifier: </b>
<br />
${bundle}
</span>
`;
};
export const getSoftwareBundleTooltipJSX = (bundle: string) => (
<span>
<b>Bundle identifier: </b>
<br />
{bundle}
</span>
);
export const TAGGED_TEMPLATES = {
queryByHostRoute: (hostId: number | undefined | null) => {

View file

@ -1,6 +1,6 @@
import { flatMap, map } from "lodash";
import { flatMap } from "lodash";
import { IOsQueryTable, IQueryTableColumn } from "interfaces/osquery_table";
import { IOsQueryTable } from "interfaces/osquery_table";
import osqueryFleetTablesJSON from "../../schema/osquery_fleet_schema.json";

View file

@ -47,6 +47,7 @@
"react-table": "7.7.0",
"react-tabs": "3.2.3",
"react-tooltip": "4.2.21",
"react-tooltip-5": "npm:react-tooltip@5.21.3",
"remark-gfm": "3.0.1",
"select": "1.1.2",
"sockjs-client": "1.6.1",

View file

@ -2665,7 +2665,7 @@
dependencies:
"@floating-ui/utils" "^0.1.3"
"@floating-ui/dom@^1.5.1":
"@floating-ui/dom@^1.0.0", "@floating-ui/dom@^1.5.1":
version "1.5.3"
resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.5.3.tgz#54e50efcb432c06c23cd33de2b575102005436fa"
integrity sha512-ClAbQnEqJAKCJOEbbLo5IUlZHkNszqhuxS4fHAVxRPXPya6Ysf2G8KypnYcOTpx6I8xcgF9bbHb6g/2KpbV8qA==
@ -7271,6 +7271,11 @@ classnames@^2.2.4:
resolved "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz"
integrity sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==
classnames@^2.3.0:
version "2.3.2"
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.2.tgz#351d813bf0137fcc6a76a16b88208d2560a0d924"
integrity sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==
clean-css@^5.2.2:
version "5.3.2"
resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-5.3.2.tgz#70ecc7d4d4114921f5d298349ff86a31a9975224"
@ -15294,6 +15299,14 @@ react-tabs@3.2.3:
clsx "^1.1.0"
prop-types "^15.5.0"
"react-tooltip-5@npm:react-tooltip@5.21.3":
version "5.21.3"
resolved "https://registry.yarnpkg.com/react-tooltip/-/react-tooltip-5.21.3.tgz#131d578c7ea69f96c65dbd09f071880c34b4f83d"
integrity sha512-z3Q+Uka4D6uYxfsssPqfx1W8vw7NIHyC2ZMq+NJkWg4EpUD3w7Fwz/o+dezyUQMCHL7nO/2sFbtWIrkyxktq2Q==
dependencies:
"@floating-ui/dom" "^1.0.0"
classnames "^2.3.0"
react-tooltip@*, react-tooltip@4.2.21:
version "4.2.21"
resolved "https://registry.yarnpkg.com/react-tooltip/-/react-tooltip-4.2.21.tgz#840123ed86cf33d50ddde8ec8813b2960bfded7f"