Fleet UI: Some of many tooltip updates to react-tooltip 5.x (#42830)

This commit is contained in:
RachelElysia
2026-04-02 13:32:30 -04:00
committed by GitHub
parent 3e55165a4b
commit 226df9277c
43 changed files with 568 additions and 931 deletions
@@ -1,15 +1,14 @@
import React, { ReactNode } from "react";
import ReactTooltip from "react-tooltip";
import classnames from "classnames";
import { noop, uniqueId } from "lodash";
import { noop } from "lodash";
import { COLORS } from "styles/var/colors";
import { dateAgo } from "utilities/date_format";
import { internationalTimeFormat } from "utilities/helpers";
import Avatar from "components/Avatar";
import Button from "components/buttons/Button";
import Icon from "components/Icon";
import TooltipWrapper from "components/TooltipWrapper";
const baseClass = "feed-list-item";
@@ -47,8 +46,6 @@ const FeedListItem = ({
[`${baseClass}__no-details`]: !allowShowDetails,
});
const tooltipId = uniqueId();
return (
<div className={classNames}>
<div className={`${baseClass}__avatar-wrapper`}>
@@ -73,25 +70,16 @@ const FeedListItem = ({
<span>{children}</span>
</span>
<br />
<span
<TooltipWrapper
className={`${baseClass}__details-bottomline`}
data-tip
data-for={tooltipId}
position="top"
disableTooltip={!createdAt}
tipContent={createdAt ? internationalTimeFormat(createdAt) : ""}
underline={false}
showArrow
>
{createdAt && dateAgo(createdAt)}
</span>
{createdAt && (
<ReactTooltip
className="date-tooltip"
place="top"
type="dark"
effect="solid"
id={tooltipId}
backgroundColor={COLORS["tooltip-bg"]}
>
{internationalTimeFormat(createdAt)}
</ReactTooltip>
)}
</TooltipWrapper>
</div>
<div className={`${baseClass}__details-actions`}>
{allowShowDetails && (
@@ -1,13 +1,10 @@
import React from "react";
import { uniqueId } from "lodash";
import ReactTooltip from "react-tooltip";
import { DEFAULT_EMPTY_CELL_VALUE } from "utilities/constants";
import { formatFloatAsPercentage } from "utilities/helpers";
import Icon from "components/Icon";
import { COLORS } from "styles/var/colors";
import TooltipWrapper from "components/TooltipWrapper";
const baseClass = "probability-of-exploit";
@@ -32,33 +29,24 @@ const ProbabilityOfExploit = ({
}
const renderExploitedIcon = () => {
const tooltipId = uniqueId();
return (
<>
<span
className={`${baseClass} tooltip tooltip__tooltip-icon`}
data-tip
data-for={tooltipId}
>
<TooltipWrapper
tipContent={
<span className="tooltip__tooltip-text">
The vulnerability has been actively exploited in the <br />
wild. This data is reported by the Cybersecurity <br />
and Infrastructure Security Agency (CISA).
</span>
}
position={tooltipPosition}
underline={false}
showArrow
tipOffset={8}
>
<span className={`${baseClass} tooltip tooltip__tooltip-icon`}>
<Icon name="error" size="small" color="status-error" />
</span>
<ReactTooltip
place={tooltipPosition}
effect="solid"
backgroundColor={COLORS["tooltip-bg"]}
id={tooltipId}
data-html
>
<span className={`tooltip__tooltip-text`}>
<>
The vulnerability has been actively exploited in the wild. This
data is reported by the Cybersecurity and Infrastructure Security
Agency (CISA).
</>
</span>
</ReactTooltip>
</>
</TooltipWrapper>
);
};
@@ -1,9 +1,8 @@
import React from "react";
import ReactTooltip from "react-tooltip";
import { COLORS } from "styles/var/colors";
import { DEFAULT_EMPTY_CELL_VALUE } from "utilities/constants";
import Icon from "components/Icon";
import NotSupported from "components/NotSupported";
import TooltipWrapper from "components/TooltipWrapper";
import { IHost } from "interfaces/host";
const baseClass = "host-mdm-status-cell";
@@ -29,28 +28,20 @@ const HostMdmStatusCell = ({
<span className={`${baseClass}`}>
{value}
{mdm?.dep_profile_error && (
<>
<span
className={`tooltip tooltip__tooltip-icon`}
data-tip
data-for={`host-mdm-status__${id}`}
data-tip-disable={false}
>
<Icon name="error-outline" color="status-error" size="medium" />
</span>
<ReactTooltip
place="top"
effect="solid"
backgroundColor={COLORS["tooltip-bg"]}
id={`host-mdm-status__${id}`}
data-html
>
<span className={`tooltip__tooltip-text`}>
Fleet hit Apples API rate limit when preparing the macOS Setup
Assistant for this host. Fleet will try again every hour.
<TooltipWrapper
tipContent={
<span className="tooltip__tooltip-text">
Fleet hit Apple&apos;s API rate limit when preparing the macOS
Setup Assistant for this host. Fleet will try again every hour.
</span>
</ReactTooltip>
</>
}
position="top"
underline={false}
showArrow
tipOffset={8}
>
<Icon name="error-outline" color="status-error" size="medium" />
</TooltipWrapper>
)}
</span>
);
@@ -3,8 +3,5 @@
flex-direction: row;
text-wrap: nowrap;
gap: $pad-small;
.tooltip__tooltip-text {
text-wrap: wrap;
}
align-items: center;
}
@@ -1,8 +1,7 @@
import React from "react";
import ReactTooltip from "react-tooltip";
import Icon from "components/Icon";
import { COLORS } from "styles/var/colors";
import TooltipWrapper from "components/TooltipWrapper";
interface ILiveQueryIssueCellProps<T> {
displayName: string;
@@ -24,39 +23,35 @@ const LiveQueryIssueCell = ({
return (
<>
{displayName}{" "}
<span
className={`host-issue tooltip tooltip__tooltip-icon`}
data-tip
data-for={`host-issue__${rowId.toString()}`}
data-tip-disable={false}
<TooltipWrapper
tipContent={
<span className="tooltip__tooltip-text">
{status === "offline" ? (
<>
Offline hosts will not <br />
respond to a live report.
</>
) : (
<>
This host might take up to
<br /> {distributedInterval} seconds to respond.
</>
)}
</span>
}
position="top"
underline={false}
showArrow
tipOffset={8}
>
<Icon
name="error-outline"
size="small"
color={status === "offline" ? "status-error" : "status-warning"}
/>
</span>
<ReactTooltip
place="top"
effect="solid"
backgroundColor={COLORS["tooltip-bg"]}
id={`host-issue__${rowId.toString()}`}
data-html
>
<span className={`tooltip__tooltip-text`}>
{status === "offline" ? (
<>
Offline hosts will not <br />
respond to a live report.
</>
) : (
<>
This host might take up to
<br /> {distributedInterval} seconds to respond.
</>
)}
<span className="host-issue tooltip tooltip__tooltip-icon">
<Icon
name="error-outline"
size="small"
color={status === "offline" ? "status-error" : "status-warning"}
/>
</span>
</ReactTooltip>
</TooltipWrapper>
</>
);
};
@@ -1,5 +1,5 @@
import React from "react";
import { screen } from "@testing-library/react";
import { screen, waitFor } from "@testing-library/react";
import { renderWithSetup } from "test/test-utils";
import PerformanceImpactCell from "./PerformanceImpactCell";
@@ -14,6 +14,8 @@ describe("Query performance cell", () => {
await user.hover(screen.getByText("Minimal"));
expect(screen.getByText(/little to no impact/i)).toBeInTheDocument();
await waitFor(() => {
expect(screen.getByText(/little to no impact/i)).toBeInTheDocument();
});
});
});
@@ -1,24 +1,20 @@
import React from "react";
import classnames from "classnames";
import { uniqueId } from "lodash";
import ReactTooltip from "react-tooltip";
import { COLORS } from "styles/var/colors";
import TooltipWrapper from "components/TooltipWrapper";
import { getPerformanceImpactIndicatorTooltip } from "utilities/helpers";
import {
isPerformanceImpactIndicator,
PerformanceImpactIndicatorValue,
} from "interfaces/schedulable_query";
import { getPerformanceImpactIndicatorTooltip } from "utilities/helpers";
interface IPerformanceImpactCellValue {
indicator: string;
id?: number;
}
interface IPerformanceImpactCellProps {
value: IPerformanceImpactCellValue;
isHostSpecific?: boolean;
customIdPrefix?: string;
}
const generateClassTag = (rawValue: string): string => {
@@ -30,9 +26,8 @@ const baseClass = "performance-impact-cell";
const PerformanceImpactCell = ({
value,
isHostSpecific = false,
customIdPrefix,
}: IPerformanceImpactCellProps): JSX.Element => {
const { indicator, id } = value;
const { indicator } = value;
const pillClassName = classnames(
"data-table__pill",
`data-table__pill--${generateClassTag(indicator || "")}`,
@@ -46,36 +41,34 @@ const PerformanceImpactCell = ({
"Undetermined",
].includes(indicator);
const tooltipId = uniqueId();
const indicatorValue = isPerformanceImpactIndicator(indicator)
? indicator
: PerformanceImpactIndicatorValue.UNDETERMINED;
return (
<span className={`${baseClass}`}>
<span
data-tip
data-for={`${customIdPrefix || "pill"}__${id?.toString() || tooltipId}`}
data-tip-disable={disableTooltip}
<TooltipWrapper
tipContent={
<span
className={`tooltip ${generateClassTag(
indicator || ""
)}__tooltip-text`}
>
{getPerformanceImpactIndicatorTooltip(
indicatorValue,
isHostSpecific
)}
</span>
}
position="top"
disableTooltip={disableTooltip}
underline={false}
showArrow
// Pills require more gap from text to tooltip
tipOffset={indicatorValue === "Undetermined" ? 8 : 12}
>
<span className={pillClassName}>{indicatorValue}</span>
</span>
<ReactTooltip
place="top"
effect="solid"
backgroundColor={COLORS["tooltip-bg"]}
id={`${customIdPrefix || "pill"}__${id?.toString() || tooltipId}`}
data-html
>
<span
className={`tooltip ${generateClassTag(
indicatorValue || ""
)}__tooltip-text`}
>
{getPerformanceImpactIndicatorTooltip(indicatorValue, isHostSpecific)}
</span>
</ReactTooltip>
</TooltipWrapper>
</span>
);
};
@@ -1,8 +1,6 @@
import classnames from "classnames";
import { uniqueId } from "lodash";
import React from "react";
import ReactTooltip from "react-tooltip";
import { COLORS } from "styles/var/colors";
import TooltipWrapper from "components/TooltipWrapper";
import { DEFAULT_EMPTY_CELL_VALUE } from "utilities/constants";
const baseClass = "text-cell";
@@ -46,24 +44,17 @@ const TextCell = ({
[grey, italic] = [true, true];
}
// TODO: Refactor to use TooltipWrapper component
const renderEmptyCell = () => {
if (emptyCellTooltipText) {
const tooltipId = uniqueId();
return (
<>
<span data-tip data-for={tooltipId}>
{DEFAULT_EMPTY_CELL_VALUE}
</span>
<ReactTooltip
place="top"
effect="solid"
backgroundColor={COLORS["tooltip-bg"]}
id={tooltipId}
>
{emptyCellTooltipText}
</ReactTooltip>
</>
<TooltipWrapper
tipContent={emptyCellTooltipText}
position="top"
underline={false}
showArrow
>
<span>{DEFAULT_EMPTY_CELL_VALUE}</span>
</TooltipWrapper>
);
}
return DEFAULT_EMPTY_CELL_VALUE;
@@ -408,7 +408,11 @@ $shadow-transition-width: 10px;
}
.italic-cell {
font-style: italic;
.__react_component_tooltip {
// old react tooltip version
.__react_component_tooltip,
// new react tooltip version
.react-tooltip {
font-style: normal;
}
}
@@ -1,7 +1,6 @@
import React, { useState, useCallback, useRef, useEffect } from "react";
import classnames from "classnames";
import { Row } from "react-table";
import ReactTooltip from "react-tooltip";
import useDeepEffect from "hooks/useDeepEffect";
import { noop } from "lodash";
@@ -11,8 +10,6 @@ import Button from "components/buttons/Button";
import Icon from "components/Icon/Icon";
import TooltipWrapper from "components/TooltipWrapper";
import { COLORS } from "styles/var/colors";
import DataTable from "./DataTable/DataTable";
import { IActionButtonProps } from "./DataTable/ActionButton/ActionButton";
@@ -76,7 +73,7 @@ interface ITableContainerProps<T = any> {
primarySelectAction?: IActionButtonProps;
/** Secondary button/s after selecting a row */
secondarySelectActions?: IActionButtonProps[]; // TODO: Combine with primarySelectAction as these are all rendered in the same spot
searchToolTipText?: string;
searchToolTipText?: JSX.Element;
// TODO - consolidate this functionality within `filters`
searchQueryColumn?: string;
// TODO - consolidate this functionality within `filters`
@@ -384,28 +381,26 @@ const TableContainer = <T,>({
{customControl && customControl()}
{searchable && !wideSearch && (
<div className={`${baseClass}__search`}>
<div
className={`${baseClass}__search-input`}
data-tip
data-for="search-tooltip"
data-tip-disable={!searchToolTipText}
<TooltipWrapper
tipContent={
<span className={`tooltip ${baseClass}__tooltip-text`}>
{searchToolTipText}
</span>
}
disableTooltip={!searchToolTipText}
underline={false}
position="top"
tipOffset={8}
showArrow
>
<SearchField
placeholder={inputPlaceHolder}
defaultValue={searchQuery}
onChange={onSearchQueryChange}
/>
</div>
<ReactTooltip
effect="solid"
backgroundColor={COLORS["tooltip-bg"]}
id="search-tooltip"
data-html
>
<span className={`tooltip ${baseClass}__tooltip-text`}>
{searchToolTipText}
</span>
</ReactTooltip>
<div className={`${baseClass}__search-input`}>
<SearchField
placeholder={inputPlaceHolder}
defaultValue={searchQuery}
onChange={onSearchQueryChange}
/>
</div>
</TooltipWrapper>
</div>
)}
{customFiltersButton && customFiltersButton()}
@@ -457,30 +452,30 @@ const TableContainer = <T,>({
{/* Render search bar only if not empty component */}
{searchable && !wideSearch && (
<div className={`${baseClass}__search`}>
<div
className={`${baseClass}__search-input ${
stackControls ? "stack-table-controls" : ""
}`}
data-tip
data-for="search-tooltip"
data-tip-disable={!searchToolTipText}
<TooltipWrapper
tipContent={
<span className={`tooltip ${baseClass}__tooltip-text`}>
{searchToolTipText}
</span>
}
disableTooltip={!searchToolTipText}
underline={false}
position="top"
tipOffset={8}
showArrow
>
<SearchField
placeholder={inputPlaceHolder}
defaultValue={searchQuery}
onChange={onSearchQueryChange}
/>
</div>
<ReactTooltip
effect="solid"
backgroundColor={COLORS["tooltip-bg"]}
id="search-tooltip"
data-html
>
<span className={`tooltip ${baseClass}__tooltip-text`}>
{searchToolTipText}
</span>
</ReactTooltip>
<div
className={`${baseClass}__search-input ${
stackControls ? "stack-table-controls" : ""
}`}
>
<SearchField
placeholder={inputPlaceHolder}
defaultValue={searchQuery}
onChange={onSearchQueryChange}
/>
</div>
</TooltipWrapper>
</div>
)}
</div>
@@ -29,9 +29,9 @@
display: inline-flex;
align-items: center;
white-space: nowrap;
gap: $pad-xsmall;
.host-issue {
margin-left: $pad-xsmall;
display: inline-flex;
}
}
@@ -81,9 +81,9 @@
display: inline-flex;
align-items: center;
white-space: nowrap;
gap: $pad-xsmall;
.host-issue {
margin-left: $pad-xsmall;
display: inline-flex;
}
}
@@ -1,44 +0,0 @@
import React from "react";
import ReactTooltip from "react-tooltip";
import { uniqueId } from "lodash";
import Icon from "components/Icon";
import { COLORS } from "styles/var/colors";
import { IconNames } from "components/icons";
const baseClass = "icon-cell";
interface IIconCellProps {
iconName: IconNames;
}
const IconCell = ({ iconName }: IIconCellProps) => {
const tooltipID = uniqueId();
return (
<div className={baseClass}>
<span
className={`${baseClass}__icon tooltip tooltip__tooltip-icon`}
data-tip
data-for={tooltipID}
data-tip-disable={false}
>
<Icon name={iconName} />
</span>
<ReactTooltip
place="top"
effect="solid"
backgroundColor={COLORS["tooltip-bg"]}
id={tooltipID}
data-html
>
<span className={`tooltip__tooltip-text`}>
{/* TODO: enhance to be dynmaic */}
Software can be installed on Host details page.
</span>
</ReactTooltip>
</div>
);
};
export default IconCell;
@@ -1,3 +0,0 @@
.icon-cell {
text-align: center;
}
@@ -1 +0,0 @@
export { default } from "./IconCell";
@@ -1,8 +1,8 @@
import React from "react";
import { uniqueId } from "lodash";
import ReactTooltip from "react-tooltip";
import { PlacesType } from "react-tooltip-5";
import TextCell from "components/TableContainer/DataTable/TextCell";
import TooltipWrapper from "components/TooltipWrapper";
import { ISoftwareVulnerability } from "interfaces/software";
const NUM_VULNERABILITIES_IN_TOOLTIP = 3;
@@ -21,6 +21,7 @@ const generateCell = (
let text = "";
let italicize = true;
if (totalCount === 1) {
italicize = false;
text =
@@ -34,8 +35,8 @@ const generateCell = (
return <TextCell value={text} italic={italicize} />;
};
const getName = (vulnerabiltiy: ISoftwareVulnerability | string) => {
return typeof vulnerabiltiy === "string" ? vulnerabiltiy : vulnerabiltiy.cve;
const getName = (vulnerability: ISoftwareVulnerability | string) => {
return typeof vulnerability === "string" ? vulnerability : vulnerability.cve;
};
const condenseVulnerabilities = (
@@ -43,12 +44,12 @@ const condenseVulnerabilities = (
totalCount?: number
) => {
const condensed =
(vulnerabilities?.length &&
vulnerabilities
.slice(-NUM_VULNERABILITIES_IN_TOOLTIP)
.map(getName)
.reverse()) ||
[];
vulnerabilities.length > 0
? vulnerabilities
.slice(-NUM_VULNERABILITIES_IN_TOOLTIP)
.map(getName)
.reverse()
: [];
const count = totalCount ?? vulnerabilities.length;
@@ -59,10 +60,10 @@ const condenseVulnerabilities = (
const generateTooltip = (
vulnerabilities: ISoftwareVulnerability[] | string[],
tooltipId: string,
totalCount?: number
) => {
const count = totalCount ?? vulnerabilities.length;
if (count <= 1) {
return null;
}
@@ -73,57 +74,48 @@ const generateTooltip = (
);
return (
<ReactTooltip
effect="solid"
backgroundColor="#3e4771"
id={tooltipId}
data-html
>
<ul className={`${baseClass}__vulnerability-list`}>
{condensedVulnerabilities.map((vulnerability) => {
const key =
typeof vulnerability === "string" ? vulnerability : uniqueId();
return <li key={key}>{vulnerability}</li>;
})}
</ul>
</ReactTooltip>
<ul className={`${baseClass}__vulnerability-list`}>
{condensedVulnerabilities.map((vulnerability) => (
<li key={vulnerability}>{vulnerability}</li>
))}
</ul>
);
};
interface IVulnerabilitiesCellProps {
vulnerabilities: ISoftwareVulnerability[] | string[] | null;
vulnerabilitiesCount?: number;
tooltipPosition?: PlacesType;
}
const VulnerabilitiesCell = ({
vulnerabilities,
vulnerabilitiesCount,
tooltipPosition = "top",
}: IVulnerabilitiesCellProps) => {
const tooltipId = uniqueId();
// only one vulnerability, no need for tooltip
const cell = generateCell(vulnerabilities, vulnerabilitiesCount);
const count = vulnerabilitiesCount ?? vulnerabilities?.length ?? 0;
if (vulnerabilities === null || count <= 1) {
return <>{cell}</>;
}
const vulnerabilityTooltip = generateTooltip(
vulnerabilities,
tooltipId,
vulnerabilitiesCount
);
return (
<>
<div
className={`${baseClass}__vulnerability-text-with-tooltip`}
data-tip
data-for={tooltipId}
>
<TooltipWrapper
tipContent={vulnerabilityTooltip}
position={tooltipPosition}
underline={false}
showArrow
>
<div className={`${baseClass}__vulnerability-text-with-tooltip`}>
{cell}
</div>
{vulnerabilityTooltip}
</>
</TooltipWrapper>
);
};
@@ -1,6 +1,7 @@
.vulnerabilities-cell {
&__vulnerability-text-with-tooltip {
display: inline-block;
vertical-align: middle;
}
&__vulnerability-list {
@@ -1,5 +1,4 @@
import React, { FormEvent, useState, useEffect } from "react";
import ReactTooltip from "react-tooltip";
import {
IIntegrationFormData,
@@ -15,7 +14,7 @@ import InputField from "components/forms/fields/InputField";
import validUrl from "components/forms/validators/valid_url";
import Spinner from "components/Spinner";
import { COLORS } from "styles/var/colors";
import TooltipWrapper from "components/TooltipWrapper";
import GitOpsModeTooltipWrapper from "components/GitOpsModeTooltipWrapper";
import { IInputFieldParseTarget } from "interfaces/form_field";
@@ -281,35 +280,27 @@ const IntegrationForm = ({
formData.email === "" ||
formData.apiToken === "" ||
formData.groupId === 0;
// TODO - refactor below to use TooltipWrapper
return (
<>
<div
data-tip
data-for="add-integration-button"
data-tip-disable={!formInvalid || disableChildren}
className="tooltip"
>
<Button
type="submit"
disabled={formInvalid || disableChildren}
>
Add
</Button>
</div>
<ReactTooltip
className="add-integration-tooltip"
place="bottom"
effect="solid"
backgroundColor={COLORS["tooltip-bg"]}
id="add-integration-button"
data-html
>
<TooltipWrapper
tipContent={
<>
Complete all fields to save <br /> the integration.
</>
</ReactTooltip>
</>
}
tooltipClass="add-integration-tooltip"
position="top"
disableTooltip={!formInvalid || disableChildren}
underline={false}
showArrow
tipOffset={8}
>
<Button
type="submit"
disabled={formInvalid || disableChildren}
>
Add
</Button>
</TooltipWrapper>
);
}}
/>
@@ -5,8 +5,7 @@ import { ITokenTeam } from "interfaces/mdm";
import { getTeamDisplayName } from "interfaces/team";
import TextCell from "components/TableContainer/DataTable/TextCell";
import { uniqueId } from "lodash";
import ReactTooltip from "react-tooltip";
import TooltipWrapper from "components/TooltipWrapper";
const baseClass = "teams-cell";
@@ -47,38 +46,11 @@ const condenseTeams = (teams: ITokenTeam[]) => {
: condensed;
};
const generateTooltip = (teams: ITokenTeam[] | null, tooltipId: string) => {
if (teams === null || teams.length <= 1) {
return null;
}
const condensedTeams = condenseTeams(teams);
return (
<ReactTooltip
effect="solid"
backgroundColor="#3e4771"
id={tooltipId}
data-html
>
<ul className={`${baseClass}__team-list`}>
{condensedTeams.map((teamName) => {
return <li key={teamName}>{teamName}</li>;
})}
</ul>
</ReactTooltip>
);
};
interface ITeamsCellProps {
teams: ITokenTeam[] | null;
className?: string;
}
const TeamsCell = ({ teams, className }: ITeamsCellProps) => {
const tooltipId = uniqueId();
const classNames = classnames(baseClass, className);
const TeamsCell = ({ teams }: ITeamsCellProps) => {
if (!teams) {
return <TextCell value={teams} />;
}
@@ -92,19 +64,25 @@ const TeamsCell = ({ teams, className }: ITeamsCellProps) => {
}
const cell = generateCell(teams);
const tooltip = generateTooltip(teams, tooltipId);
const condensedTeams = condenseTeams(teams);
return (
<>
<div
className={`${baseClass}__team-text-with-tooltip`}
data-tip
data-for={tooltipId}
>
{cell}
</div>
{tooltip}
</>
<TooltipWrapper
tipContent={
<ul className={`${baseClass}__team-list`}>
{condensedTeams.map((teamName) => {
return <li key={teamName}>{teamName}</li>;
})}
</ul>
}
underline={false}
showArrow
position="top"
className={`${baseClass}__team-text-with-tooltip`}
tipOffset={8}
>
{cell}
</TooltipWrapper>
);
};
@@ -111,7 +111,7 @@ export const generateTableConfig = (
Header: "Fleets",
disableSortBy: true,
Cell: (cellProps: ITeamsCellProps) => (
<TeamsCell teams={cellProps.cell.value} className="vpp-teams-cell" />
<TeamsCell teams={cellProps.cell.value} />
),
},
{
@@ -3,7 +3,6 @@
// definitions for the selection row for some reason when we dont really need it.
import React from "react";
import { CellProps, Column } from "react-table";
import ReactTooltip from "react-tooltip";
import { IDeviceUser, IHost } from "interfaces/host";
import {
@@ -34,7 +33,6 @@ import {
hostTeamName,
tooltipTextWithLineBreaks,
} from "utilities/helpers";
import { COLORS } from "styles/var/colors";
import {
IHeaderProps,
IStringCellProps,
@@ -1,6 +1,6 @@
import React from "react";
import { screen } from "@testing-library/react";
import { screen, waitFor } from "@testing-library/react";
import { renderWithSetup } from "test/test-utils";
import IssuesIndicator from "./IssuesIndicator";
@@ -16,12 +16,14 @@ describe("Issues indicator", () => {
);
await user.hover(screen.getByText("5"));
const vulnerabilitiesTooltip = screen.getByText(
/Critical vulnerabilities/i
);
const policiesTooltip = screen.getByText(/Failing policies/i);
await waitFor(() => {
const vulnerabilitiesTooltip = screen.getByText(
/Critical vulnerabilities/i
);
const policiesTooltip = screen.getByText(/Failing policies/i);
expect(vulnerabilitiesTooltip).toBeInTheDocument();
expect(policiesTooltip).toBeInTheDocument();
expect(vulnerabilitiesTooltip).toBeInTheDocument();
expect(policiesTooltip).toBeInTheDocument();
});
});
});
@@ -1,7 +1,8 @@
import React from "react";
import { PlacesType } from "react-tooltip-5";
import ReactTooltip from "react-tooltip";
import { COLORS } from "styles/var/colors";
import classnames from "classnames";
import TooltipWrapper from "components/TooltipWrapper";
import Icon from "components/Icon";
interface IIssuesIndicatorProps {
@@ -9,45 +10,43 @@ interface IIssuesIndicatorProps {
failingPoliciesCount?: number;
/** Premium only */
criticalVulnerabilitiesCount?: number;
rowId?: number;
tooltipPosition?: "top" | "bottom";
tooltipPosition?: PlacesType;
rowId?: string | number;
}
const baseClass = "issues-indicator";
const IssuesIndicator = ({
totalIssuesCount,
failingPoliciesCount,
criticalVulnerabilitiesCount,
rowId,
tooltipPosition = "top",
rowId,
}: IIssuesIndicatorProps): JSX.Element => {
return (
const classNames = classnames(baseClass, {
[`${baseClass}--${rowId}`]: !!rowId,
});
const tipContent = (
<>
<span
className="host-issue tooltip tooltip__tooltip-icon"
data-tip
data-for={`host-issue-count-${rowId}`}
data-tip-disable={false}
>
<Icon name="error-outline" color="ui-fleet-black-50" />{" "}
{totalIssuesCount}
</span>
<ReactTooltip
place={tooltipPosition}
effect="solid"
backgroundColor={COLORS["tooltip-bg"]}
id={`host-issue-count-${rowId}`}
data-html
>
<span className={`tooltip__tooltip-text`}>
{!!criticalVulnerabilitiesCount &&
`Critical vulnerabilities (${criticalVulnerabilitiesCount})`}
{!!criticalVulnerabilitiesCount && !!failingPoliciesCount && <br />}
{!!failingPoliciesCount &&
`Failing policies (${failingPoliciesCount})`}
</span>
</ReactTooltip>
{!!criticalVulnerabilitiesCount &&
`Critical vulnerabilities (${criticalVulnerabilitiesCount})`}
{!!criticalVulnerabilitiesCount && !!failingPoliciesCount && <br />}
{!!failingPoliciesCount && `Failing policies (${failingPoliciesCount})`}
</>
);
return (
<TooltipWrapper
tipContent={tipContent}
position={tooltipPosition}
underline={false}
showArrow
className={classNames}
tipOffset={8}
>
<Icon name="error-outline" color="ui-fleet-black-50" /> {totalIssuesCount}
</TooltipWrapper>
);
};
export default IssuesIndicator;
@@ -0,0 +1,7 @@
.issues-indicator {
.component__tooltip-wrapper__element {
display: inline-flex;
align-items: center;
gap: $pad-small;
}
}
@@ -1218,21 +1218,21 @@ const HostDetailsPage = ({
{isBYODAccountDrivenUserEnrollment(host.mdm.enrollment_status) ||
isAndroidHost ? (
<EmptyTable
header="Software library is currently not supported on this host."
info={
<>
Software install is coming soon.{" "}
<CustomLink
newTab
text="Learn more"
url={
isAndroidHost
? ANDROID_SW_INSTALL_LEARN_MORE_LINK
: BYOD_SW_INSTALL_LEARN_MORE_LINK
}
newTab
/>
</>
}
header="Software library is currently not supported on this host."
/>
) : (
<SoftwareLibraryCard
@@ -1,5 +1,5 @@
import React from "react";
import { render, screen } from "@testing-library/react";
import { render, screen, waitFor } from "@testing-library/react";
import { createCustomRenderer } from "test/test-utils";
import {
FLEET_ANDROID_CERTIFICATE_TEMPLATE_PROFILE_ID,
@@ -40,8 +40,9 @@ describe("OS setting status cell", () => {
const statusText = screen.getByText("Verifying");
await user.hover(statusText);
expect(screen.getByText(/verifying/)).toBeInTheDocument();
await waitFor(() => {
expect(screen.getByText(/verifying/)).toBeInTheDocument();
});
});
// Android cert statuses
@@ -62,9 +63,11 @@ describe("OS setting status cell", () => {
expect(statusText).toBeInTheDocument();
await user.hover(statusText);
expect(
screen.getByText(/The host is running the command/)
).toBeInTheDocument();
await waitFor(() => {
expect(
screen.getByText(/The host is running the command/)
).toBeInTheDocument();
});
});
it("Displays Pending UI for 'delivering' status with optype 'install'", async () => {
const customRender = createCustomRenderer();
@@ -83,9 +86,11 @@ describe("OS setting status cell", () => {
expect(statusText).toBeInTheDocument();
await user.hover(statusText);
expect(
screen.getByText(/The host is running the command/)
).toBeInTheDocument();
await waitFor(() => {
expect(
screen.getByText(/The host is running the command/)
).toBeInTheDocument();
});
});
it("Displays Pending UI for 'delivered' status with optype 'install'", async () => {
const customRender = createCustomRenderer();
@@ -104,9 +109,11 @@ describe("OS setting status cell", () => {
expect(statusText).toBeInTheDocument();
await user.hover(statusText);
expect(
screen.getByText(/The host is running the command/)
).toBeInTheDocument();
await waitFor(() => {
expect(
screen.getByText(/The host is running the command/)
).toBeInTheDocument();
});
});
it("Displays Pending UI for 'delivering' status with optype 'remove'", async () => {
const customRender = createCustomRenderer();
@@ -125,9 +132,11 @@ describe("OS setting status cell", () => {
expect(statusText).toBeInTheDocument();
await user.hover(statusText);
expect(
screen.getByText(/The host is running the command/)
).toBeInTheDocument();
await waitFor(() => {
expect(
screen.getByText(/The host is running the command/)
).toBeInTheDocument();
});
});
it("Displays Pending UI for 'delivered' status with optype 'remove'", async () => {
const customRender = createCustomRenderer();
@@ -146,8 +155,10 @@ describe("OS setting status cell", () => {
expect(statusText).toBeInTheDocument();
await user.hover(statusText);
expect(
screen.getByText(/The host is running the command/)
).toBeInTheDocument();
await waitFor(() => {
expect(
screen.getByText(/The host is running the command/)
).toBeInTheDocument();
});
});
});
@@ -1,6 +1,4 @@
import React from "react";
import ReactTooltip from "react-tooltip";
import { uniqueId } from "lodash";
import { REC_LOCK_SYNTHETIC_PROFILE_UUID } from "pages/hosts/details/helpers";
@@ -13,7 +11,7 @@ import {
ProfilePlatform,
RecoveryLockPasswordStatus,
} from "interfaces/mdm";
import { COLORS } from "styles/var/colors";
import TooltipWrapper from "components/TooltipWrapper";
import { OsSettingsTableStatusValue } from "../OSSettingsTableConfig";
import TooltipContent from "./components/Tooltip/TooltipContent";
@@ -122,27 +120,12 @@ const OSSettingStatusCell = ({
if (displayOption) {
const { statusText, iconName, tooltip } = displayOption;
const tooltipId = uniqueId();
return (
<span className={baseClass}>
<Icon name={iconName} />
{tooltip ? (
<>
<span
className={`${baseClass}__status-text`}
data-tip
data-for={tooltipId}
data-tip-disable={false}
>
{statusText}
</span>
<ReactTooltip
place="top"
effect="solid"
backgroundColor={COLORS["tooltip-bg"]}
id={tooltipId}
data-html
>
<TooltipWrapper
tipContent={
<span className="tooltip__tooltip-text">
{status !== "action_required" ? (
<TooltipContent
@@ -160,8 +143,14 @@ const OSSettingStatusCell = ({
/>
)}
</span>
</ReactTooltip>
</>
}
position="top"
underline={false}
showArrow
tipOffset={8}
>
<span className={`${baseClass}__status-text`}>{statusText}</span>
</TooltipWrapper>
) : (
<span className={`${baseClass}__status-text`}>{statusText}</span>
)}
@@ -1,68 +0,0 @@
import React from "react";
import { fireEvent, render, screen } from "@testing-library/react";
import ProfileStatusIndicator from "./ProfileStatusIndicator";
describe("ProfileStatusIndicator component", () => {
it("Renders the text and icon", () => {
const indicatorText = "test text";
render(
<ProfileStatusIndicator
indicatorText={indicatorText}
iconName="success"
/>
);
const renderedIndicatorText = screen.getByText(indicatorText);
const renderedIcon = screen.getByTestId("success-icon");
expect(renderedIndicatorText).toBeInTheDocument();
expect(renderedIcon).toBeInTheDocument();
});
it("Renders text, icon, and tooltip", () => {
const indicatorText = "test text";
const tooltipText = "test tooltip text";
render(
<ProfileStatusIndicator
indicatorText={indicatorText}
iconName="success"
tooltip={{ tooltipText }}
/>
);
const renderedIndicatorText = screen.getByText(indicatorText);
const renderedIcon = screen.getByTestId("success-icon");
const renderedTooltipText = screen.getByText(tooltipText);
expect(renderedIndicatorText).toBeInTheDocument();
expect(renderedIcon).toBeInTheDocument();
expect(renderedTooltipText).toBeInTheDocument();
});
it("Renders text, icon, and onClick", () => {
const indicatorText = "test text";
const onClick = () => {
const newDiv = document.createElement("div");
newDiv.appendChild(document.createTextNode("onClick called"));
document.body.appendChild(newDiv);
};
render(
<ProfileStatusIndicator
indicatorText={indicatorText}
iconName="success"
onClick={() => {
onClick();
}}
/>
);
const renderedIndicatorText = screen.getByText(indicatorText);
const renderedIcon = screen.getByTestId("success-icon");
const renderedButton = screen.getByRole("button");
expect(renderedIndicatorText).toBeInTheDocument();
expect(renderedIcon).toBeInTheDocument();
expect(renderedButton).toBeInTheDocument();
fireEvent.click(renderedButton);
expect(screen.getByText("onClick called")).toBeInTheDocument();
});
});
@@ -1,107 +0,0 @@
import React from "react";
import ReactTooltip from "react-tooltip";
import { IconNames } from "components/icons";
import Icon from "components/Icon";
import Button from "components/buttons/Button";
import { COLORS } from "styles/var/colors";
const baseClass = "profile-status-indicator";
export interface IProfileStatusIndicatorProps {
indicatorText: string;
iconName: IconNames;
onClick?: () => void;
tooltip?: {
tooltipText: string | null;
position?: "top" | "bottom";
};
}
const ProfileStatusIndicator = ({
indicatorText,
iconName,
onClick,
tooltip,
}: IProfileStatusIndicatorProps) => {
const getIndicatorTextWrapped = () => {
if (onClick && tooltip?.tooltipText) {
return (
<>
<span
className="tooltip tooltip__tooltip-icon"
data-tip
data-for={`${indicatorText}-tooltip`}
data-tip-disable={false}
>
<Button
onClick={onClick}
variant="text-link"
className={`${baseClass}__button`}
>
{indicatorText}
</Button>
</span>
<ReactTooltip
place={tooltip.position ?? "bottom"}
effect="solid"
backgroundColor={COLORS["tooltip-bg"]}
id={`${indicatorText}-tooltip`}
data-html
>
<span className="tooltip__tooltip-text">{tooltip.tooltipText}</span>
</ReactTooltip>
</>
);
}
// onclick without tooltip
if (onClick) {
return (
<Button
onClick={onClick}
variant="text-link"
className={`${baseClass}__button`}
>
{indicatorText}
</Button>
);
}
// tooltip without onclick
if (tooltip?.tooltipText) {
return (
<>
<span
className="tooltip tooltip__tooltip-icon"
data-tip
data-for={`${indicatorText}-tooltip`}
data-tip-disable={false}
>
{indicatorText}
</span>
<ReactTooltip
place={tooltip.position ?? "bottom"}
effect="solid"
backgroundColor={COLORS["tooltip-bg"]}
id={`${indicatorText}-tooltip`}
data-html
>
<span className="tooltip__tooltip-text">{tooltip.tooltipText}</span>
</ReactTooltip>
</>
);
}
// no tooltip, no onclick
return indicatorText;
};
return (
<span className={`${baseClass} info-flex__data`}>
<Icon name={iconName} />
{getIndicatorTextWrapped()}
</span>
);
};
export default ProfileStatusIndicator;
@@ -1,18 +0,0 @@
.profile-status-indicator {
display: flex;
gap: 4px;
&__button {
font-weight: normal;
}
.icon {
width: 16px;
height: 16px;
align-self: center;
}
.__react_component_tooltip {
white-space: normal;
}
}
@@ -1 +0,0 @@
export { default } from "./ProfileStatusIndicator";
@@ -1,14 +1,15 @@
import React from "react";
import { render, screen } from "@testing-library/react";
import { render, screen, waitFor } from "@testing-library/react";
import { renderWithSetup } from "test/test-utils";
import ReportUpdatedCell from "./ReportUpdatedCell";
const HUMAN_READABLE_DATETIME_REGEX = /\d{1,2}\/\d{1,2}\/\d\d\d\d, \d{1,2}:\d{1,2}:\d{1,2}\s(A|P)M/;
describe("ReportUpdatedCell component", () => {
it("Renders 'No report' with tooltip and no link when run on an interval with discard data and automations enabled", () => {
render(
it("Renders 'No report' with tooltip and no link when run on an interval with discard data and automations enabled", async () => {
const { user } = renderWithSetup(
<ReportUpdatedCell
interval={1000}
discard_data
@@ -18,14 +19,18 @@ describe("ReportUpdatedCell component", () => {
hostId={4}
/>
);
const noReportText = screen.getByText(/No report/);
expect(noReportText).toBeInTheDocument();
await user.hover(noReportText);
expect(screen.getByText(/No report/)).toBeInTheDocument();
expect(screen.getByText(/Results from this report/)).toBeInTheDocument();
waitFor(() => {
expect(screen.getByText(/Results from this report/)).toBeInTheDocument();
});
expect(screen.queryByText(/View report/)).toBeNull();
});
it("Renders '---' with tooltip and link to report when run on an interval with discard data off and no last_fetched time", () => {
render(
it("Renders '---' with tooltip and link to report when run on an interval with discard data off and no last_fetched time", async () => {
const { user } = renderWithSetup(
<ReportUpdatedCell
interval={1000}
discard_data={false}
@@ -36,17 +41,22 @@ describe("ReportUpdatedCell component", () => {
/>
);
expect(screen.getByText(/---/)).toBeInTheDocument();
expect(
screen.getByText(/Fleet is collecting report results\./)
).toBeInTheDocument();
expect(screen.getByText(/Check back later./)).toBeInTheDocument();
const noReportText = screen.getByText(/---/);
expect(noReportText).toBeInTheDocument();
await user.hover(noReportText);
waitFor(() => {
expect(
screen.getByText(/Fleet is collecting report results\./)
).toBeInTheDocument();
expect(screen.getByText(/Check back later./)).toBeInTheDocument();
});
});
it("Renders a last-updated timestamp with tooltip and link to report when a last_fetched date is present", () => {
it("Renders a last-updated timestamp with tooltip and link to report when a last_fetched date is present", async () => {
const tenDaysAgo = new Date();
tenDaysAgo.setDate(tenDaysAgo.getDate() - 10);
render(
const { user } = renderWithSetup(
<ReportUpdatedCell
interval={1000}
discard_data={false}
@@ -57,15 +67,22 @@ describe("ReportUpdatedCell component", () => {
hostId={4}
/>
);
const timeAgo = screen.getByText(/\d+.+ago/);
expect(screen.getByText(HUMAN_READABLE_DATETIME_REGEX)).toBeInTheDocument();
expect(screen.getByText(/\d+.+ago/)).toBeInTheDocument();
expect(timeAgo).toBeInTheDocument();
await user.hover(timeAgo);
await waitFor(() => {
expect(
screen.getByText(HUMAN_READABLE_DATETIME_REGEX)
).toBeInTheDocument();
});
expect(screen.getByText(/View data/)).toBeInTheDocument();
});
it("Renders a last-updated timestamp with tooltip and link to report when a last_fetched date is present but not currently running an interval", () => {
it("Renders a last-updated timestamp with tooltip and link to report when a last_fetched date is present but not currently running an interval", async () => {
const tenDaysAgo = new Date();
tenDaysAgo.setDate(tenDaysAgo.getDate() - 10);
render(
const { user } = renderWithSetup(
<ReportUpdatedCell
interval={0}
discard_data={false}
@@ -76,9 +93,15 @@ describe("ReportUpdatedCell component", () => {
hostId={4}
/>
);
const timeAgo = screen.getByText(/\d+.+ago/);
expect(timeAgo).toBeInTheDocument();
await user.hover(timeAgo);
expect(screen.getByText(HUMAN_READABLE_DATETIME_REGEX)).toBeInTheDocument();
expect(screen.getByText(/\d+.+ago/)).toBeInTheDocument();
await waitFor(() => {
expect(
screen.getByText(HUMAN_READABLE_DATETIME_REGEX)
).toBeInTheDocument();
});
expect(screen.getByText(/View data/)).toBeInTheDocument();
});
});
@@ -2,10 +2,8 @@ import React from "react";
import { browserHistory } from "react-router";
import { HumanTimeDiffWithFleetLaunchCutoff } from "components/HumanTimeDiffWithDateTip";
import { uniqueId } from "lodash";
import ReactTooltip from "react-tooltip";
import { COLORS } from "styles/var/colors";
import Icon from "components/Icon";
import TooltipWrapper from "components/TooltipWrapper";
import TextCell from "components/TableContainer/DataTable/TextCell";
import Button from "components/buttons/Button";
import { DEFAULT_EMPTY_CELL_VALUE } from "utilities/constants";
@@ -42,30 +40,22 @@ const ReportUpdatedCell = ({
return (
<TextCell
className={`${baseClass}__value no-report`}
formatter={(val) => {
const tooltipId = uniqueId();
return (
<>
<span data-tip data-for={tooltipId}>
{val}
</span>
<ReactTooltip
place="top"
effect="solid"
backgroundColor={COLORS["tooltip-bg"]}
id={tooltipId}
>
{
<>
Results from this report are not reported in Fleet.
<br />
Data is being sent to your log destination.
</>
}
</ReactTooltip>
</>
);
}}
formatter={(val) => (
<TooltipWrapper
tipContent={
<>
Results from this report are not reported in Fleet.
<br />
Data is being sent to your log destination.
</>
}
position="top"
underline={false}
showArrow
>
<span>{val}</span>
</TooltipWrapper>
)}
value="No report"
/>
);
@@ -73,26 +63,24 @@ const ReportUpdatedCell = ({
// Query is scheduled to run on host, but hasn't yet
if (!last_fetched) {
const tipId = uniqueId();
return (
<TextCell
value={DEFAULT_EMPTY_CELL_VALUE}
formatter={(val) => (
<>
<span data-tip data-for={tipId}>
{val}
</span>
<ReactTooltip
id={tipId}
effect="solid"
backgroundColor={COLORS["tooltip-bg"]}
place="top"
>
Fleet is collecting report results.
<br />
Check back later.
</ReactTooltip>
</>
<TooltipWrapper
tipContent={
<>
Fleet is collecting report results.
<br />
Check back later.
</>
}
position="top"
underline={false}
showArrow
>
<span>{val}</span>
</TooltipWrapper>
)}
grey
italic
@@ -1,5 +1,5 @@
import React from "react";
import { screen } from "@testing-library/react";
import { screen, waitFor } from "@testing-library/react";
import { createCustomRenderer } from "test/test-utils";
import { noop } from "lodash";
@@ -86,7 +86,7 @@ describe("Self-service - UpdateSoftwareItem component", () => {
ui_status: "failed_install_update_available",
};
render(
const { user } = render(
<UpdateSoftwareItem
software={software}
onClickUpdateAction={noop}
@@ -97,7 +97,13 @@ describe("Self-service - UpdateSoftwareItem component", () => {
const failedButton = screen.getByRole("button", { name: "Failed" });
expect(failedButton).toBeInTheDocument();
expect(screen.getByText(/Software failed to install/)).toBeInTheDocument();
await user.hover(failedButton);
await waitFor(() => {
expect(
screen.getByText(/Software failed to install/)
).toBeInTheDocument();
});
});
it("renders 'Updated' state when ui_status is 'recently_updated'", () => {
@@ -1,5 +1,4 @@
import React, { useEffect, useRef } from "react";
import ReactTooltip from "react-tooltip";
import {
IAppLastInstall,
@@ -90,13 +89,12 @@ const InstallerInfo = ({ software }: IInstallerInfoProps) => {
);
};
type IInstallerStatusProps = Pick<IHostSoftware, "id" | "status"> & {
type IInstallerStatusProps = Pick<IHostSoftware, "status"> & {
last_install: ISoftwareLastInstall | IAppLastInstall | null;
onShowInstallerDetails: (uuid?: InstallOrCommandUuid) => void;
};
const InstallerStatus = ({
id,
status,
last_install,
onShowInstallerDetails,
@@ -109,43 +107,36 @@ const InstallerStatus = ({
return (
<div className={`${baseClass}__status-content`}>
<div
className={`${baseClass}__status-with-tooltip`}
data-tip
data-for={`install-tooltip__${id}`}
<TooltipWrapper
tipContent={displayConfig.tooltip({
lastInstalledAt: last_install?.installed_at,
})}
underline={false}
showArrow
position="top"
tipOffset={8}
>
{displayConfig.iconName === "pending-outline" ? (
<Spinner size="x-small" includeContainer={false} centered={false} />
) : (
<Icon name={displayConfig.iconName || "install"} />
)}
{last_install && displayConfig.displayText === "Failed" && (
<span data-testid={`${baseClass}__status--test`}>
<Button
className={`${baseClass}__item-status-button`}
variant="inverse"
onClick={() => {
onShowInstallerDetails();
}}
>
{displayConfig.displayText}
</Button>
</span>
)}
</div>
<ReactTooltip
className={`${baseClass}__status-tooltip`}
effect="solid"
backgroundColor="#3e4771"
id={`install-tooltip__${id}`}
data-html
>
<span className={`${baseClass}__status-tooltip-text`}>
{displayConfig.tooltip({
lastInstalledAt: last_install?.installed_at,
})}
</span>
</ReactTooltip>
<div className={`${baseClass}__status-with-tooltip`}>
{displayConfig.iconName === "pending-outline" && (
<Spinner size="x-small" includeContainer={false} centered={false} />
)}
{last_install && displayConfig.displayText === "Failed" && (
<span data-testid={`${baseClass}__status--test`}>
<Button
className={`${baseClass}__item-status-button`}
variant="inverse"
onClick={() => {
onShowInstallerDetails();
}}
size="small"
>
<Icon name={displayConfig.iconName || "install"} />
{displayConfig.displayText}
</Button>
</span>
)}
</div>
</TooltipWrapper>
</div>
);
};
@@ -220,7 +211,6 @@ const InstallerStatusAction = ({
{showFailedInstallStatus && (
<div className={`${baseClass}__item-status`}>
<InstallerStatus
id={id}
status={status}
last_install={lastInstall}
onShowInstallerDetails={onShowInstallerDetails}
@@ -188,7 +188,7 @@ describe("PolicyForm - component", () => {
},
});
const { container, user } = render(
const { user } = render(
<PolicyForm
policyIdForEdit={mockPolicy.id}
showOpenSchemaActionText={false}
@@ -214,15 +214,12 @@ describe("PolicyForm - component", () => {
expect(screen.getByRole("button", { name: "Save" })).toBeDisabled();
expect(screen.getByRole("button", { name: "Run" })).toBeDisabled();
await user.hover(screen.getByRole("button", { name: "Save" }));
await waitFor(() => {
waitFor(() => {
user.hover(screen.getByRole("button", { name: "Save" }));
});
expect(
container.querySelector("#save-policy-button")
).toHaveTextContent(/to save or run the policy/i);
screen.getByText(/to save or run the policy/i)
).toBeInTheDocument();
});
});
@@ -4,11 +4,9 @@ import React, { useState, useContext, useEffect, KeyboardEvent } from "react";
import { useQuery, useQueryClient } from "react-query";
import { Ace } from "ace-builds";
import ReactTooltip from "react-tooltip";
import { useDebouncedCallback } from "use-debounce";
import { size } from "lodash";
import classnames from "classnames";
import { COLORS } from "styles/var/colors";
import { addGravatarUrlToResource } from "utilities/helpers";
import { AppContext } from "context/app";
@@ -884,16 +882,22 @@ const PolicyForm = ({
{hasSavePermissions && (
<GitOpsModeTooltipWrapper
renderChildren={(disableChildren) => (
// TODO - update to use TooltipWrapper
<>
<span
className={`${baseClass}__button-wrap--tooltip`}
data-tip
data-for="save-policy-button"
data-tip-disable={
!isExistingPolicy || isAnyPlatformSelected
}
>
<TooltipWrapper
tipContent={
<>
Select the platforms this
<br />
policy will be checked on
<br />
to save or run the policy.
</>
}
tooltipClass={`${baseClass}__button-wrap--tooltip`}
position="top"
disableTooltip={!isExistingPolicy || isAnyPlatformSelected}
underline={false}
>
<span className={`${baseClass}__button-wrap--tooltip`}>
<Button
onClick={promptSavePolicy()}
disabled={disableSaveFormErrors || disableChildren}
@@ -903,63 +907,47 @@ const PolicyForm = ({
Save
</Button>
</span>
<ReactTooltip
className={`${baseClass}__button-wrap--tooltip`}
place="bottom"
effect="solid"
id="save-policy-button"
backgroundColor={COLORS["tooltip-bg"]}
>
Select the platforms this
<br />
policy will be checked on
<br />
to save or run the policy.
</ReactTooltip>
</>
</TooltipWrapper>
)}
/>
)}
{/* TODO - update to use TooltipWrapper */}
<span
className={`${baseClass}__button-wrap--tooltip`}
data-tip
data-for="run-policy-button"
data-tip-disable={
<TooltipWrapper
tipContent={
disabledLiveQuery ? (
<>
Live reports are disabled <br />
in organization settings.
</>
) : (
<>
Select the platforms this <br />
policy will be checked on <br />
to save or run the policy.
</>
)
}
disableTooltip={
(!isExistingPolicy || isAnyPlatformSelected) &&
!disabledLiveQuery
}
underline={false}
showArrow
position="top"
>
<Button
onClick={goToSelectTargets}
disabled={
isAddingAutomation ||
(isExistingPolicy && !isAnyPlatformSelected) ||
disabledLiveQuery
}
variant="inverse"
>
Run <Icon name="run" />
</Button>
</span>
<ReactTooltip
className={`${baseClass}__button-wrap--tooltip`}
place="bottom"
effect="solid"
id="run-policy-button"
backgroundColor={COLORS["tooltip-bg"]}
data-html
>
{disabledLiveQuery ? (
<>Live reports are disabled in organization settings</>
) : (
<>
Select the platforms this <br />
policy will be checked on <br />
to save or run the policy.
</>
)}
</ReactTooltip>
<span className={`${baseClass}__button-wrap--tooltip`}>
<Button
onClick={goToSelectTargets}
disabled={
isAddingAutomation ||
(isExistingPolicy && !isAnyPlatformSelected) ||
disabledLiveQuery
}
variant="inverse"
>
Run <Icon name="run" />
</Button>
</span>
</TooltipWrapper>
</div>
</form>
{isSaveNewPolicyModalOpen && (
@@ -131,7 +131,7 @@
}
&__policy-name {
font-size:$large;
font-size: $large;
&.input-field--error {
border: 1px solid $core-vibrant-red;
@@ -142,21 +142,9 @@
display: flex;
justify-content: space-between;
align-items: center;
.autofill-tooltip-wrapper {
display: flex; // Required for vertical centering
}
.autofill-button-tooltip {
font-weight: $regular;
}
}
&__button-wrap {
&--tooltip {
display: flex;
}
.policy-form__run {
min-width: 64px;
}
@@ -20,8 +20,6 @@ import Button from "components/buttons/Button";
import Modal from "components/Modal";
import TargetLabelSelector from "components/TargetLabelSelector";
import Icon from "components/Icon";
import ReactTooltip from "react-tooltip";
import { COLORS } from "styles/var/colors";
export interface ISaveNewPolicyModalProps {
baseClass: string;
@@ -171,14 +169,23 @@ const SaveNewPolicyModal = ({
(labelName === "Resolution" && isFetchingAutofillResolution);
return (
<>
<div
data-tip
data-for={`autofill-button-${labelName}`}
// Tooltip shows except when fetching AI autofill
data-tip-disable={disableForm}
className="autofill-tooltip-wrapper"
>
<TooltipWrapper
tipContent={
aiFeaturesDisabled ? (
"AI features are disabled in organization settings"
) : (
<>
Policy queries (SQL) will be sent to a <br />
large language model (LLM). Fleet <br />
doesn&apos;t use this data to train models.
</>
)
}
position="top"
disableTooltip={disableForm}
underline={false}
>
<div className="autofill-tooltip-wrapper">
<Button
variant="inverse"
disabled={aiFeaturesDisabled || disableForm}
@@ -198,25 +205,7 @@ const SaveNewPolicyModal = ({
)}
</Button>
</div>
<ReactTooltip
className="autofill-button-tooltip"
place="top"
effect="solid"
backgroundColor={COLORS["tooltip-bg"]}
id={`autofill-button-${labelName}`}
data-html
>
{aiFeaturesDisabled ? (
"AI features are disabled in organization settings"
) : (
<>
Policy queries (SQL) will be sent to a <br />
large language model (LLM). Fleet <br />
doesn&apos;t use this data to train models.
</>
)}
</ReactTooltip>
</>
</TooltipWrapper>
);
},
[isFetchingAutofillDescription, isFetchingAutofillResolution, disableForm]
@@ -325,35 +314,35 @@ const SaveNewPolicyModal = ({
</div>
)}
<div className="modal-cta-wrap">
<span
className={`${baseClass}__button-wrap--modal-save`}
data-tip
data-for={`${baseClass}__button--modal-save-tooltip`}
data-tip-disable={!disableSave}
<TooltipWrapper
tipContent={
<>
Select the platforms this
<br />
policy will be checked on
<br />
to save the policy.
</>
}
tooltipClass={`${baseClass}__button--modal-save-tooltip`}
position="top"
disableTooltip={!disableSave}
underline={false}
showArrow
tipOffset={8}
>
<Button
type="submit"
onClick={handleSavePolicy}
disabled={disableSave}
className="save-policy-loading"
isLoading={isUpdatingPolicy}
>
Save
</Button>
<ReactTooltip
className={`${baseClass}__button--modal-save-tooltip`}
place="bottom"
effect="solid"
id={`${baseClass}__button--modal-save-tooltip`}
backgroundColor={COLORS["tooltip-bg"]}
>
Select the platforms this
<br />
policy will be checked on
<br />
to save the policy.
</ReactTooltip>
</span>
<span className={`${baseClass}__button-wrap--modal-save`}>
<Button
type="submit"
onClick={handleSavePolicy}
disabled={disableSave}
className="save-policy-loading"
isLoading={isUpdatingPolicy}
>
Save
</Button>
</span>
</TooltipWrapper>
<Button
className={`${baseClass}__button--modal-cancel`}
onClick={() => setIsSaveNewPolicyModalOpen(false)}
@@ -242,7 +242,6 @@ const generateColumnConfigs = ({
<PerformanceImpactCell
value={{
indicator: cellProps.cell.value,
id: cellProps.row.original.id,
}}
/>
),
@@ -2,8 +2,6 @@ import React, { useContext, useState, useEffect } from "react";
import { useQuery } from "react-query";
import { InjectedRouter, Params } from "react-router/lib/Router";
import { useErrorHandler } from "react-error-boundary";
import ReactTooltip from "react-tooltip";
import { COLORS } from "styles/var/colors";
import PATHS from "router/paths";
import { AppContext } from "context/app";
@@ -295,42 +293,35 @@ const QueryDetailsPage = ({
<div
className={`button-wrap ${baseClass}__button-wrap--new-query`}
>
<div
data-tip
data-for="live-query-button"
// Tooltip shows when live queries are globally disabled
data-tip-disable={!isLiveQueryDisabled}
<TooltipWrapper
tipContent="Live reports are disabled in organization settings."
position="top"
disableTooltip={!isLiveQueryDisabled}
underline={false}
showArrow
>
<Button
className={`${baseClass}__run`}
variant="inverse"
onClick={() => {
queryId &&
router.push(
getPathWithQueryParams(
PATHS.LIVE_REPORT(queryId),
{
host_id: hostId,
fleet_id: currentTeamId,
}
)
);
}}
disabled={isLiveQueryDisabled}
>
Live report <Icon name="run" />
</Button>
</div>
<ReactTooltip
className="live-query-button-tooltip"
place="top"
effect="solid"
backgroundColor={COLORS["tooltip-bg"]}
id="live-query-button"
data-html
>
Live reports are disabled in organization settings
</ReactTooltip>
<div>
<Button
className={`${baseClass}__run`}
variant="inverse"
onClick={() => {
queryId &&
router.push(
getPathWithQueryParams(
PATHS.LIVE_REPORT(queryId),
{
host_id: hostId,
fleet_id: currentTeamId,
}
)
);
}}
disabled={isLiveQueryDisabled}
>
Live report <Icon name="run" />
</Button>
</div>
</TooltipWrapper>
</div>
)}
{canEditQuery && (
@@ -653,7 +653,6 @@ const EditQueryForm = ({
isAnyTeamObserverPlus) && (
<div className={`button-wrap ${baseClass}__button-wrap--new-query`}>
<TooltipWrapper
className="live-query-button-tooltip"
tipContent="Live reports are disabled in organization settings"
disableTooltip={!disabledLiveQuery}
position="top"
@@ -932,7 +931,6 @@ const EditQueryForm = ({
</>
)}
<TooltipWrapper
className="live-query-button-tooltip"
tipContent="Live reports are disabled in organization settings"
disableTooltip={!disabledLiveQuery}
position="top"
+14 -4
View File
@@ -339,11 +339,21 @@ export const SCHEDULE_PLATFORM_DROPDOWN_OPTIONS = [
export const HOSTS_SEARCH_BOX_PLACEHOLDER =
"Search name, user email, hostname, UUID, serial number, or private IP address";
export const HOSTS_SEARCH_BOX_TOOLTIP =
"Search hosts by name, user email, hostname, UUID, serial number, or private IP address";
export const HOSTS_SEARCH_BOX_TOOLTIP = (
<>
Search hosts by name, user email, hostname,
<br />
UUID, serial number, or private IP address.
</>
);
export const VULNERABILITIES_SEARCH_BOX_TOOLTIP =
'To search for an exact CVE, surround the string in double quotes (e.g. "CVE-2024-1234")';
export const VULNERABILITIES_SEARCH_BOX_TOOLTIP = (
<>
To search for an exact CVE, surround the string
<br />
in double quotes (e.g. &quot;CVE-2024-1234&quot;).
</>
);
// Keys from API
export const MDM_STATUS_TOOLTIP: Record<
+3 -3
View File
@@ -730,9 +730,9 @@ export const getPerformanceImpactIndicatorTooltip = (
case PerformanceImpactIndicatorValue.UNDETERMINED:
return (
<>
Performance impact will be available when{" "}
{isHostSpecific ? "the" : "this"} <br />
report runs{isHostSpecific && " on this host"}.
Performance impact will be available
<br /> when {isHostSpecific ? "the" : "this"} report runs
{isHostSpecific && " on this host"}.
</>
);
default: