From bf8504a028bbd4b577dc4fa2527db0e659bd30aa Mon Sep 17 00:00:00 2001 From: Jacob Shandling <61553566+jacobshandling@users.noreply.github.com> Date: Tue, 7 Nov 2023 13:15:49 -0800 Subject: [PATCH] Refactor Tooltip Wrapper (#13845) --- .../DownloadInstallers/DownloadInstallers.tsx | 2 +- .../PlatformWrapper/PlatformWrapper.tsx | 2 +- .../LastUpdatedText/LastUpdatedText.tests.tsx | 9 +- .../LastUpdatedText/LastUpdatedText.tsx | 7 +- .../components/LiveQuery/SelectTargets.tsx | 8 +- .../LogDestinationIndicator.tsx | 62 +++++++++---- .../PlatformCompatibility.tsx | 16 +++- .../DataTable/HeaderCell/HeaderCell.tsx | 20 +---- .../DataTable/LinkCell/LinkCell.tsx | 4 +- .../TableContainer/DataTable/_styles.scss | 9 +- frontend/components/TooltipWrapper/README.md | 22 +++-- .../TooltipWrapper/TooltipWrapper.stories.tsx | 23 ++++- .../TooltipWrapper/TooltipWrapper.tsx | 78 +++++++++++------ .../components/TooltipWrapper/_styles.scss | 78 ++--------------- .../RevealButton/RevealButton.tests.tsx | 4 +- .../components/forms/FormField/FormField.tsx | 2 +- .../UserSettingsForm/UserSettingsForm.jsx | 13 +-- .../forms/fields/Checkbox/Checkbox.tsx | 8 +- .../InputFieldWithIcon/InputFieldWithIcon.jsx | 2 +- .../forms/fields/Radio/Radio.tests.tsx | 5 +- .../components/forms/fields/Radio/Radio.tsx | 2 +- .../PackQueriesTableConfig.tsx | 12 ++- .../QueryResultsHeading.tsx | 16 +++- .../QuerySidePanel/QuerySidePanel.tests.tsx | 19 ++-- .../ColumnListItem/ColumnListItem.tsx | 83 +++++++++--------- .../ColumnListItem/_styles.scss | 2 +- frontend/interfaces/datatable_config.ts | 1 - .../SummaryTile/SummaryTile.tests.tsx | 4 +- .../HostsSummary/SummaryTile/SummaryTile.tsx | 3 +- .../cards/MDM/MDMStatusTableConfig.tsx | 2 +- .../cards/Munki/MunkiIssuesTableConfig.tsx | 6 +- .../AppleBusinessManagerSection.tsx | 2 +- .../IntegrationForm/IntegrationForm.tsx | 27 +++--- .../cards/Advanced/Advanced.tsx | 81 +++++++++++++---- .../HostStatusWebhook/HostStatusWebhook.tsx | 29 +++++-- .../admin/OrgSettingsPage/cards/Smtp/Smtp.tsx | 20 +++-- .../admin/OrgSettingsPage/cards/Sso/Sso.tsx | 6 +- .../MembersPage/MembersPageTableConfig.tsx | 32 ++++--- .../components/UserForm/UserForm.tsx | 63 +++++++++----- .../UsersTable/UsersTableConfig.tsx | 32 ++++--- .../hosts/ManageHostsPage/HostTableConfig.tsx | 86 ++++++++----------- .../hosts/ManageHostsPage/ManageHostsPage.tsx | 6 -- .../pages/hosts/ManageHostsPage/_styles.scss | 18 ---- .../pages/hosts/details/cards/About/About.tsx | 1 - .../cards/AgentOptions/AgentOptions.tsx | 1 - .../details/cards/HostSummary/HostSummary.tsx | 2 +- .../MunkiIssues/MunkiIssuesTableConfig.tsx | 10 +-- .../cards/Packs/PackTable/PackTableConfig.tsx | 20 ++++- .../cards/Schedule/ScheduleTableConfig.tsx | 9 +- .../cards/Software/SoftwareTableConfig.tsx | 22 +++-- .../Users/UsersTable/UsersTableConfig.tsx | 12 ++- .../ManagePoliciesPage/ManagePoliciesPage.tsx | 6 +- .../PolicyForm/PolicyForm.tests.tsx | 2 +- .../components/PolicyForm/PolicyForm.tsx | 6 +- .../SaveNewPolicyModal/SaveNewPolicyModal.tsx | 6 +- .../PreviewDataModal/PreviewDataModal.tsx | 7 +- .../QueriesTable/QueriesTableConfig.tsx | 9 +- .../QueryDetailsPage/QueryDetailsPage.tsx | 11 ++- .../details/QueryDetailsPage/_styles.scss | 4 - .../components/NoResults/NoResults.tsx | 46 ++++++++-- .../components/QueryReport/QueryReport.tsx | 14 ++- .../DiscardDataOption.tests.tsx | 4 - .../DiscardDataOption/DiscardDataOption.tsx | 12 ++- .../SoftwareTableConfig.tsx | 20 +++-- .../Vulnerabilities/VulnTableConfig.tsx | 43 +++++++--- frontend/styles/var/colors.scss | 1 + frontend/test/test-setup.ts | 8 ++ .../utilities/{helpers.ts => helpers.tsx} | 57 +++--------- frontend/utilities/osquery_tables.ts | 4 +- package.json | 1 + yarn.lock | 15 +++- 71 files changed, 735 insertions(+), 544 deletions(-) rename frontend/utilities/{helpers.ts => helpers.tsx} (95%) diff --git a/frontend/components/AddHostsModal/DownloadInstallers/DownloadInstallers.tsx b/frontend/components/AddHostsModal/DownloadInstallers/DownloadInstallers.tsx index 200cca2579..d598af35e2 100644 --- a/frontend/components/AddHostsModal/DownloadInstallers/DownloadInstallers.tsx +++ b/frontend/components/AddHostsModal/DownloadInstallers/DownloadInstallers.tsx @@ -239,7 +239,7 @@ const DownloadInstallers = ({ Include  Include Fleet Desktop if your’re adding workstations.

" +

Include Fleet Desktop if you're adding workstations.

} > Fleet Desktop diff --git a/frontend/components/AddHostsModal/PlatformWrapper/PlatformWrapper.tsx b/frontend/components/AddHostsModal/PlatformWrapper/PlatformWrapper.tsx index 6b29b76db0..5f3a6b1465 100644 --- a/frontend/components/AddHostsModal/PlatformWrapper/PlatformWrapper.tsx +++ b/frontend/components/AddHostsModal/PlatformWrapper/PlatformWrapper.tsx @@ -538,7 +538,7 @@ const PlatformWrapper = ({ Include  Fleet Desktop diff --git a/frontend/components/LastUpdatedText/LastUpdatedText.tests.tsx b/frontend/components/LastUpdatedText/LastUpdatedText.tests.tsx index 77046548a4..1dbb1aa7a7 100644 --- a/frontend/components/LastUpdatedText/LastUpdatedText.tests.tsx +++ b/frontend/components/LastUpdatedText/LastUpdatedText.tests.tsx @@ -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( - - ); + render(); - await user.hover(screen.getByText("Updated never")); + await fireEvent.mouseEnter(screen.getByText("Updated never")); expect(screen.getByText(/to retrieve software/i)).toBeInTheDocument(); }); diff --git a/frontend/components/LastUpdatedText/LastUpdatedText.tsx b/frontend/components/LastUpdatedText/LastUpdatedText.tsx index 542bea6b91..74985fbd28 100644 --- a/frontend/components/LastUpdatedText/LastUpdatedText.tsx +++ b/frontend/components/LastUpdatedText/LastUpdatedText.tsx @@ -27,7 +27,12 @@ const LastUpdatedText = ({ return ( to retrieve ${whatToRetrieve}.`} + tipContent={ + <> + Fleet periodically queries all hosts
+ to retrieve {whatToRetrieve}. + + } > {`Updated ${lastUpdatedAt}`}
diff --git a/frontend/components/LiveQuery/SelectTargets.tsx b/frontend/components/LiveQuery/SelectTargets.tsx index f6026f2a8e..f8cb285466 100644 --- a/frontend/components/LiveQuery/SelectTargets.tsx +++ b/frontend/components/LiveQuery/SelectTargets.tsx @@ -399,7 +399,13 @@ const SelectTargets = ({ {onlinePercentage()} %  have recently checked
into Fleet.`} + tipContent={ + <> + Hosts are online if they
+ have recently checked
+ into Fleet. + + } > online
diff --git a/frontend/components/LogDestinationIndicator/LogDestinationIndicator.tsx b/frontend/components/LogDestinationIndicator/LogDestinationIndicator.tsx index 82de7b5d0b..3bc7998618 100644 --- a/frontend/components/LogDestinationIndicator/LogDestinationIndicator.tsx +++ b/frontend/components/LogDestinationIndicator/LogDestinationIndicator.tsx @@ -49,30 +49,62 @@ const LogDestinationIndicator = ({ const tooltipText = () => { switch (logDestination) { case "filesystem": - return `Each time a query runs, the data is sent to
+ return ( + <> + Each time a query runs, the data is sent to
/var/log/osquery/osqueryd.snapshots.log
- in each host's filesystem.`; + in each host's filesystem. + + ); case "firehose": - return `Each time a query runs, the data is sent to
- Amazon Kinesis Data Firehose.`; + return ( + <> + Each time a query runs, the data is sent to
+ Amazon Kinesis Data Firehose.` + + ); case "kinesis": - return `Each time a query runs, the data is sent to
- Amazon Kinesis Data Streams.`; + return ( + <> + Each time a query runs, the data is sent to
+ Amazon Kinesis Data Streams. + + ); case "lambda": - return ` - Each time a query runs, the data
is sent to AWS Lambda. - `; + return ( + <> + Each time a query runs, the data
+ is sent to AWS Lambda. + + ); case "pubsub": - return `Each time a query runs, the data is
sent to Google Cloud Pub/Sub.`; + return ( + <> + Each time a query runs, the data is
sent to Google Cloud Pub + / Sub.` + + ); case "kafta": - return `Each time a query runs, the data
is sent to Apache Kafka.`; + return ( + <> + Each time a query runs, the data
is sent to Apache Kafka. + + ); case "stdout": - return `Each time a query runs, the data is sent to
- standard output (stdout) on the Fleet server.`; + return ( + <> + Each time a query runs, the data is sent to
+ 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. + + ); } }; diff --git a/frontend/components/PlatformCompatibility/PlatformCompatibility.tsx b/frontend/components/PlatformCompatibility/PlatformCompatibility.tsx index 110c58b9c8..e2310bf9e8 100644 --- a/frontend/components/PlatformCompatibility/PlatformCompatibility.tsx +++ b/frontend/components/PlatformCompatibility/PlatformCompatibility.tsx @@ -56,8 +56,12 @@ const PlatformCompatibility = ({ + Estimated compatiblity based on
+ the tables used in the query. + + } > Compatible with:
@@ -73,8 +77,12 @@ const PlatformCompatibility = ({ + Estimated compatiblity based on
the tables used in the + query. + + } > Compatible with:
diff --git a/frontend/components/TableContainer/DataTable/HeaderCell/HeaderCell.tsx b/frontend/components/TableContainer/DataTable/HeaderCell/HeaderCell.tsx index a438662889..608ccd91fd 100644 --- a/frontend/components/TableContainer/DataTable/HeaderCell/HeaderCell.tsx +++ b/frontend/components/TableContainer/DataTable/HeaderCell/HeaderCell.tsx @@ -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 ( -
+
{value} {!disableSortBy && (
diff --git a/frontend/components/TableContainer/DataTable/LinkCell/LinkCell.tsx b/frontend/components/TableContainer/DataTable/LinkCell/LinkCell.tsx index f57d6e7055..c04cbf17b5 100644 --- a/frontend/components/TableContainer/DataTable/LinkCell/LinkCell.tsx +++ b/frontend/components/TableContainer/DataTable/LinkCell/LinkCell.tsx @@ -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 ? ( diff --git a/frontend/components/TableContainer/DataTable/_styles.scss b/frontend/components/TableContainer/DataTable/_styles.scss index 8bad872b93..5288b5f029 100644 --- a/frontend/components/TableContainer/DataTable/_styles.scss +++ b/frontend/components/TableContainer/DataTable/_styles.scss @@ -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; } } diff --git a/frontend/components/TooltipWrapper/README.md b/frontend/components/TooltipWrapper/README.md index 980e6639fe..cb7ce643a9 100644 --- a/frontend/components/TooltipWrapper/README.md +++ b/frontend/components/TooltipWrapper/README.md @@ -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 + The "snapshot" key includes the query's results.
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
- 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.

+ This user will not be asked to set a new password after logging in to fleetctl or the Fleet API. + + } /> ``` \ No newline at end of file diff --git a/frontend/components/TooltipWrapper/TooltipWrapper.stories.tsx b/frontend/components/TooltipWrapper/TooltipWrapper.stories.tsx index 85d26ffb90..4cc307c41b 100644 --- a/frontend/components/TooltipWrapper/TooltipWrapper.stories.tsx +++ b/frontend/components/TooltipWrapper/TooltipWrapper.stories.tsx @@ -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 = (props) => (

Example text +
+
+
+
); diff --git a/frontend/components/TooltipWrapper/TooltipWrapper.tsx b/frontend/components/TooltipWrapper/TooltipWrapper.tsx index 43e334495a..aa81f28c50 100644 --- a/frontend/components/TooltipWrapper/TooltipWrapper.tsx +++ b/frontend/components/TooltipWrapper/TooltipWrapper.tsx @@ -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 ( -
-
+ +
{children} -
-
{ - e.stopPropagation(); - }} - /> -
+ + {tipContent} + + ); }; diff --git a/frontend/components/TooltipWrapper/_styles.scss b/frontend/components/TooltipWrapper/_styles.scss index b06a53cbaa..3f6ca91977 100644 --- a/frontend/components/TooltipWrapper/_styles.scss +++ b/frontend/components/TooltipWrapper/_styles.scss @@ -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; - } - } - } } diff --git a/frontend/components/buttons/RevealButton/RevealButton.tests.tsx b/frontend/components/buttons/RevealButton/RevealButton.tests.tsx index ff9c566a0a..c18dd5c49a 100644 --- a/frontend/components/buttons/RevealButton/RevealButton.tests.tsx +++ b/frontend/components/buttons/RevealButton/RevealButton.tests.tsx @@ -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(); }); diff --git a/frontend/components/forms/FormField/FormField.tsx b/frontend/components/forms/FormField/FormField.tsx index 392ac516de..2f1a7fee53 100644 --- a/frontend/components/forms/FormField/FormField.tsx +++ b/frontend/components/forms/FormField/FormField.tsx @@ -14,7 +14,7 @@ export interface IFormFieldProps { label: Array | JSX.Element | string; name: string; type: string; - tooltip?: string; + tooltip?: React.ReactNode; } const FormField = ({ diff --git a/frontend/components/forms/UserSettingsForm/UserSettingsForm.jsx b/frontend/components/forms/UserSettingsForm/UserSettingsForm.jsx index c4886eaced..e07fe35124 100644 --- a/frontend/components/forms/UserSettingsForm/UserSettingsForm.jsx +++ b/frontend/components/forms/UserSettingsForm/UserSettingsForm.jsx @@ -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.\ -

\ - Users with Admin role can configure SMTP in Settings > Organization settings.\ - " + <> + Editing your email address requires that SMTP or SES is + configured in order to send a validation email. +
+
+ Users with Admin role can configure SMTP in{" "} + Settings > Organization settings. + } />
diff --git a/frontend/components/forms/fields/Checkbox/Checkbox.tsx b/frontend/components/forms/fields/Checkbox/Checkbox.tsx index c8daf7bfea..b439235c00 100644 --- a/frontend/components/forms/fields/Checkbox/Checkbox.tsx +++ b/frontend/components/forms/fields/Checkbox/Checkbox.tsx @@ -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" /> - {tooltip ? ( + {tooltipContent ? ( - + {children as string} diff --git a/frontend/components/forms/fields/InputFieldWithIcon/InputFieldWithIcon.jsx b/frontend/components/forms/fields/InputFieldWithIcon/InputFieldWithIcon.jsx index 9e7db4d0ec..c051873e5e 100644 --- a/frontend/components/forms/fields/InputFieldWithIcon/InputFieldWithIcon.jsx +++ b/frontend/components/forms/fields/InputFieldWithIcon/InputFieldWithIcon.jsx @@ -45,7 +45,7 @@ class InputFieldWithIcon extends InputField { data-has-tooltip={!!tooltip} > {tooltip && !error ? ( - + {label} ) : ( diff --git a/frontend/components/forms/fields/Radio/Radio.tests.tsx b/frontend/components/forms/fields/Radio/Radio.tests.tsx index 23b36de311..0846d63524 100644 --- a/frontend/components/forms/fields/Radio/Radio.tests.tsx +++ b/frontend/components/forms/fields/Radio/Radio.tests.tsx @@ -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( { /> ); + await fireEvent.mouseEnter(screen.getByText("Radio Label")); const tooltip = screen.getByText("A Test Radio Tooltip"); expect(tooltip).toBeInTheDocument(); }); diff --git a/frontend/components/forms/fields/Radio/Radio.tsx b/frontend/components/forms/fields/Radio/Radio.tsx index a595e4f4bc..cf38349bac 100644 --- a/frontend/components/forms/fields/Radio/Radio.tsx +++ b/frontend/components/forms/fields/Radio/Radio.tsx @@ -14,7 +14,7 @@ export interface IRadioProps { name?: string; className?: string; disabled?: boolean; - tooltip?: string; + tooltip?: React.ReactNode; testId?: string; } diff --git a/frontend/components/queries/PackQueriesTable/PackQueriesTable/PackQueriesTableConfig.tsx b/frontend/components/queries/PackQueriesTable/PackQueriesTable/PackQueriesTableConfig.tsx index 6a8772b309..84f4d3d5b8 100644 --- a/frontend/components/queries/PackQueriesTable/PackQueriesTable/PackQueriesTableConfig.tsx +++ b/frontend/components/queries/PackQueriesTable/PackQueriesTable/PackQueriesTableConfig.tsx @@ -152,7 +152,17 @@ const generateTableHeaders = ( Header: () => { return (
- + + This is the average performance +
+ impact across all hosts where +
+ this query was scheduled. + + } + > Performance impact
diff --git a/frontend/components/queries/queryResults/QueryResultsHeading/QueryResultsHeading.tsx b/frontend/components/queries/queryResults/QueryResultsHeading/QueryResultsHeading.tsx index 68bb0207f8..74a8229bc8 100644 --- a/frontend/components/queries/queryResults/QueryResultsHeading/QueryResultsHeading.tsx +++ b/frontend/components/queries/queryResults/QueryResultsHeading/QueryResultsHeading.tsx @@ -101,8 +101,13 @@ const QuertResultsHeading = ({ ({`${percentResponded}% `} return results, errors, or
no results`} + tipContent={ + <> + Hosts that respond may +
return results, errors, or
+ no results + + } > responded
@@ -120,7 +125,12 @@ const QuertResultsHeading = ({ {!isQueryFinished && (
impact live query response times.`} + tipContent={ + <> + The hosts’ distributed interval can
+ impact live query response times. + + } > Taking longer than 15 seconds?
diff --git a/frontend/components/side_panels/QuerySidePanel/QuerySidePanel.tests.tsx b/frontend/components/side_panels/QuerySidePanel/QuerySidePanel.tests.tsx index 6c1c7b727a..25aa78ba5a 100644 --- a/frontend/components/side_panels/QuerySidePanel/QuerySidePanel.tests.tsx +++ b/frontend/components/side_panels/QuerySidePanel/QuerySidePanel.tests.tsx @@ -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( noop} + onOsqueryTableSelect={() => noop} onClose={noop} /> ); @@ -23,7 +23,7 @@ describe("QuerySidePanel - component", () => { const { container } = render( noop} + onOsqueryTableSelect={() => noop} onClose={noop} /> ); @@ -42,7 +42,7 @@ describe("QuerySidePanel - component", () => { const { container } = render( 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( 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( 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( noop} + onOsqueryTableSelect={() => noop} onClose={noop} /> ); diff --git a/frontend/components/side_panels/QuerySidePanel/QueryTableColumns/ColumnListItem/ColumnListItem.tsx b/frontend/components/side_panels/QuerySidePanel/QueryTableColumns/ColumnListItem/ColumnListItem.tsx index 45fc893401..7dd1ec2e06 100644 --- a/frontend/components/side_panels/QuerySidePanel/QueryTableColumns/ColumnListItem/ColumnListItem.tsx +++ b/frontend/components/side_panels/QuerySidePanel/QueryTableColumns/ColumnListItem/ColumnListItem.tsx @@ -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 = `${column.description}`; - toolTipHtml.push(descriptionHtml); - - if (column.required) { - toolTipHtml.push( - `${FOOTNOTES.required}` - ); - } - - 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( - `${FOOTNOTES.requires_user_context}` + return ( + + ${FOOTNOTES.requires_user_context} + ); - } + }; - if (column.platforms?.length === 1) { - const platform = column.platforms[0]; - toolTipHtml.push( - `${FOOTNOTES.platform} ${platform}` + 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 ( + + {FOOTNOTES.platform} {platformsCopy} + ); - } + }; - if (column.platforms?.length === 2) { - const platform1 = PLATFORM_DISPLAY_NAMES[column.platforms[0]]; - const platform2 = PLATFORM_DISPLAY_NAMES[column.platforms[1]]; - toolTipHtml.push( - `${FOOTNOTES.platform} ${platform1} and ${platform2}.` - ); - } - - 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( - `${FOOTNOTES.platform} ${platform1}, ${platform2}, and ${platform3}.` - ); - } - - const tooltip = toolTipHtml.join(""); - return tooltip; + return ( + <> + + {column.description} + + {column.required && ( + {FOOTNOTES.required} + )} + {column.requires_user_context && renderUserContextFootnote()} + {column.platforms && renderPlatformFootnotes(column.platforms)} + + ); }; const hasFootnotes = (column: IQueryTableColumn) => { @@ -113,7 +112,7 @@ const ColumnListItem = ({
{column.name} diff --git a/frontend/components/side_panels/QuerySidePanel/QueryTableColumns/ColumnListItem/_styles.scss b/frontend/components/side_panels/QuerySidePanel/QueryTableColumns/ColumnListItem/_styles.scss index 78495fbedb..71b8414307 100644 --- a/frontend/components/side_panels/QuerySidePanel/QueryTableColumns/ColumnListItem/_styles.scss +++ b/frontend/components/side_panels/QuerySidePanel/QueryTableColumns/ColumnListItem/_styles.scss @@ -52,7 +52,7 @@ margin: $pad-small 0; display: block; - &:last-child { + &:last-of-type { margin-bottom: 0; } } diff --git a/frontend/interfaces/datatable_config.ts b/frontend/interfaces/datatable_config.ts index 8d8b781caa..79207ff83f 100644 --- a/frontend/interfaces/datatable_config.ts +++ b/frontend/interfaces/datatable_config.ts @@ -4,7 +4,6 @@ export type IDataColumn = Column & { title?: string; disableHidden?: boolean; disableSortBy?: boolean; - isLastColumn?: boolean; filterValue?: any; preFilteredRows?: any; setFilter?: any; diff --git a/frontend/pages/DashboardPage/cards/HostsSummary/SummaryTile/SummaryTile.tests.tsx b/frontend/pages/DashboardPage/cards/HostsSummary/SummaryTile/SummaryTile.tests.tsx index 0b1fcdebe9..e5b9404476 100644 --- a/frontend/pages/DashboardPage/cards/HostsSummary/SummaryTile/SummaryTile.tests.tsx +++ b/frontend/pages/DashboardPage/cards/HostsSummary/SummaryTile/SummaryTile.tests.tsx @@ -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(); }); diff --git a/frontend/pages/DashboardPage/cards/HostsSummary/SummaryTile/SummaryTile.tsx b/frontend/pages/DashboardPage/cards/HostsSummary/SummaryTile/SummaryTile.tsx index a6984654ea..55e5954ab3 100644 --- a/frontend/pages/DashboardPage/cards/HostsSummary/SummaryTile/SummaryTile.tsx +++ b/frontend/pages/DashboardPage/cards/HostsSummary/SummaryTile/SummaryTile.tsx @@ -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 = ( <>
diff --git a/frontend/pages/DashboardPage/cards/MDM/MDMStatusTableConfig.tsx b/frontend/pages/DashboardPage/cards/MDM/MDMStatusTableConfig.tsx index c0c8d2bd95..1b39017259 100644 --- a/frontend/pages/DashboardPage/cards/MDM/MDMStatusTableConfig.tsx +++ b/frontend/pages/DashboardPage/cards/MDM/MDMStatusTableConfig.tsx @@ -54,7 +54,7 @@ export const generateStatusTableHeaders = (teamId?: number): IDataColumn[] => [ accessor: "status", Cell: (cellProps: IStringCellProps) => ( {cellProps.cell.value} diff --git a/frontend/pages/DashboardPage/cards/Munki/MunkiIssuesTableConfig.tsx b/frontend/pages/DashboardPage/cards/Munki/MunkiIssuesTableConfig.tsx index c10dff38a3..8270dba22e 100644 --- a/frontend/pages/DashboardPage/cards/Munki/MunkiIssuesTableConfig.tsx +++ b/frontend/pages/DashboardPage/cards/Munki/MunkiIssuesTableConfig.tsx @@ -41,9 +41,9 @@ const generateMunkiIssuesTableHeaders = (teamId?: number): IDataColumn[] => [ Header: (): JSX.Element => { const titleWithToolTip = ( Issues reported the last time Munki ran on each host. + } > Issue diff --git a/frontend/pages/admin/IntegrationsPage/cards/AutomaticEnrollment/components/AppleBusinessManagerSection/AppleBusinessManagerSection.tsx b/frontend/pages/admin/IntegrationsPage/cards/AutomaticEnrollment/components/AppleBusinessManagerSection/AppleBusinessManagerSection.tsx index 205293d380..156c6fd5d9 100644 --- a/frontend/pages/admin/IntegrationsPage/cards/AutomaticEnrollment/components/AppleBusinessManagerSection/AppleBusinessManagerSection.tsx +++ b/frontend/pages/admin/IntegrationsPage/cards/AutomaticEnrollment/components/AppleBusinessManagerSection/AppleBusinessManagerSection.tsx @@ -201,7 +201,7 @@ const AppleBusinessManagerSection = ({

Team diff --git a/frontend/pages/admin/IntegrationsPage/cards/Integrations/components/IntegrationForm/IntegrationForm.tsx b/frontend/pages/admin/IntegrationsPage/cards/Integrations/components/IntegrationForm/IntegrationForm.tsx index 43b7658eab..aeb72fe200 100644 --- a/frontend/pages/admin/IntegrationsPage/cards/Integrations/components/IntegrationForm/IntegrationForm.tsx +++ b/frontend/pages/admin/IntegrationsPage/cards/Integrations/components/IntegrationForm/IntegrationForm.tsx @@ -226,12 +226,13 @@ const IntegrationForm = ({ parseTarget value={projectKey} tooltip={ - "\ - To find the Jira project key, head to your project in
\ - Jira. Your project key is located in the URL. For example, in
\ - “jira.example.com/projects/JRAEXAMPLE,”
\ - “JRAEXAMPLE” is your project key. \ - " + <> + To find the Jira project key, head to your project in
+ Jira. Your project key is located in the URL. For example, in{" "} +
+ “jira.example.com/projects/JRAEXAMPLE,”
+ “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 Admin >
\ - People > Groups
. Find the group and select it.
\ - The group ID will appear in the search field. \ - " + <> + To find the Zendesk group ID, select{" "} + + Admin >
+ People > Groups +
+ . Find the group and select it.
+ The group ID will appear in the search field. + } /> )} diff --git a/frontend/pages/admin/OrgSettingsPage/cards/Advanced/Advanced.tsx b/frontend/pages/admin/OrgSettingsPage/cards/Advanced/Advanced.tsx index 62f6d4d4b5..bc0852f9e9 100644 --- a/frontend/pages/admin/OrgSettingsPage/cards/Advanced/Advanced.tsx +++ b/frontend/pages/admin/OrgSettingsPage/cards/Advanced/Advanced.tsx @@ -115,7 +115,13 @@ const Advanced = ({ value={domain} parseTarget tooltip={ - '

If you need to specify a HELO domain,
you can do it here (Default: Blank)

' +

+ If you need to specify a HELO domain,
+ you can do it here{" "} + + (Default: Blank) + +

} /> Turn this off (not recommended)
if you use a self-signed certificate
(Default: On)

' + tooltipContent={ +

+ Turn this off (not recommended)
+ if you use a self-signed certificate{" "} + +
+ (Default: On) +
+

} > Verify SSL certs @@ -134,8 +147,15 @@ const Advanced = ({ name="enableStartTLS" value={enableStartTLS} parseTarget - tooltip={ - '

Detects if STARTTLS is enabled
in your SMTP server and starts
to use it. (Default: On)

' + tooltipContent={ +

+ Detects if STARTTLS is enabled
+ in your SMTP server and starts
+ to use it.{" "} + + (Default: On) + +

} > Enable STARTTLS @@ -145,8 +165,15 @@ const Advanced = ({ name="enableHostExpiry" value={enableHostExpiry} parseTarget - tooltip={ - '

When enabled, allows automatic cleanup
of hosts that have not communicated with Fleet
in some number of days. (Default: Off)

' + tooltipContent={ +

+ When enabled, allows automatic cleanup
+ of hosts that have not communicated with Fleet
+ in some number of days.{" "} + + (Default: Off) + +

} > Host expiry @@ -161,8 +188,11 @@ const Advanced = ({ parseTarget onBlur={validateForm} error={formErrors.host_expiry_window} - tooltip={ - "

If a host has not communicated with Fleet in the specified number of days, it will be removed.

" + tooltipContent={ +

+ If a host has not communicated with Fleet in the specified + number of days, it will be removed. +

} /> When enabled, disables the ability to run live queries
(ad hoc queries executed via the UI or fleetctl). (Default: Off)

' + tooltipContent={ +

+ When enabled, disables the ability to run live queries{" "} +
+ (ad hoc queries executed via the UI or fleetctl).{" "} + + (Default: Off) + +

} > 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={ - '

Disabling query reports will decrease database usage,
\ - but will prevent you from accessing query results in
\ - Fleet and will delete existing reports. This can also be
\ - disabled on a per-query basis by enabling "Discard
\ - data". (Default: Off)

' + tooltipContent={ + <> +

+ Disabling query reports will decrease database usage,{" "} +
\ but will prevent you from accessing query results + in +
\ Fleet and will delete existing reports. This can + also be +
\ disabled on a per-query basis by enabling + "Discard
\ data".{" "} + + (Default: Off) + +

+ } > Disable query reports diff --git a/frontend/pages/admin/OrgSettingsPage/cards/HostStatusWebhook/HostStatusWebhook.tsx b/frontend/pages/admin/OrgSettingsPage/cards/HostStatusWebhook/HostStatusWebhook.tsx index 8dd1bab37e..fd149e1a34 100644 --- a/frontend/pages/admin/OrgSettingsPage/cards/HostStatusWebhook/HostStatusWebhook.tsx +++ b/frontend/pages/admin/OrgSettingsPage/cards/HostStatusWebhook/HostStatusWebhook.tsx @@ -181,9 +181,10 @@ const HostStatusWebhook = ({ onBlur={validateForm} error={formErrors.destination_url} tooltip={ - "\ -

Provide a URL to deliver
the webhook request to.

\ - " +

+ Provide a URL to deliver
+ the webhook request to. +

} />

@@ -197,9 +198,13 @@ const HostStatusWebhook = ({ parseTarget onBlur={validateForm} tooltip={ - "\ -

Select the minimum percentage of hosts that
must fail to check into Fleet in order to trigger
the webhook request.

\ - " +

+ Select the minimum percentage of hosts that +
+ must fail to check into Fleet in order to trigger +
+ the webhook request. +

} />
@@ -213,9 +218,15 @@ const HostStatusWebhook = ({ parseTarget onBlur={validateForm} tooltip={ - "\ -

Select the minimum number of days that the
configured Percentage of hosts must fail to
check into Fleet in order to trigger the
webhook request.

\ - " +

+ Select the minimum number of days that the +
+ configured Percentage of hosts must fail to +
+ check into Fleet in order to trigger the +
+ webhook request. +

} />
diff --git a/frontend/pages/admin/OrgSettingsPage/cards/Smtp/Smtp.tsx b/frontend/pages/admin/OrgSettingsPage/cards/Smtp/Smtp.tsx index e1ae833ec4..ecca349a3a 100644 --- a/frontend/pages/admin/OrgSettingsPage/cards/Smtp/Smtp.tsx +++ b/frontend/pages/admin/OrgSettingsPage/cards/Smtp/Smtp.tsx @@ -248,11 +248,21 @@ const Smtp = ({ value={smtpAuthenticationType} parseTarget tooltip={ - "\ -

If your mail server requires authentication, you need to specify the authentication type here.

\ -

No Authentication - Select this if your SMTP is open.

\ -

Username & Password - Select this if your SMTP server requires authentication with a username and password.

\ - " + <> +

+ If your mail server requires authentication, you need to + specify the authentication type here. +

+

+ No Authentication - Select this if your SMTP + is open. +

+

+ Username & Password - Select this if your + SMTP server requires authentication with a username and + password. +

+ } /> {renderSmtpSection()} diff --git a/frontend/pages/admin/OrgSettingsPage/cards/Sso/Sso.tsx b/frontend/pages/admin/OrgSettingsPage/cards/Sso/Sso.tsx index 6af41648d6..a20fe14418 100644 --- a/frontend/pages/admin/OrgSettingsPage/cards/Sso/Sso.tsx +++ b/frontend/pages/admin/OrgSettingsPage/cards/Sso/Sso.tsx @@ -171,7 +171,8 @@ const Sso = ({ parseTarget onBlur={validateForm} error={formErrors.idp_image_url} - tooltip="An optional link to an image such
as a logo for the identity provider." + tooltip={`An optional link to an image such + as a logo for the identity provider.`} />
@@ -184,7 +185,8 @@ const Sso = ({ parseTarget onBlur={validateForm} error={formErrors.metadata} - tooltip="Metadata provided by the identity provider. Either
metadata or a metadata url must be provided." + tooltip={`Metadata provided by the identity provider. Either + metadata or a metadata url must be provided.`} />
diff --git a/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/MembersPage/MembersPageTableConfig.tsx b/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/MembersPage/MembersPageTableConfig.tsx index d2634c094b..e159e6449b 100644 --- a/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/MembersPage/MembersPageTableConfig.tsx +++ b/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/MembersPage/MembersPageTableConfig.tsx @@ -125,12 +125,16 @@ const generateTableHeaders = ( if (cellProps.cell.value === "GitOps") { return ( - when creating an API-only user. This user has no
- access to the UI. - `} + position="top-start" + tipContent={ + <> + The GitOps role is only available on the command-line +
+ when creating an API-only user. This user has no +
+ access to the UI. + + } > GitOps
@@ -139,12 +143,16 @@ const generateTableHeaders = ( if (cellProps.cell.value === "Observer+") { return ( - the same functions as an Observer, with the added
- ability to run any live query against all hosts. - `} + position="top-start" + tipContent={ + <> + Users with the Observer+ role have access to all of +
+ the same functions as an Observer, with the added +
+ ability to run any live query against all hosts. + + } > {cellProps.cell.value}
diff --git a/frontend/pages/admin/UserManagementPage/components/UserForm/UserForm.tsx b/frontend/pages/admin/UserManagementPage/components/UserForm/UserForm.tsx index 9a35ecf208..353036bce2 100644 --- a/frontend/pages/admin/UserManagementPage/components/UserForm/UserForm.tsx +++ b/frontend/pages/admin/UserManagementPage/components/UserForm/UserForm.tsx @@ -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. \ -

\ - Users with Admin role can configure SMTP in Settings > Organization settings. \ - " + <> + Editing an email address requires that SMTP or SES is configured in + order to send a validation email. +
+
+ Users with Admin role can configure SMTP in{" "} + Settings > Organization settings. + } /> {!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. -

- Users with Admin role can configure SSO in Settings > Organization settings. - `} + tooltipContent={ + <> + Enabling single sign-on for a user requires that SSO is first + enabled for the organization. +
+
+ Users with Admin role can configure SSO in{" "} + Settings > Organization settings. + + } > Enable single sign-on @@ -470,14 +477,18 @@ const UserForm = ({ name={"newUserType"} onChange={onRadioChange("newUserType")} tooltip={ - smtpConfigured || sesConfigured - ? "" - : ` - The "Invite user" feature requires that SMTP or SES - is configured in order to send invitation emails. -

- SMTP can be configured in Settings > Organization settings. - ` + smtpConfigured || sesConfigured ? ( + "" + ) : ( + <> + The "Invite user" feature requires that SMTP + or SES is configured in order to send invitation emails. +
+
+ SMTP can be configured in Settings > 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.

\ - 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. +
+
+ This user will not be asked to set a new password after + logging in to fleetctl or the Fleet API. + + } />
diff --git a/frontend/pages/admin/UserManagementPage/components/UsersTable/UsersTableConfig.tsx b/frontend/pages/admin/UserManagementPage/components/UsersTable/UsersTableConfig.tsx index 445dc42bec..96c6da6d2f 100644 --- a/frontend/pages/admin/UserManagementPage/components/UsersTable/UsersTableConfig.tsx +++ b/frontend/pages/admin/UserManagementPage/components/UsersTable/UsersTableConfig.tsx @@ -131,12 +131,16 @@ const generateTableHeaders = ( if (cellProps.cell.value === "GitOps") { return ( - when creating an API-only user. This user has no
- access to the UI. - `} + position="top-start" + tipContent={ + <> + The GitOps role is only available on the command-line +
+ when creating an API-only user. This user has no +
+ access to the UI. + + } > GitOps
@@ -145,12 +149,16 @@ const generateTableHeaders = ( if (cellProps.cell.value === "Observer+") { return ( - the same functions as an Observer, with the added
- ability to run any live query against all hosts. - `} + position="top-start" + tipContent={ + <> + Users with the Observer+ role have access to all of +
+ the same functions as an Observer, with the added +
+ ability to run any live query against all hosts. + + } > {cellProps.cell.value}
diff --git a/frontend/pages/hosts/ManageHostsPage/HostTableConfig.tsx b/frontend/pages/hosts/ManageHostsPage/HostTableConfig.tsx index 183cb4c00b..66fa71a3cf 100644 --- a/frontend/pages/hosts/ManageHostsPage/HostTableConfig.tsx +++ b/frontend/pages/hosts/ManageHostsPage/HostTableConfig.tsx @@ -47,7 +47,6 @@ interface IHeaderProps { column: { title: string; isSortedDesc: boolean; - isLastColumn?: boolean; }; getToggleAllRowsSelectedProps: () => IGetToggleAllRowsSelectedProps; toggleAllRowsSelected: () => void; @@ -149,7 +148,6 @@ const allHostTableHeaders: IDataColumn[] = [ ), accessor: "display_name", @@ -208,7 +206,6 @@ const allHostTableHeaders: IDataColumn[] = [ ), accessor: "hostname", @@ -220,7 +217,6 @@ const allHostTableHeaders: IDataColumn[] = [ ), accessor: "computer_name", @@ -232,7 +228,6 @@ const allHostTableHeaders: IDataColumn[] = [ ), accessor: "team_name", @@ -245,11 +240,13 @@ const allHostTableHeaders: IDataColumn[] = [ Header: (cellProps: IHeaderProps): JSX.Element => { const titleWithToolTip = ( - hosts won’t respond to a live query because
- they may be shut down, asleep, or not
- connected to the internet.`} + tipContent={ + <> + Online hosts will respond to a live query. Offline hosts won’t + 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[] = [ ); }, @@ -291,7 +287,6 @@ const allHostTableHeaders: IDataColumn[] = [ ), accessor: "gigs_disk_space_available", @@ -321,7 +316,6 @@ const allHostTableHeaders: IDataColumn[] = [ ), accessor: "os_version", @@ -382,7 +376,6 @@ const allHostTableHeaders: IDataColumn[] = [ ), accessor: "primary_ip", @@ -393,21 +386,18 @@ const allHostTableHeaders: IDataColumn[] = [ Header: (cellProps: IHeaderProps): JSX.Element => { const titleWithToolTip = ( - on. To filter by MDM status, head to the Dashboard page. - `} + tipContent={ + <> + Settings can be updated remotely on hosts with MDM turned +
+ on. To filter by MDM status, head to the Dashboard page. + + } > MDM status
); - return ( - - ); + return ; }, disableSortBy: true, accessor: "mdm.enrollment_status", @@ -427,21 +417,18 @@ const allHostTableHeaders: IDataColumn[] = [ Header: (cellProps: IHeaderProps): JSX.Element => { const titleWithToolTip = ( - filter by MDM server URL, head to the Dashboard page. - `} + tipContent={ + <> + The MDM server that updates settings on the host. To +
+ filter by MDM server URL, head to the Dashboard page. + + } > MDM server URL
); - return ( - - ); + return ; }, disableSortBy: true, accessor: "mdm.server_url", @@ -462,7 +449,6 @@ const allHostTableHeaders: IDataColumn[] = [ ), accessor: "public_ip", @@ -506,9 +492,12 @@ const allHostTableHeaders: IDataColumn[] = [ Header: (cellProps: IHeaderProps): JSX.Element => { const titleWithToolTip = ( reported vitals. - `} + tipContent={ + <> + The last time the host +
reported vitals. + + } > Last fetched
@@ -517,7 +506,6 @@ const allHostTableHeaders: IDataColumn[] = [ ); }, @@ -534,9 +522,12 @@ const allHostTableHeaders: IDataColumn[] = [ Header: (cellProps: IHeaderProps): JSX.Element => { const titleWithToolTip = ( host was online. - `} + tipContent={ + <> + The last time the
+ host was online. + + } > Last seen
@@ -545,7 +536,6 @@ const allHostTableHeaders: IDataColumn[] = [ ); }, @@ -563,7 +553,6 @@ const allHostTableHeaders: IDataColumn[] = [ ), accessor: "uuid", @@ -577,7 +566,6 @@ const allHostTableHeaders: IDataColumn[] = [ ), accessor: "uptime", @@ -610,7 +598,6 @@ const allHostTableHeaders: IDataColumn[] = [ ), accessor: "memory", @@ -624,7 +611,6 @@ const allHostTableHeaders: IDataColumn[] = [ ), accessor: "primary_mac", @@ -636,7 +622,6 @@ const allHostTableHeaders: IDataColumn[] = [ ), accessor: "hardware_serial", @@ -648,7 +633,6 @@ const allHostTableHeaders: IDataColumn[] = [ ), accessor: "hardware_model", diff --git a/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tsx b/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tsx index b832c48c64..677536b62b 100644 --- a/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tsx +++ b/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tsx @@ -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", diff --git a/frontend/pages/hosts/ManageHostsPage/_styles.scss b/frontend/pages/hosts/ManageHostsPage/_styles.scss index e4562695a4..c25f09502f 100644 --- a/frontend/pages/hosts/ManageHostsPage/_styles.scss +++ b/frontend/pages/hosts/ManageHostsPage/_styles.scss @@ -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 { diff --git a/frontend/pages/hosts/details/cards/About/About.tsx b/frontend/pages/hosts/details/cards/About/About.tsx index c63449ee80..f9de0d032b 100644 --- a/frontend/pages/hosts/details/cards/About/About.tsx +++ b/frontend/pages/hosts/details/cards/About/About.tsx @@ -103,7 +103,6 @@ const About = ({ MDM status {mdm.enrollment_status} diff --git a/frontend/pages/hosts/details/cards/AgentOptions/AgentOptions.tsx b/frontend/pages/hosts/details/cards/AgentOptions/AgentOptions.tsx index d5884e4bc5..6bb1932c29 100644 --- a/frontend/pages/hosts/details/cards/AgentOptions/AgentOptions.tsx +++ b/frontend/pages/hosts/details/cards/AgentOptions/AgentOptions.tsx @@ -48,7 +48,6 @@ const AgentOptions = ({ {isChromeOS ? ( Agent options diff --git a/frontend/pages/hosts/details/cards/HostSummary/HostSummary.tsx b/frontend/pages/hosts/details/cards/HostSummary/HostSummary.tsx index 789bc51c2a..a39ad9a23b 100644 --- a/frontend/pages/hosts/details/cards/HostSummary/HostSummary.tsx +++ b/frontend/pages/hosts/details/cards/HostSummary/HostSummary.tsx @@ -176,7 +176,7 @@ const HostSummary = ({ return (
Disk encryption - + {statusText}
diff --git a/frontend/pages/hosts/details/cards/MunkiIssues/MunkiIssuesTableConfig.tsx b/frontend/pages/hosts/details/cards/MunkiIssues/MunkiIssuesTableConfig.tsx index 24852d1ae2..eb8c31b602 100644 --- a/frontend/pages/hosts/details/cards/MunkiIssues/MunkiIssuesTableConfig.tsx +++ b/frontend/pages/hosts/details/cards/MunkiIssues/MunkiIssuesTableConfig.tsx @@ -60,9 +60,9 @@ export const munkiIssuesTableHeaders: IDataColumn[] = [ Header: (headerProps: IHeaderProps): JSX.Element => { const titleWithToolTip = ( Issues reported the last time Munki ran on each host. + } > Issue @@ -95,9 +95,7 @@ export const munkiIssuesTableHeaders: IDataColumn[] = [ Header: (headerProps: IHeaderProps): JSX.Element => { const titleWithToolTip = ( The first time Munki reported this issue.} > Time diff --git a/frontend/pages/hosts/details/cards/Packs/PackTable/PackTableConfig.tsx b/frontend/pages/hosts/details/cards/Packs/PackTable/PackTableConfig.tsx index 36ca84584a..4f1a5101d3 100644 --- a/frontend/pages/hosts/details/cards/Packs/PackTable/PackTableConfig.tsx +++ b/frontend/pages/hosts/details/cards/Packs/PackTable/PackTableConfig.tsx @@ -79,7 +79,16 @@ const generatePackTableHeaders = (): IDataColumn[] => { { Header: () => { return ( - + + The last time the query ran +
+ since the last time osquery
+ started on this host. + + } + > Last run
); @@ -93,7 +102,14 @@ const generatePackTableHeaders = (): IDataColumn[] => { { Header: () => { return ( - + + This is the performance
+ impact on this host. + + } + > Performance impact
); diff --git a/frontend/pages/hosts/details/cards/Schedule/ScheduleTableConfig.tsx b/frontend/pages/hosts/details/cards/Schedule/ScheduleTableConfig.tsx index c02374741b..1bada23c35 100644 --- a/frontend/pages/hosts/details/cards/Schedule/ScheduleTableConfig.tsx +++ b/frontend/pages/hosts/details/cards/Schedule/ScheduleTableConfig.tsx @@ -76,7 +76,14 @@ const generateTableHeaders = (): IDataColumn[] => { { Header: () => { return ( - + + This is the performance
+ impact on this host. + + } + > Performance impact
); diff --git a/frontend/pages/hosts/details/cards/Software/SoftwareTableConfig.tsx b/frontend/pages/hosts/details/cards/Software/SoftwareTableConfig.tsx index 1dec977d81..8e7e244835 100644 --- a/frontend/pages/hosts/details/cards/Software/SoftwareTableConfig.tsx +++ b/frontend/pages/hosts/details/cards/Software/SoftwareTableConfig.tsx @@ -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) => ( Bundle identifier: -
- ${bundle} +
${bundle}
- `} + } > {name}
@@ -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 ( - + + This is where the software is
+ located on this host. + + } + > File path
); diff --git a/frontend/pages/hosts/details/cards/Users/UsersTable/UsersTableConfig.tsx b/frontend/pages/hosts/details/cards/Users/UsersTable/UsersTableConfig.tsx index 501789dbb2..7d5e2c857d 100644 --- a/frontend/pages/hosts/details/cards/Users/UsersTable/UsersTableConfig.tsx +++ b/frontend/pages/hosts/details/cards/Users/UsersTable/UsersTableConfig.tsx @@ -49,7 +49,17 @@ const generateUsersTableHeaders = (): IDataColumn[] => { { Header: () => { return ( - + + The command line shell, such as bash, +
+ that this user is equipped with by +
+ default when they log in to the system. + + } + > Shell
); diff --git a/frontend/pages/policies/ManagePoliciesPage/ManagePoliciesPage.tsx b/frontend/pages/policies/ManagePoliciesPage/ManagePoliciesPage.tsx index 3f6c875c1e..bde8ac5a48 100644 --- a/frontend/pages/policies/ManagePoliciesPage/ManagePoliciesPage.tsx +++ b/frontend/pages/policies/ManagePoliciesPage/ManagePoliciesPage.tsx @@ -767,9 +767,9 @@ const ManagePolicyPage = ({ globalPoliciesCount )} caretPosition={"before"} - tooltipHtml={ - '"All teams" policies are checked
for this team’s hosts.' - } + tooltipHtml={`"All teams" policies are checked ${( +
+ )} for this team's hosts.`} onClick={toggleShowInheritedPolicies} /> )} diff --git a/frontend/pages/policies/PolicyPage/components/PolicyForm/PolicyForm.tests.tsx b/frontend/pages/policies/PolicyPage/components/PolicyForm/PolicyForm.tests.tsx index b72e4d954c..145908bb0c 100644 --- a/frontend/pages/policies/PolicyPage/components/PolicyForm/PolicyForm.tests.tsx +++ b/frontend/pages/policies/PolicyPage/components/PolicyForm/PolicyForm.tests.tsx @@ -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: { diff --git a/frontend/pages/policies/PolicyPage/components/PolicyForm/PolicyForm.tsx b/frontend/pages/policies/PolicyPage/components/PolicyForm/PolicyForm.tsx index 46a9b8a283..14d61f37e2 100644 --- a/frontend/pages/policies/PolicyPage/components/PolicyForm/PolicyForm.tsx +++ b/frontend/pages/policies/PolicyPage/components/PolicyForm/PolicyForm.tsx @@ -460,9 +460,11 @@ const PolicyForm = ({ > If automations are turned on, this
information is included.

" +

+ If automations are turned on, this +
information is included. +

} - isDelayed > Critical:
diff --git a/frontend/pages/policies/PolicyPage/components/SaveNewPolicyModal/SaveNewPolicyModal.tsx b/frontend/pages/policies/PolicyPage/components/SaveNewPolicyModal/SaveNewPolicyModal.tsx index d4d69b8fac..3b0c8e976e 100644 --- a/frontend/pages/policies/PolicyPage/components/SaveNewPolicyModal/SaveNewPolicyModal.tsx +++ b/frontend/pages/policies/PolicyPage/components/SaveNewPolicyModal/SaveNewPolicyModal.tsx @@ -153,9 +153,11 @@ const SaveNewPolicyModal = ({ > If automations are turned on, this
information is included.

" +

+ If automations are turned on, this +
information is included. +

} - isDelayed > Critical:
diff --git a/frontend/pages/queries/ManageQueriesPage/components/PreviewDataModal/PreviewDataModal.tsx b/frontend/pages/queries/ManageQueriesPage/components/PreviewDataModal/PreviewDataModal.tsx index 3156c73288..308679f1a0 100644 --- a/frontend/pages/queries/ManageQueriesPage/components/PreviewDataModal/PreviewDataModal.tsx +++ b/frontend/pages/queries/ManageQueriesPage/components/PreviewDataModal/PreviewDataModal.tsx @@ -39,7 +39,12 @@ const PreviewDataModal = ({

+ The "snapshot" key includes the query's results. + These will be unique to your query. + + } > The data sent to your configured log destination will look similar to the following JSON: diff --git a/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTableConfig.tsx b/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTableConfig.tsx index b577a6e266..052843d00f 100644 --- a/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTableConfig.tsx +++ b/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTableConfig.tsx @@ -197,9 +197,12 @@ const generateTableHeaders = ({ return (

- all hosts where this query was scheduled.`} + tipContent={ + <> + This is the average performance impact across
+ all hosts where this query was scheduled. + + } > Performance impact
diff --git a/frontend/pages/queries/details/QueryDetailsPage/QueryDetailsPage.tsx b/frontend/pages/queries/details/QueryDetailsPage/QueryDetailsPage.tsx index 1ef27f8b0f..3d88a76f51 100644 --- a/frontend/pages/queries/details/QueryDetailsPage/QueryDetailsPage.tsx +++ b/frontend/pages/queries/details/QueryDetailsPage/QueryDetailsPage.tsx @@ -255,8 +255,15 @@ const QueryDetailsPage = ({
destination on a schedule. When automations are on,
data is sent according to a query’s frequency.`} + tipContent={ + <> + Query automations let you send data to your log
+ destination on a schedule. When automations are + on + ,
+ data is sent according to a query's frequency. + + } > Automations:
diff --git a/frontend/pages/queries/details/QueryDetailsPage/_styles.scss b/frontend/pages/queries/details/QueryDetailsPage/_styles.scss index cb560d4bf7..29374fb06a 100644 --- a/frontend/pages/queries/details/QueryDetailsPage/_styles.scss +++ b/frontend/pages/queries/details/QueryDetailsPage/_styles.scss @@ -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, diff --git a/frontend/pages/queries/details/components/NoResults/NoResults.tsx b/frontend/pages/queries/details/components/NoResults/NoResults.tsx index 3b46b6840c..4841acb24d 100644 --- a/frontend/pages/queries/details/components/NoResults/NoResults.tsx +++ b/frontend/pages/queries/details/components/NoResults/NoResults.tsx @@ -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 `
The following setting prevents saving this query's results in Fleet:
\ -
  • Query reports are globally disabled in organization settings.
`; + return ( + <> + {" "} +
+ The following setting prevents saving this query's results + in Fleet: +
+ \ +
+   • Query reports are globally disabled in organization + settings. +
+ + ); } if (discardDataEnabled) { - return `
The following setting prevents saving this query's results in Fleet:
\ -
  • This query has Discard data enabled.
`; + return ( + <> +
+ The following setting prevents saving this query's results + in Fleet: +
+ \ +
+   • This query has Discard data enabled. +
+ + ); } if (!loggingSnapshot) { - return `
The following setting prevents saving this query's results in Fleet:
\ -
  • The logging setting for this query is not Snapshot.
`; + return ( + <> +
+ The following setting prevents saving this query's results + in Fleet: +
+ \ +
+   • The logging setting for this query is not{" "} + Snapshot. +
+ + ); } return "Unknown"; }; diff --git a/frontend/pages/queries/details/components/QueryReport/QueryReport.tsx b/frontend/pages/queries/details/components/QueryReport/QueryReport.tsx index 32516465e5..5467d97cce 100644 --- a/frontend/pages/queries/details/components/QueryReport/QueryReport.tsx +++ b/frontend/pages/queries/details/components/QueryReport/QueryReport.tsx @@ -114,10 +114,16 @@ const QueryReport = ({ return (

- You can reset this report by updating the query's SQL, or by - temporarily enabling the discard data setting and disabling it again.`} + tipContent={ + <> + Fleet has retained a sample of early results for reference. + Reporting is paused until existing data is deleted.
+
+ You can reset this report by updating the query's SQL, or + by temporarily enabling the discard data setting and + disabling it again. + + } > {`${count} result${count === 1 ? "" : "s"}`}
diff --git a/frontend/pages/queries/edit/components/DiscardDataOption/DiscardDataOption.tests.tsx b/frontend/pages/queries/edit/components/DiscardDataOption/DiscardDataOption.tests.tsx index 011e23d502..89cce972f1 100644 --- a/frontend/pages/queries/edit/components/DiscardDataOption/DiscardDataOption.tests.tsx +++ b/frontend/pages/queries/edit/components/DiscardDataOption/DiscardDataOption.tests.tsx @@ -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 () => { diff --git a/frontend/pages/queries/edit/components/DiscardDataOption/DiscardDataOption.tsx b/frontend/pages/queries/edit/components/DiscardDataOption/DiscardDataOption.tsx index 6cfa573679..863fad7d40 100644 --- a/frontend/pages/queries/edit/components/DiscardDataOption/DiscardDataOption.tsx +++ b/frontend/pages/queries/edit/components/DiscardDataOption/DiscardDataOption.tsx @@ -32,12 +32,16 @@ const DiscardDataOption = ({ <> This setting is ignored because query reports in Fleet have been{" "} \ - Organization settings > Advanced options > Disable query reports." + <> + A Fleet administrator can enable query reports under
+ + Organization settings > Advanced options > Disable query + reports + + . + } - position="bottom" > {"globally disabled."}
{" "} diff --git a/frontend/pages/software/ManageSoftwarePage/SoftwareTableConfig.tsx b/frontend/pages/software/ManageSoftwarePage/SoftwareTableConfig.tsx index 58f7a8a3d4..531479d2eb 100644 --- a/frontend/pages/software/ManageSoftwarePage/SoftwareTableConfig.tsx +++ b/frontend/pages/software/ManageSoftwarePage/SoftwareTableConfig.tsx @@ -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 = ( - in the next 30 days (EPSS probability). This data is -
- reported by FIRST.org. - `} + tipContent={ + <> + The probability that this software will be exploited +
+ in the next 30 days (EPSS probability). This data is +
+ reported by FIRST.org. + + } > Probability of exploit
@@ -205,7 +207,7 @@ const generateTableHeaders = ( customOnClick={onClickSoftware} value={name} tooltipContent={ - bundle ? getSoftwareBundleTooltipMarkup(bundle) : undefined + bundle ? getSoftwareBundleTooltipJSX(bundle) : undefined } /> ); diff --git a/frontend/pages/software/SoftwareDetailsPage/components/Vulnerabilities/VulnTableConfig.tsx b/frontend/pages/software/SoftwareDetailsPage/components/Vulnerabilities/VulnTableConfig.tsx index dd1556ff3e..518b8f58b5 100644 --- a/frontend/pages/software/SoftwareDetailsPage/components/Vulnerabilities/VulnTableConfig.tsx +++ b/frontend/pages/software/SoftwareDetailsPage/components/Vulnerabilities/VulnTableConfig.tsx @@ -92,10 +92,14 @@ const generateVulnTableHeaders = ( Header: (headerProps: IHeaderProps): JSX.Element => { const titleWithToolTip = ( - This data is reported by FIRST.org. - `} + tipContent={ + <> + The probability that this vulnerability will be exploited in the + next 30 days (EPSS probability). +
+ This data is reported by FIRST.org. + + } > Probability of exploit
@@ -121,10 +125,15 @@ const generateVulnTableHeaders = ( Header: (headerProps: IHeaderProps): JSX.Element => { const titleWithToolTip = ( - This data is reported by the National Vulnerability Database (NVD). - `} + tipContent={ + <> + The worst case impact across different environments (CVSS base + score). +
+ This data is reported by the National Vulnerability Database + (NVD). + + } > Severity
@@ -151,10 +160,13 @@ const generateVulnTableHeaders = ( Header: (headerProps: IHeaderProps): JSX.Element => { const titleWithToolTip = ( + The vulnerability has been actively exploited in the wild. This + data is reported by the Cybersecurity and Infrustructure + Security Agency (CISA). + + } > Known exploit @@ -181,7 +193,12 @@ const generateVulnTableHeaders = ( Header: (headerProps: IHeaderProps): JSX.Element => { const titleWithToolTip = ( + The date this vulnerability was published in the National + Vulnerability Database (NVD). + + } > Published diff --git a/frontend/styles/var/colors.scss b/frontend/styles/var/colors.scss index a16b2fa847..fd64221d3f 100644 --- a/frontend/styles/var/colors.scss +++ b/frontend/styles/var/colors.scss @@ -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; diff --git a/frontend/test/test-setup.ts b/frontend/test/test-setup.ts index 32d6215065..fd4495869b 100644 --- a/frontend/test/test-setup.ts +++ b/frontend/test/test-setup.ts @@ -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()); diff --git a/frontend/utilities/helpers.ts b/frontend/utilities/helpers.tsx similarity index 95% rename from frontend/utilities/helpers.ts rename to frontend/utilities/helpers.tsx index 3caddfbd26..4e5fa11932 100644 --- a/frontend/utilities/helpers.ts +++ b/frontend/utilities/helpers.tsx @@ -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 ` - - Bundle identifier: -
- ${bundle} -
- `; -}; +export const getSoftwareBundleTooltipJSX = (bundle: string) => ( + + Bundle identifier: +
+ {bundle} +
+); export const TAGGED_TEMPLATES = { queryByHostRoute: (hostId: number | undefined | null) => { diff --git a/frontend/utilities/osquery_tables.ts b/frontend/utilities/osquery_tables.ts index 375426a5a0..d9264d13f1 100644 --- a/frontend/utilities/osquery_tables.ts +++ b/frontend/utilities/osquery_tables.ts @@ -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"; diff --git a/package.json b/package.json index 63a054c584..7ea919209d 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/yarn.lock b/yarn.lock index 73678900e3..a5ab594660 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"