New tooltips! (#4326)

* Allow sort by more than one key

* created custom tooltip component

* remove unused code

* fixed style for more layouts

* added tooltip to query side panel

* tooltips added to setting form

* finished settings form

* added tooltip for perf impact table headers

* tooltip for pack table and user form

* tooltip on manage policies page

* tooltip for manage schedules

* tooltip for automations; spacing for form input

* tooltip for automations modal

* user form; fixed input with icon component

* more user form tooltips

* tooltip for homepage; style fixes

* replaced many more tooltips with new version

* added story for tooltip

* added position prop

* fixed tests

* re-work how we click react-select dropdowns

* forcing the update button click

* trying a blur

* fixed typo

* trying blur on another element

* temp check-in

* replaced tooltip from host details software

* more consolidation of tooltip use for software

* fixed settings flow test

Co-authored-by: Tomas Touceda <chiiph@gmail.com>
This commit is contained in:
Martavis Parker 2022-02-28 13:25:06 -08:00 committed by GitHub
parent 455033476f
commit 33c5f0651c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
50 changed files with 572 additions and 797 deletions

View file

@ -49,8 +49,10 @@ describe("App settings flow", () => {
.click()
.type("https://http.cat/100");
// only allowed to fill in either metadata || metadata url
cy.findByLabelText(/metadata url/i)
// specifically targeting this one to avoid conflict
// with cypress seeing multiple "metadata url" - one
// in a tooltip, the other as the actual label
cy.getAttached("[for='metadataURL']")
.click()
.type("http://github.com/fleetdm/fleet");
@ -62,9 +64,10 @@ describe("App settings flow", () => {
.click()
.type("rachel@example.com");
cy.findByLabelText(/smtp server/i)
.click()
.type("localhost");
// specifically targeting this one to avoid conflict
// with cypress seeing multiple "metadata" - one
// in a tooltip, the other as the actual label
cy.getAttached("[for='smtpServer']").click().type("localhost");
cy.getAttached("#smtpPort").clear().type("1025");
@ -86,15 +89,13 @@ describe("App settings flow", () => {
.click()
.type("http://server.com/example");
cy.getAttached(".app-config-form__host-percentage").click();
cy.getAttached(
".app-config-form__host-percentage .Select-control"
).click();
cy.getAttached(".Select-menu-outer").contains(/5%/i).click();
cy.getAttached(".app-config-form__host-percentage")
.contains(/5%/i)
.click();
cy.getAttached(".app-config-form__days-count").click();
cy.getAttached(".app-config-form__days-count")
cy.getAttached(".app-config-form__days-count .Select-control").click();
cy.getAttached(".Select-menu-outer")
.contains(/7 days/i)
.click();
@ -104,15 +105,20 @@ describe("App settings flow", () => {
cy.findByLabelText(/verify ssl certs/i).check({ force: true });
cy.findByLabelText(/enable starttls/i).check({ force: true });
cy.findByLabelText(/^host expiry$/i).check({ force: true });
cy.getAttached("[for='enableHostExpiry']").within(() => {
cy.getAttached("[type='checkbox']").check({ force: true });
});
cy.findByLabelText(/host expiry window/i)
.clear()
.type("5");
// specifically targeting this one to avoid conflict
// with cypress seeing multiple "host expiry" - one
// in the checkbox above, the other as this label
cy.getAttached("[name='hostExpiryWindow']").clear().type("5");
cy.findByLabelText(/disable live queries/i).check({ force: true });
cy.findByRole("button", { name: /update settings/i }).click();
cy.findByRole("button", { name: /update settings/i })
.invoke("attr", "disabled", false)
.click();
cy.findByText(/updated settings/i).should("exist");
@ -150,7 +156,7 @@ describe("App settings flow", () => {
"https://http.cat/100"
);
cy.findByLabelText(/metadata url/i).should(
cy.getAttached("#metadataURL").should(
"have.value",
"http://github.com/fleetdm/fleet"
);
@ -160,7 +166,7 @@ describe("App settings flow", () => {
"rachel@example.com"
);
cy.findByLabelText(/smtp server/i).should("have.value", "localhost");
cy.getAttached("#smtpServer").should("have.value", "localhost");
cy.getAttached("#smtpPort").should("have.value", "1025");

View file

@ -0,0 +1,31 @@
import React from "react";
import formatDistanceToNowStrict from "date-fns/formatDistanceToNowStrict";
import TooltipWrapper from "components/TooltipWrapper";
const baseClass = "component__last-updated-text";
const renderLastUpdatedText = (
lastUpdatedAt: string,
whatToRetrieve: string
): JSX.Element => {
if (!lastUpdatedAt || lastUpdatedAt === "0001-01-01T00:00:00Z") {
lastUpdatedAt = "never";
} else {
lastUpdatedAt = formatDistanceToNowStrict(new Date(lastUpdatedAt), {
addSuffix: true,
});
}
return (
<span className={baseClass}>
<TooltipWrapper
tipContent={`Fleet periodically queries all hosts to retrieve ${whatToRetrieve}`}
>
{`Last updated ${lastUpdatedAt}`}
</TooltipWrapper>
</span>
);
};
export default renderLastUpdatedText;

View file

@ -0,0 +1,19 @@
.component__last-updated-text {
font-size: $xx-small;
color: $ui-fleet-black-75;
padding-right: $pad-small;
.tooltip {
padding-left: $pad-xsmall;
}
.tooltip__tooltip-text {
display: flex;
text-align: center;
}
img {
height: 16px;
width: 16px;
vertical-align: text-top;
}
}

View file

@ -0,0 +1,38 @@
import React from "react";
import { Meta, Story } from "@storybook/react";
import TooltipWrapper from ".";
import "../../index.scss";
interface ITooltipWrapperProps {
children: string;
tipContent: string;
}
export default {
component: TooltipWrapper,
title: "Components/Tooltip",
args: {
tipContent: "This is an example tooltip.",
},
argTypes: {
position: {
options: ["top", "bottom"],
control: "radio",
},
},
} as Meta;
// using line breaks to create space for top position
const Template: Story<ITooltipWrapperProps> = (props) => (
<>
<br />
<br />
<br />
<br />
<TooltipWrapper {...props}>Example text</TooltipWrapper>
</>
);
export const Default = Template.bind({});

View file

@ -0,0 +1,30 @@
import React from "react";
interface ITooltipWrapperProps {
children: string;
tipContent: string;
position?: "top" | "bottom";
}
const baseClass = "component__tooltip-wrapper";
const TooltipWrapper = ({
children,
tipContent,
position = "bottom",
}: ITooltipWrapperProps): JSX.Element => {
return (
<div className={baseClass} data-position={position}>
<div className={`${baseClass}__element`}>
{children}
<div className={`${baseClass}__underline`} data-text={children} />
</div>
<div
className={`${baseClass}__tip-text`}
dangerouslySetInnerHTML={{ __html: tipContent }}
/>
</div>
);
};
export default TooltipWrapper;

View file

@ -0,0 +1,78 @@
.component__tooltip-wrapper {
position: relative;
cursor: help;
&:hover {
.component__tooltip-wrapper__tip-text {
visibility: visible;
opacity: 1;
}
}
&__element {
position: static;
display: inline; // treat like a span but allow other tags as children
}
&__underline {
position: absolute;
top: 0;
left: 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;
}
}
&__tip-text {
width: max-content;
max-width: 296px;
padding: 12px;
color: $core-white;
background-color: $core-fleet-blue;
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 .3s ease;
line-height: 1.375;
// invisible block to cover space so
// hover state can continue from text to bubble
&::before {
content: "";
width: 100%;
height: 6px;
position: absolute;
top: -6px;
left: 0;
}
p {
margin: 0;
}
}
&[data-position="top"] {
.component__tooltip-wrapper__tip-text {
top: auto;
bottom: 100%;
&::before {
display: none;
}
}
}
}

View file

@ -0,0 +1 @@
export { default } from "./TooltipWrapper";

View file

@ -2,6 +2,8 @@ import React from "react";
import classnames from "classnames";
import { isEmpty } from "lodash";
import TooltipWrapper from "components/TooltipWrapper";
const baseClass = "form-field";
export interface IFormFieldProps {
@ -12,6 +14,7 @@ export interface IFormFieldProps {
label: Array<any> | JSX.Element | string;
name: string;
type: string;
tooltip?: string;
}
const FormField = ({
@ -22,6 +25,7 @@ const FormField = ({
label,
name,
type,
tooltip,
}: IFormFieldProps): JSX.Element => {
const renderLabel = () => {
const labelWrapperClasses = classnames(`${baseClass}__label`, {
@ -33,8 +37,19 @@ const FormField = ({
}
return (
<label className={labelWrapperClasses} htmlFor={name}>
{error || label}
<label
className={labelWrapperClasses}
htmlFor={name}
data-has-tooltip={!!tooltip}
>
{error ||
(tooltip ? (
<TooltipWrapper tipContent={tooltip}>
{label as string}
</TooltipWrapper>
) : (
<>{label}</>
))}
</label>
);
};

View file

@ -12,6 +12,10 @@
font-weight: $bold;
color: $core-vibrant-red;
}
&[data-has-tooltip="true"] {
margin-bottom: $pad-small;
}
}
&__hint {

View file

@ -1,7 +1,6 @@
import React, { Component } from "react";
import PropTypes from "prop-types";
import ReactTooltip from "react-tooltip";
import Button from "components/buttons/Button";
import Form from "components/forms/Form";
import formFieldInterface from "interfaces/form_field";
@ -57,25 +56,15 @@ class UserSettingsForm extends Component {
label="Email (required)"
hint={renderEmailHint()}
disabled={!smtpConfigured}
tooltip={
"\
Editing your email address requires that SMTP is configured in order to send a validation email.\
<br /><br /> \
Users with Admin role can configure SMTP in <strong>Settings &gt; Organization settings</strong>.\
"
}
/>
</div>
<ReactTooltip
place="bottom"
type="dark"
effect="solid"
id="smtp-tooltip"
backgroundColor="#3e4771"
data-html
>
<span className={`${baseClass}__tooltip-text`}>
Editing your email address requires that SMTP is <br />
configured in order to send a validation email. <br />
<br />
Users with Admin role can configure SMTP in
<br />
<strong>Settings &gt; Organization settings</strong>.
</span>
</ReactTooltip>
<InputField
{...fields.name}
label="Full name (required)"

View file

@ -21,12 +21,10 @@ import validateYaml from "components/forms/validators/validate_yaml";
import validEmail from "components/forms/validators/valid_email";
import validUrl from "components/forms/validators/valid_url";
import IconToolTip from "components/IconToolTip";
import InfoBanner from "components/InfoBanner/InfoBanner";
// @ts-ignore
import YamlAce from "components/YamlAce";
import Modal from "components/Modal";
import SelectTargetsDropdownStories from "components/forms/fields/SelectTargetsDropdown/SelectTargetsDropdown.stories";
import OpenNewTabIcon from "../../../../../assets/images/open-new-tab-12x12@2x.png";
import {
IAppConfigFormProps,
@ -383,11 +381,7 @@ const AppConfigFormFunctional = ({
parseTarget
onBlur={validateForm}
error={formErrors.server_url}
/>
</div>
<div className={`${baseClass}__details`}>
<IconToolTip
text={"The base URL of this instance for use in Fleet links."}
tooltip="The base URL of this instance for use in Fleet links."
/>
</div>
</div>
@ -419,13 +413,7 @@ const AppConfigFormFunctional = ({
parseTarget
onBlur={validateForm}
error={formErrors.idp_name}
/>
</div>
<div className={`${baseClass}__details`}>
<IconToolTip
text={
"A required human friendly name for the identity provider that will provide single sign on authentication."
}
tooltip="A required human friendly name for the identity provider that will provide single sign on authentication."
/>
</div>
<div className={`${baseClass}__inputs`}>
@ -443,13 +431,7 @@ const AppConfigFormFunctional = ({
parseTarget
onBlur={validateForm}
error={formErrors.entity_id}
/>
</div>
<div className={`${baseClass}__details`}>
<IconToolTip
text={
"The required entity ID is a URI that you use to identify Fleet when configuring the identity provider."
}
tooltip="The required entity ID is a URI that you use to identify Fleet when configuring the identity provider."
/>
</div>
<div className={`${baseClass}__inputs`}>
@ -459,11 +441,7 @@ const AppConfigFormFunctional = ({
name="issuerURI"
value={issuerURI}
parseTarget
/>
</div>
<div className={`${baseClass}__details`}>
<IconToolTip
text={"The issuer URI supplied by the identity provider."}
tooltip="The issuer URI supplied by the identity provider."
/>
</div>
<div className={`${baseClass}__inputs`}>
@ -475,13 +453,7 @@ const AppConfigFormFunctional = ({
parseTarget
onBlur={validateForm}
error={formErrors.idp_image_url}
/>
</div>
<div className={`${baseClass}__details`}>
<IconToolTip
text={
"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."
/>
</div>
<div className={`${baseClass}__inputs`}>
@ -493,13 +465,7 @@ const AppConfigFormFunctional = ({
value={metadata}
parseTarget
onBlur={validateForm}
/>
</div>
<div className={`${baseClass}__details`}>
<IconToolTip
text={
"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."
/>
</div>
<div className={`${baseClass}__inputs`}>
@ -517,14 +483,9 @@ const AppConfigFormFunctional = ({
parseTarget
onBlur={validateForm}
error={formErrors.metadata_url}
tooltip="A URL that references the identity provider metadata."
/>
</div>
<div className={`${baseClass}__details`}>
<IconToolTip
text={"A URL that references the identity provider metadata."}
/>
</div>
<div className={`${baseClass}__inputs`}>
<Checkbox
onChange={handleInputChange}
@ -621,11 +582,9 @@ const AppConfigFormFunctional = ({
parseTarget
onBlur={validateForm}
error={formErrors.sender_address}
tooltip="The sender address for emails from Fleet."
/>
</div>
<div className={`${baseClass}__details`}>
<IconToolTip text={"The sender address for emails from Fleet."} />
</div>
<div className={`${baseClass}__inputs ${baseClass}__inputs--smtp`}>
<InputField
label="SMTP server"
@ -635,6 +594,7 @@ const AppConfigFormFunctional = ({
parseTarget
onBlur={validateForm}
error={formErrors.server}
tooltip="The hostname / IP address and corresponding port of your organization's SMTP server."
/>
<InputField
label="&nbsp;"
@ -655,13 +615,6 @@ const AppConfigFormFunctional = ({
Use SSL/TLS to connect (recommended)
</Checkbox>
</div>
<div className={`${baseClass}__details`}>
<IconToolTip
text={
"The hostname / IP address and corresponding port of your organization's SMTP server."
}
/>
</div>
<div className={`${baseClass}__inputs`}>
<Dropdown
label="Authentication type"
@ -670,20 +623,15 @@ const AppConfigFormFunctional = ({
name="smtpAuthenticationType"
value={smtpAuthenticationType}
parseTarget
/>
{renderSmtpSection()}
</div>
<div className={`${baseClass}__details`}>
<IconToolTip
isHtml
text={
tooltip={
"\
<p>If your mail server requires authentication, you need to specify the authentication type here.</p> \
<p><strong>No Authentication</strong> - Select this if your SMTP is open.</p> \
<p><strong>Username & Password</strong> - Select this if your SMTP server requires authentication with a username and password.</p>\
"
<p>If your mail server requires authentication, you need to specify the authentication type here.</p> \
<p><strong>No Authentication</strong> - Select this if your SMTP is open.</p> \
<p><strong>Username & Password</strong> - Select this if your SMTP server requires authentication with a username and password.</p>\
"
}
/>
{renderSmtpSection()}
</div>
</div>
);
@ -794,14 +742,9 @@ const AppConfigFormFunctional = ({
parseTarget
onBlur={validateForm}
error={formErrors.destination_url}
/>
</div>
<div className={`${baseClass}__details`}>
<IconToolTip
isHtml
text={
tooltip={
"\
<center><p>Provide a URL to deliver <br/>the webhook request to.</p></center>\
<p>Provide a URL to deliver <br/>the webhook request to.</p>\
"
}
/>
@ -814,14 +757,9 @@ const AppConfigFormFunctional = ({
name="hostStatusWebhookHostPercentage"
value={hostStatusWebhookHostPercentage}
parseTarget
/>
</div>
<div className={`${baseClass}__details`}>
<IconToolTip
isHtml
text={
tooltip={
"\
<center><p>Select the minimum percentage of hosts that<br/>must fail to check into Fleet in order to trigger<br/>the webhook request.</p></center>\
<p>Select the minimum percentage of hosts that<br/>must fail to check into Fleet in order to trigger<br/>the webhook request.</p>\
"
}
/>
@ -834,14 +772,9 @@ const AppConfigFormFunctional = ({
name="hostStatusWebhookDaysCount"
value={hostStatusWebhookDaysCount}
parseTarget
/>
</div>
<div className={`${baseClass}__details`}>
<IconToolTip
isHtml
text={
tooltip={
"\
<center><p>Select the minimum number of days that the<br/>configured <b>Percentage of hosts</b> must fail to<br/>check into Fleet in order to trigger the<br/>webhook request.</p></center>\
<p>Select the minimum number of days that the<br/>configured <b>Percentage of hosts</b> must fail to<br/>check into Fleet in order to trigger the<br/>webhook request.</p>\
"
}
/>
@ -917,10 +850,7 @@ const AppConfigFormFunctional = ({
name="domain"
value={domain}
parseTarget
/>
<IconToolTip
isHtml
text={
tooltip={
'<p>If you need to specify a HELO domain, <br />you can do it here <em className="hint hint--brand">(Default: <strong>Blank</strong>)</em></p>'
}
/>
@ -931,15 +861,12 @@ const AppConfigFormFunctional = ({
name="verifySSLCerts"
value={verifySSLCerts}
parseTarget
tooltip={
'<p>Turn this off (not recommended) <br />if you use a self-signed certificate <em className="hint hint--brand"><br />(Default: <strong>On</strong>)</em></p>'
}
>
Verify SSL certs
</Checkbox>
<IconToolTip
isHtml
text={
'<p>Turn this off (not recommended) <br />if you use a self-signed certificate <em className="hint hint--brand"><br />(Default: <strong>On</strong>)</em></p>'
}
/>
</div>
<div className="tooltip-wrap">
<Checkbox
@ -947,15 +874,12 @@ const AppConfigFormFunctional = ({
name="enableStartTLS"
value={enableStartTLS}
parseTarget
tooltip={
'<p>Detects if STARTTLS is enabled <br />in your SMTP server and starts <br />to use it. <em className="hint hint--brand">(Default: <strong>On</strong>)</em></p>'
}
>
Enable STARTTLS
</Checkbox>
<IconToolTip
isHtml
text={
'<p>Detects if STARTTLS is enabled <br />in your SMTP server and starts <br />to use it. <em className="hint hint--brand">(Default: <strong>On</strong>)</em></p>'
}
/>
</div>
<div className="tooltip-wrap">
<Checkbox
@ -963,15 +887,12 @@ const AppConfigFormFunctional = ({
name="enableHostExpiry"
value={enableHostExpiry}
parseTarget
tooltip={
'<p>When enabled, allows automatic cleanup <br />of hosts that have not communicated with Fleet <br />in some number of days. <em className="hint hint--brand">(Default: <strong>Off</strong>)</em></p>'
}
>
Host expiry
</Checkbox>
<IconToolTip
isHtml
text={
'<p>When enabled, allows automatic cleanup <br />of hosts that have not communicated with Fleet <br />in some number of days. <em className="hint hint--brand">(Default: <strong>Off</strong>)</em></p>'
}
/>
</div>
<div className="tooltip-wrap tooltip-wrap--input">
<InputField
@ -984,11 +905,8 @@ const AppConfigFormFunctional = ({
parseTarget
onBlur={validateForm}
error={formErrors.host_expiry_window}
/>
<IconToolTip
isHtml
text={
"<p>If a host has not communicated with Fleet <br />in the specified number of days, it will be removed.</p>"
tooltip={
"<p>If a host has not communicated with Fleet in the specified number of days, it will be removed.</p>"
}
/>
</div>
@ -998,15 +916,12 @@ const AppConfigFormFunctional = ({
name="disableLiveQuery"
value={disableLiveQuery}
parseTarget
tooltip={
'<p>When enabled, disables the ability to run live queries <br />(ad hoc queries executed via the UI or fleetctl). <em className="hint hint--brand">(Default: <strong>Off</strong>)</em></p>'
}
>
Disable live queries
</Checkbox>
<IconToolTip
isHtml
text={
'<p>When enabled, disables the ability to run live queries <br />(ad hoc queries executed via the UI or fleetctl). <em className="hint hint--brand">(Default: <strong>Off</strong>)</em></p>'
}
/>
</div>
</div>
</div>

View file

@ -4,6 +4,7 @@ import { noop, pick } from "lodash";
import FormField from "components/forms/FormField";
import { IFormFieldProps } from "components/forms/FormField/FormField";
import TooltipWrapper from "components/TooltipWrapper";
const baseClass = "fleet-checkbox";
@ -18,6 +19,7 @@ export interface ICheckboxProps {
wrapperClassName?: string;
indeterminate?: boolean;
parseTarget?: boolean;
tooltip?: string;
}
const Checkbox = (props: ICheckboxProps) => {
@ -32,6 +34,7 @@ const Checkbox = (props: ICheckboxProps) => {
wrapperClassName,
indeterminate,
parseTarget,
tooltip,
} = props;
const handleChange = () => {
@ -72,7 +75,15 @@ const Checkbox = (props: ICheckboxProps) => {
}}
/>
<span className={checkBoxTickClass} />
<span className={`${checkBoxClass}__label`}>{children}</span>
<span className={`${checkBoxClass}__label`}>
{tooltip ? (
<TooltipWrapper tipContent={tooltip}>
{children as string}
</TooltipWrapper>
) : (
<>{children}</>
)}
</span>
</label>
</FormField>
);

View file

@ -85,5 +85,6 @@
&__label {
font-size: $x-small;
padding-left: $pad-small;
display: inherit;
}
}

View file

@ -32,6 +32,7 @@ class Dropdown extends Component {
]),
wrapperClassName: PropTypes.string,
parseTarget: PropTypes.bool,
tooltip: PropTypes.string,
};
static defaultProps = {
@ -45,6 +46,7 @@ class Dropdown extends Component {
name: "targets",
placeholder: "Select One...",
parseTarget: false,
tooltip: "",
};
onMenuOpen = () => {
@ -124,7 +126,13 @@ class Dropdown extends Component {
searchable,
} = this.props;
const formFieldProps = pick(this.props, ["hint", "label", "error", "name"]);
const formFieldProps = pick(this.props, [
"hint",
"label",
"error",
"name",
"tooltip",
]);
const selectClasses = classnames(className, `${baseClass}__select`, {
[`${baseClass}__select--error`]: error,
});

View file

@ -28,6 +28,7 @@ class InputField extends Component {
PropTypes.number,
]).isRequired,
parseTarget: PropTypes.bool,
tooltip: PropTypes.string,
};
static defaultProps = {
@ -42,6 +43,7 @@ class InputField extends Component {
blockAutoComplete: false,
value: "",
parseTarget: false,
tooltip: "",
};
componentDidMount() {
@ -93,7 +95,13 @@ class InputField extends Component {
[`${baseClass}__textarea`]: type === "textarea",
});
const formFieldProps = pick(this.props, ["hint", "label", "error", "name"]);
const formFieldProps = pick(this.props, [
"hint",
"label",
"error",
"name",
"tooltip",
]);
if (type === "textarea") {
return (

View file

@ -3,6 +3,7 @@ import PropTypes from "prop-types";
import classnames from "classnames";
import FleetIcon from "components/icons/FleetIcon";
import TooltipWrapper from "components/TooltipWrapper";
import InputField from "../InputField";
const baseClass = "input-icon-field";
@ -23,10 +24,11 @@ class InputFieldWithIcon extends InputField {
disabled: PropTypes.bool,
iconPosition: PropTypes.oneOf(["start", "end"]),
inputOptions: PropTypes.object, // eslint-disable-line react/forbid-prop-types
tooltip: PropTypes.string,
};
renderHeading = () => {
const { error, placeholder, name, label } = this.props;
const { error, placeholder, name, label, tooltip } = this.props;
const labelClasses = classnames(`${baseClass}__label`);
if (error) {
@ -34,8 +36,16 @@ class InputFieldWithIcon extends InputField {
}
return (
<label htmlFor={name} className={labelClasses}>
{label || placeholder}
<label
htmlFor={name}
className={labelClasses}
data-has-tooltip={!!tooltip}
>
{tooltip ? (
<TooltipWrapper tipContent={tooltip}>{label}</TooltipWrapper>
) : (
<>{label || placeholder}</>
)}
</label>
);
};

View file

@ -56,9 +56,14 @@
}
&__label {
display: block;
font-size: $x-small;
font-weight: $bold;
margin-bottom: $pad-xsmall;
&[data-has-tooltip="true"] {
margin-bottom: $pad-small;
}
}
&__errors {

View file

@ -1,6 +1,8 @@
import React from "react";
import classnames from "classnames";
import TooltipWrapper from "components/TooltipWrapper";
const baseClass = "radio";
export interface IRadioProps {
@ -12,6 +14,7 @@ export interface IRadioProps {
name?: string;
className?: string;
disabled?: boolean;
tooltip?: string;
}
const Radio = ({
@ -22,6 +25,7 @@ const Radio = ({
checked,
disabled,
label,
tooltip,
onChange,
}: IRadioProps): JSX.Element => {
const wrapperClasses = classnames(baseClass, className);
@ -44,7 +48,13 @@ const Radio = ({
/>
<span className={`${baseClass}__control`} />
</span>
<span className={`${baseClass}__label`}>{label}</span>
<span className={`${baseClass}__label`}>
{tooltip ? (
<TooltipWrapper tipContent={tooltip}>{label}</TooltipWrapper>
) : (
<>{label}</>
)}
</span>
</label>
);
};

View file

@ -2,18 +2,18 @@
// disable this rule as it was throwing an error in Header and Cell component
// definitions for the selection row for some reason when we dont really need it.
import React from "react";
import ReactTooltip from "react-tooltip";
import { find } from "lodash";
import { performanceIndicator } from "fleet/helpers";
import { IScheduledQuery } from "interfaces/scheduled_query";
import { IDropdownOption } from "interfaces/dropdownOption";
import Checkbox from "components/forms/fields/Checkbox";
import DropdownCell from "components/TableContainer/DataTable/DropdownCell";
import HeaderCell from "components/TableContainer/DataTable/HeaderCell/HeaderCell";
import PillCell from "components/TableContainer/DataTable/PillCell";
import TextCell from "components/TableContainer/DataTable/TextCell";
import { IScheduledQuery } from "interfaces/scheduled_query";
import { IDropdownOption } from "interfaces/dropdownOption";
import QuestionIcon from "../../../../../assets/images/icon-question-16x16@2x.png";
import TooltipWrapper from "components/TooltipWrapper";
interface IGetToggleAllRowsSelectedProps {
checked: boolean;
@ -123,33 +123,12 @@ const generateTableHeaders = (
title: "Performance impact",
Header: () => {
return (
<div>
<div className="column-with-tooltip">
<span className="queries-table__performance-impact-header">
Performance impact
<TooltipWrapper tipContent="This is the average performance impact across all hosts where this query was scheduled.">
Performance impact
</TooltipWrapper>
</span>
<span
data-tip
data-for="queries-table__performance-impact-tooltip"
data-tip-disable={false}
>
<img alt="question icon" src={QuestionIcon} />
</span>
<ReactTooltip
className="queries-table__performance-impact-tooltip"
place="bottom"
type="dark"
effect="solid"
backgroundColor="#3e4771"
id="queries-table__performance-impact-tooltip"
data-html
>
<div style={{ textAlign: "center" }}>
This is the average <br />
performance impact <br />
across all hosts where this <br />
query was scheduled.
</div>
</ReactTooltip>
</div>
);
},

View file

@ -48,4 +48,10 @@
.queries-table__performance-impact-tooltip {
font-weight: 400;
}
.column-with-tooltip {
height: 0;
position: relative;
top: -10px;
}
}

View file

@ -1,13 +1,12 @@
import React from "react";
import classnames from "classnames";
import IconToolTip from "components/IconToolTip";
import { IOsqueryTable } from "interfaces/osquery_table"; // @ts-ignore
import { osqueryTableNames } from "utilities/osquery_tables"; // @ts-ignore
import Dropdown from "components/forms/fields/Dropdown"; // @ts-ignore
import FleetIcon from "components/icons/FleetIcon"; // @ts-ignore
import FleetIcon from "components/icons/FleetIcon";
import TooltipWrapper from "components/TooltipWrapper"; // @ts-ignore
import SecondarySidePanelContainer from "../SecondarySidePanelContainer";
import AppleIcon from "../../../../assets/images/icon-apple-dark-20x20@2x.png";
import LinuxIcon from "../../../../assets/images/icon-linux-dark-20x20@2x.png";
import WindowsIcon from "../../../../assets/images/icon-windows-dark-20x20@2x.png";
@ -49,8 +48,11 @@ const QuerySidePanel = ({
return columns?.map((column) => (
<li key={column.name} className={`${columnBaseClass}__item`}>
<span className={`${columnBaseClass}__name`}>{column.name}</span>
<IconToolTip text={column.description} />
<span className={`${columnBaseClass}__name`}>
<TooltipWrapper tipContent={column.description}>
{column.name}
</TooltipWrapper>
</span>
<div className={`${columnBaseClass}__description`}>
<span className={`${columnBaseClass}__type`}>
{displayTypeForDataType(column.type)}

View file

@ -3,6 +3,6 @@
@import "styles/helpers.scss";
@import "styles/var/**/*.scss";
@import "styles/global/**/*.scss";
@import "components/**/*.scss";
@import "components/**/**/*.scss";
@import "layouts/**/*.scss";
@import "pages/**/**/*.scss";

View file

@ -7,7 +7,7 @@ import { IMacadminAggregate, IDataTableMDMFormat } from "interfaces/macadmins";
import TableContainer from "components/TableContainer";
// @ts-ignore
import Spinner from "components/Spinner";
import renderLastUpdatedText from "../../components/LastUpdatedText";
import renderLastUpdatedText from "components/LastUpdatedText";
import generateTableHeaders from "./MDMTableConfig";
interface IMDMCardProps {

View file

@ -84,24 +84,3 @@
color: $ui-error;
}
}
.homepage-info-card__section-title-detail {
.last-updated {
font-size: $xx-small;
color: $ui-fleet-black-75;
padding-right: $pad-small;
.tooltip {
padding-left: $pad-xsmall;
}
.tooltip__tooltip-text {
display: flex;
text-align: center;
}
img {
height: 16px;
width: 16px;
vertical-align: text-top;
}
}
}

View file

@ -7,7 +7,7 @@ import { IMacadminAggregate, IMunkiAggregate } from "interfaces/macadmins";
import TableContainer from "components/TableContainer";
// @ts-ignore
import Spinner from "components/Spinner";
import renderLastUpdatedText from "../../components/LastUpdatedText";
import renderLastUpdatedText from "components/LastUpdatedText";
import generateTableHeaders from "./MunkiTableConfig";
interface IMunkiCardProps {

View file

@ -84,24 +84,3 @@
color: $ui-error;
}
}
.homepage-info-card__section-title-detail {
.last-updated {
font-size: $xx-small;
color: $ui-fleet-black-75;
padding-right: $pad-small;
.tooltip {
padding-left: $pad-xsmall;
}
.tooltip__tooltip-text {
display: flex;
text-align: center;
}
img {
height: 16px;
width: 16px;
vertical-align: text-top;
}
}
}

View file

@ -10,7 +10,7 @@ import TableContainer, { ITableQueryData } from "components/TableContainer";
import TableDataError from "components/TableDataError"; // TODO how do we handle errors? UI just keeps spinning?
// @ts-ignore
import Spinner from "components/Spinner";
import renderLastUpdatedText from "../../components/LastUpdatedText/LastUpdatedText";
import renderLastUpdatedText from "components/LastUpdatedText/LastUpdatedText";
import generateTableHeaders from "./SoftwareTableConfig";
interface ISoftwareCardProps {

View file

@ -136,24 +136,3 @@
color: $ui-error;
}
}
.homepage-info-card__section-title-detail {
.last-updated {
font-size: $xx-small;
color: $ui-fleet-black-75;
padding-right: $pad-small;
.tooltip {
padding-left: $pad-xsmall;
}
.tooltip__tooltip-text {
display: flex;
text-align: center;
}
img {
height: 16px;
width: 16px;
vertical-align: text-top;
}
}
}

View file

@ -1,53 +0,0 @@
import React from "react";
import ReactTooltip from "react-tooltip";
import formatDistanceToNowStrict from "date-fns/formatDistanceToNowStrict";
import { kebabCase } from "lodash";
import QuestionIcon from "../../../../../assets/images/icon-question-16x16@2x.png";
const renderLastUpdatedText = (
lastUpdatedAt: string,
whatToRetrieve: string
): JSX.Element => {
if (!lastUpdatedAt || lastUpdatedAt === "0001-01-01T00:00:00Z") {
lastUpdatedAt = "never";
} else {
lastUpdatedAt = formatDistanceToNowStrict(new Date(lastUpdatedAt), {
addSuffix: true,
});
}
return (
<span className="last-updated">
{`Last updated ${lastUpdatedAt}`}
<span className={`tooltip`}>
<span
className={`tooltip__tooltip-icon`}
data-tip
data-for={`last-updated-tooltip-${kebabCase(whatToRetrieve)}`}
data-tip-disable={false}
>
<img alt="question icon" src={QuestionIcon} />
</span>
<ReactTooltip
place="top"
type="dark"
effect="solid"
backgroundColor="#3e4771"
id={`last-updated-tooltip-${kebabCase(whatToRetrieve)}`}
data-html
>
<span className={`tooltip__tooltip-text`}>
Fleet periodically
<br />
queries all hosts
<br />
to retrieve {whatToRetrieve}
</span>
</ReactTooltip>
</span>
</span>
);
};
export default renderLastUpdatedText;

View file

@ -1,29 +1,20 @@
import React, { Component, FormEvent } from "react";
import ReactTooltip from "react-tooltip";
import { Link } from "react-router";
import PATHS from "router/paths";
import { Dispatch } from "redux";
import { ITeam } from "interfaces/team";
import { IUserFormErrors } from "interfaces/user";
import Button from "components/buttons/Button";
import validatePresence from "components/forms/validators/validate_presence";
import validEmail from "components/forms/validators/valid_email";
import { IUserFormErrors } from "interfaces/user"; // @ts-ignore
import { renderFlash } from "redux/nodes/notifications/actions";
// ignore TS error for now until these are rewritten in ts.
// @ts-ignore
import { renderFlash } from "redux/nodes/notifications/actions";
// @ts-ignore
import validPassword from "components/forms/validators/valid_password";
// @ts-ignore
import IconToolTip from "components/IconToolTip";
// @ts-ignore
import InputField from "components/forms/fields/InputField";
// @ts-ignore
import InputFieldWithIcon from "components/forms/fields/InputFieldWithIcon";
// @ts-ignore
import Checkbox from "components/forms/fields/Checkbox";
// @ts-ignore
import Button from "components/buttons/Button";
import validatePresence from "components/forms/validators/validate_presence";
import validEmail from "components/forms/validators/valid_email"; // @ts-ignore
import validPassword from "components/forms/validators/valid_password"; // @ts-ignore
import InputField from "components/forms/fields/InputField"; // @ts-ignore
import InputFieldWithIcon from "components/forms/fields/InputFieldWithIcon"; // @ts-ignore
import Checkbox from "components/forms/fields/Checkbox"; // @ts-ignore
import Dropdown from "components/forms/fields/Dropdown";
import Radio from "components/forms/fields/Radio";
import InfoBanner from "components/InfoBanner/InfoBanner";
@ -485,69 +476,40 @@ class UserForm extends Component<ICreateUserFormProps, ICreateUserFormState> {
data-tip-disable={isNewUser || smtpConfigured}
>
<InputFieldWithIcon
label="Email"
error={errors.email || serverErrors?.email}
name="email"
onChange={onInputChange("email")}
placeholder="Email"
value={email || ""}
disabled={!isNewUser && !smtpConfigured}
tooltip={
"\
Editing an email address requires that SMTP is configured in order to send a validation email. \
<br /><br /> \
Users with Admin role can configure SMTP in <strong>Settings &gt; Organization settings</strong>. \
"
}
/>
</div>
<ReactTooltip
place="bottom"
type="dark"
effect="solid"
id="email-disabled-tooltip"
backgroundColor="#3e4771"
data-html
>
<span className={`${baseClass}__tooltip-text`}>
Editing an email address requires that SMTP is <br />
configured in order to send a validation email. <br />
<br />
Users with Admin role can configure SMTP in
<br />
<strong>Settings &gt; Organization settings</strong>.
</span>
</ReactTooltip>
<div className={`${baseClass}__sso-input`}>
<div
className="sso-disabled"
data-tip
data-for="sso-disabled-tooltip"
data-tip-disable={canUseSso}
data-offset="{'top': 25, 'left': 100}"
<Checkbox
name="sso_enabled"
onChange={onCheckboxChange("sso_enabled")}
value={canUseSso && sso_enabled}
disabled={!canUseSso}
wrapperClassName={`${baseClass}__invite-admin`}
tooltip={`
Enabling single sign on for a user requires that SSO is first enabled for the organization.
<br /><br />
Users with Admin role can configure SSO in <strong>Settings &gt; Organization settings</strong>.
`}
>
<Checkbox
name="sso_enabled"
onChange={onCheckboxChange("sso_enabled")}
value={canUseSso && sso_enabled}
disabled={!canUseSso}
wrapperClassName={`${baseClass}__invite-admin`}
>
Enable single sign on
</Checkbox>
<p className={`${baseClass}__sso-input sublabel`}>
Password authentication will be disabled for this user.
</p>
</div>
<ReactTooltip
place="bottom"
type="dark"
effect="solid"
id="sso-disabled-tooltip"
backgroundColor="#3e4771"
data-html
>
<span className={`${baseClass}__tooltip-text`}>
Enabling single sign on for a user requires that SSO is <br />
first enabled for the organization. <br />
<br />
Users with Admin role can configure SSO in
<br />
<strong>Settings &gt; Organization settings</strong>.
</span>
</ReactTooltip>
Enable single sign on
</Checkbox>
<p className={`${baseClass}__sso-input sublabel`}>
Password authentication will be disabled for this user.
</p>
</div>
{isNewUser && (
<div className={`${baseClass}__new-user-container`}>
@ -563,45 +525,26 @@ class UserForm extends Component<ICreateUserFormProps, ICreateUserFormState> {
name={"newUserType"}
onChange={onRadioChange("newUserType")}
/>
<div
className="invite-disabled"
data-tip
data-for="invite-disabled-tooltip"
data-tip-disable={smtpConfigured}
>
<Radio
className={`${baseClass}__radio-input`}
label={"Invite user"}
id={"invite-user"}
disabled={!smtpConfigured}
checked={newUserType === NewUserType.AdminInvited}
value={NewUserType.AdminInvited}
name={"newUserType"}
onChange={onRadioChange("newUserType")}
/>
<ReactTooltip
place="bottom"
type="dark"
effect="solid"
id="invite-disabled-tooltip"
backgroundColor="#3e4771"
data-html
>
<span className={`${baseClass}__tooltip-text`}>
The &quot;Invite user&quot; feature requires that SMTP
is
<br />
configured in order to send invitation emails. <br />
<br />
SMTP can be configured in{" "}
<strong>
Settings &gt; <br />
Organization settings
</strong>
.
</span>
</ReactTooltip>
</div>
<Radio
className={`${baseClass}__radio-input`}
label={"Invite user"}
id={"invite-user"}
disabled={!smtpConfigured}
checked={newUserType === NewUserType.AdminInvited}
value={NewUserType.AdminInvited}
name={"newUserType"}
onChange={onRadioChange("newUserType")}
tooltip={
smtpConfigured
? ""
: `
The &quot;Invite user&quot; feature requires that SMTP
is configured in order to send invitation emails.
<br /><br />
SMTP can be configured in Settings &gt; Organization settings.
`
}
/>
</>
) : (
<input
@ -616,6 +559,7 @@ class UserForm extends Component<ICreateUserFormProps, ICreateUserFormState> {
<>
<div className={`${baseClass}__password`}>
<InputField
label="Password"
error={errors.password}
name="password"
onChange={onInputChange("password")}
@ -626,16 +570,9 @@ class UserForm extends Component<ICreateUserFormProps, ICreateUserFormState> {
"Must include 7 characters, at least 1 number (e.g. 0 - 9), and at least 1 symbol (e.g. &*#)",
]}
blockAutoComplete
/>
</div>
<div className={`${baseClass}__details`}>
<IconToolTip
isHtml
text={`\
<div class="password-tooltip-text">\
<p>This password is temporary. This user will be asked to set a new password after logging in to the Fleet UI.</p>\
<p>This user will not be asked to set a new password after logging in to fleetctl or the Fleet API.</p>\
</div>\
tooltip={`\
This password is temporary. This user will be asked to set a new password after logging in to the Fleet UI.<br /><br />\
This user will not be asked to set a new password after logging in to fleetctl or the Fleet API.\
`}
/>
</div>

View file

@ -53,7 +53,7 @@
color: $core-fleet-black;
font-size: $x-small;
font-weight: $bold;
margin-bottom: $pad-xsmall;
margin-bottom: $pad-small;
}
&__user-permissions-info {

View file

@ -43,6 +43,7 @@ import Modal from "components/Modal";
import TableContainer from "components/TableContainer";
import TabsWrapper from "components/TabsWrapper";
import InfoBanner from "components/InfoBanner";
import TooltipWrapper from "components/TooltipWrapper";
import {
Accordion,
AccordionItem,
@ -82,7 +83,6 @@ import CopyIcon from "../../../../assets/images/icon-copy-clipboard-fleet-blue-2
import DeleteIcon from "../../../../assets/images/icon-action-delete-14x14@2x.png";
import IssueIcon from "../../../../assets/images/icon-issue-fleet-black-50-16x16@2x.png";
import QueryIcon from "../../../../assets/images/icon-action-query-16x16@2x.png";
import QuestionIcon from "../../../../assets/images/icon-question-16x16@2x.png";
import TransferIcon from "../../../../assets/images/icon-action-transfer-16x16@2x.png";
const baseClass = "host-details";
@ -611,29 +611,10 @@ const HostDetailsPage = ({
</span>
</p>
<span className={`${baseClass}__os-modal-example-title`}>
Example policy:
</span>{" "}
<span
className="policy-isexamplesue tooltip__tooltip-icon"
data-tip
data-for="policy-example"
data-tip-disable={false}
>
<img alt="host issue" src={QuestionIcon} />
<TooltipWrapper tipContent="A policy is a yes or no question you can ask all your devices.">
Example policy:
</TooltipWrapper>
</span>
<ReactTooltip
place="bottom"
type="dark"
effect="solid"
backgroundColor="#3e4771"
id="policy-example"
data-html
>
<span className={`${baseClass}__tooltip-text`}>
A policy is a yes or no question
<br /> you can ask all your devices.
</span>
</ReactTooltip>
<InputField
disabled
inputWrapperClass={`${baseClass}__os-policy`}

View file

@ -1,16 +1,16 @@
import React from "react";
import ReactTooltip from "react-tooltip";
import { uniqueId } from "lodash";
import TextCell from "components/TableContainer/DataTable/TextCell";
import PillCell from "components/TableContainer/DataTable/PillCell";
import { IQueryStats } from "interfaces/query_stats";
import {
humanQueryLastRun,
performanceIndicator,
secondsToHms,
} from "fleet/helpers";
import IconToolTip from "components/IconToolTip";
import TextCell from "components/TableContainer/DataTable/TextCell";
import PillCell from "components/TableContainer/DataTable/PillCell";
import TooltipWrapper from "components/TooltipWrapper";
interface IHeaderProps {
column: {
@ -65,13 +65,9 @@ const generatePackTableHeaders = (): IDataColumn[] => {
title: "Last run",
Header: () => {
return (
<>
<TooltipWrapper tipContent="The last time the query ran<br/>since the last time osquery <br/>started on this host.">
Last run
<IconToolTip
isHtml
text={`The last time the query ran<br/>since the last time osquery <br/>started on this host.`}
/>
</>
</TooltipWrapper>
);
},
disableSortBy: true,
@ -82,13 +78,9 @@ const generatePackTableHeaders = (): IDataColumn[] => {
title: "Performance impact",
Header: () => {
return (
<>
<TooltipWrapper tipContent="This is the performance <br />impact on this host.">
Performance impact
<IconToolTip
isHtml
text={`This is the performance <br />impact on this host.`}
/>
</>
</TooltipWrapper>
);
},
disableSortBy: true,

View file

@ -2,14 +2,17 @@ import React from "react";
import { Link } from "react-router";
import ReactTooltip from "react-tooltip";
import { isEmpty } from "lodash";
// import distanceInWordsToNow from "date-fns/distance_in_words_to_now"; // TODO: Enable after backend has been updated to provide last_opened_at
// TODO: Enable after backend has been updated to provide last_opened_at
// import distanceInWordsToNow from "date-fns/distance_in_words_to_now";
import { ISoftware } from "interfaces/software";
import PATHS from "router/paths";
import HeaderCell from "components/TableContainer/DataTable/HeaderCell/HeaderCell";
import TextCell from "components/TableContainer/DataTable/TextCell";
import { ISoftware } from "interfaces/software";
import TooltipWrapper from "components/TooltipWrapper";
import IssueIcon from "../../../../../assets/images/icon-issue-fleet-black-50-16x16@2x.png";
import QuestionIcon from "../../../../../assets/images/icon-question-16x16@2x.png";
import Chevron from "../../../../../assets/images/icon-chevron-right-9x6@2x.png";
interface IHeaderProps {
@ -127,29 +130,17 @@ const generateSoftwareTableHeaders = (): IDataColumn[] => {
if (bundle_identifier) {
return (
<span className="name-container">
{name}
<span
className={`software-name tooltip__tooltip-icon`}
data-tip
data-for={`software-name__${cellProps.row.original.id.toString()}`}
data-tip-disable={false}
>
<img alt="bundle identifier" src={QuestionIcon} />
</span>
<ReactTooltip
place="bottom"
type="dark"
effect="solid"
backgroundColor="#3e4771"
id={`software-name__${cellProps.row.original.id.toString()}`}
data-html
>
<span className={`software-name tooltip__tooltip-text`}>
<TooltipWrapper
tipContent={`
<span>
<b>Bundle identifier: </b>
<br />
{bundle_identifier}
${bundle_identifier}
</span>
</ReactTooltip>
`}
>
{name}
</TooltipWrapper>
</span>
);
}

View file

@ -1,9 +1,8 @@
import React from "react";
import ReactTooltip from "react-tooltip";
import HeaderCell from "components/TableContainer/DataTable/HeaderCell/HeaderCell";
import TextCell from "components/TableContainer/DataTable/TextCell";
import QuestionIcon from "../../../../../assets/images/icon-question-16x16@2x.png";
import TooltipWrapper from "components/TooltipWrapper";
interface IHeaderProps {
column: {
@ -51,33 +50,9 @@ const generateUsersTableHeaders = (): IDataColumn[] => {
title: "Shell",
Header: () => {
return (
<div>
<span>Shell</span>
<span
data-tip
data-for="host-users-table__shell-tooltip"
data-tip-disable={false}
>
<img alt="question icon" src={QuestionIcon} />
</span>
<ReactTooltip
className="host-users-table__shell-tooltip"
place="bottom"
type="dark"
effect="solid"
backgroundColor="#3e4771"
id="host-users-table__shell-tooltip"
data-html
>
<div style={{ textAlign: "center" }}>
The command line shell, such as bash,
<br />
that this user is equipped with by default
<br />
when they log in to the system.
</div>
</ReactTooltip>
</div>
<TooltipWrapper tipContent="The command line shell, such as bash,<br />that this user is equipped with by<br />default when they log in to the system.">
Shell
</TooltipWrapper>
);
},
disableSortBy: true,

View file

@ -508,14 +508,6 @@
&:first-child {
padding-right: 0px;
}
.name-container {
.tooltip__tooltip-icon {
@include breakpoint(ltdesktop) {
position: absolute;
right: 0.5rem;
}
}
}
}
.software-link {
@ -546,6 +538,12 @@
text-overflow: unset;
}
}
.name__cell,
.version__cell {
overflow: initial;
text-overflow: initial;
}
}
}

View file

@ -24,10 +24,10 @@ import { DEFAULT_POLICY } from "utilities/constants";
import Button from "components/buttons/Button";
import InfoBanner from "components/InfoBanner/InfoBanner";
import IconToolTip from "components/IconToolTip";
import Spinner from "components/Spinner";
import TeamsDropdown from "components/TeamsDropdown";
import TableDataError from "components/TableDataError";
import TooltipWrapper from "components/TooltipWrapper";
import PoliciesListWrapper from "./components/PoliciesListWrapper";
import ManageAutomationsModal from "./components/ManageAutomationsModal";
import AddPolicyModal from "./components/AddPolicyModal";
@ -471,21 +471,17 @@ const ManagePolicyPage = ({
${baseClass}__inherited-policies-button`}
onClick={toggleShowInheritedPolicies}
>
{inheritedPoliciesButtonText(
showInheritedPolicies,
globalPolicies.length
)}
</Button>
<div className={`${baseClass}__details`}>
<IconToolTip
isHtml
text={
"\
<center><p>All teams policies are checked <br/> for this teams hosts.</p></center>\
"
<TooltipWrapper
tipContent={
'"All teams" policies are checked <br/> for this teams hosts.'
}
/>
</div>
>
{inheritedPoliciesButtonText(
showInheritedPolicies,
globalPolicies.length
)}
</TooltipWrapper>
</Button>
</span>
)}
{showInheritedPoliciesButton && showInheritedPolicies && (

View file

@ -1,19 +1,17 @@
import React, { useState } from "react";
import { IPolicy } from "interfaces/policy";
import { IWebhookFailingPolicies } from "interfaces/webhook";
import { useDeepEffect } from "utilities/hooks";
import { size } from "lodash";
import Modal from "components/Modal";
import Button from "components/buttons/Button";
// @ts-ignore
import Checkbox from "components/forms/fields/Checkbox";
// @ts-ignore
import InputField from "components/forms/fields/InputField";
import IconToolTip from "components/IconToolTip";
import validURL from "components/forms/validators/valid_url";
import { IPolicy } from "interfaces/policy";
import { IWebhookFailingPolicies } from "interfaces/webhook";
import { useDeepEffect } from "utilities/hooks";
import { size } from "lodash";
import PreviewPayloadModal from "../PreviewPayloadModal";
interface IManageAutomationsModalProps {
@ -192,10 +190,7 @@ const ManageAutomationsModal = ({
'For each policy, Fleet will send a JSON payload to this URL with a list of the hosts that updated their answer to "No."'
}
placeholder={"https://server.com/example"}
/>
<IconToolTip
isHtml
text={"<p>Provide a URL to deliver a<br />webhook request to.</p>"}
tooltip="Provide a URL to deliver a webhook request to."
/>
</div>
<Button

View file

@ -2,7 +2,6 @@
/* eslint-disable jsx-a11y/interactive-supports-focus */
import React, { useState, useContext, KeyboardEvent } from "react";
import { IAceEditor } from "react-ace/lib/types";
import ReactTooltip from "react-tooltip";
import { isUndefined } from "lodash";
import classnames from "classnames";
@ -20,9 +19,9 @@ import Button from "components/buttons/Button";
import Checkbox from "components/forms/fields/Checkbox";
import Spinner from "components/Spinner";
import AutoSizeInputField from "components/forms/fields/AutoSizeInputField";
import TooltipWrapper from "components/TooltipWrapper";
import NewPolicyModal from "../NewPolicyModal";
import InfoIcon from "../../../../../../assets/images/icon-info-purple-14x14@2x.png";
import QuestionIcon from "../../../../../../assets/images/icon-question-16x16@2x.png";
import PencilIcon from "../../../../../../assets/images/icon-pencil-14x14@2x.png";
const baseClass = "policy-form";
@ -372,31 +371,9 @@ const PolicyForm = ({
<>
<b>Checks on:</b>
<span className="platforms-text">
{displayPlatforms.join(", ")}
</span>
<span className={`tooltip`}>
<span
className={`tooltip__tooltip-icon`}
data-tip
data-for="query-compatibility-tooltip"
data-tip-disable={false}
>
<img alt="question icon" src={QuestionIcon} />
</span>
<ReactTooltip
place="bottom"
type="dark"
effect="solid"
backgroundColor="#3e4771"
id="query-compatibility-tooltip"
data-html
>
<span className={`tooltip__tooltip-text`}>
To choose new platforms,
<br />
please create a new policy.
</span>
</ReactTooltip>
<TooltipWrapper tipContent="To choose new platforms, please create a new policy.">
{displayPlatforms.join(", ")}
</TooltipWrapper>
</span>
</>
) : (

View file

@ -4,8 +4,12 @@
import React from "react";
import ReactTooltip from "react-tooltip";
import formatDistanceToNow from "date-fns/formatDistanceToNow";
import PATHS from "router/paths";
import permissionsUtils from "utilities/permissions";
import { IQuery } from "interfaces/query";
import { IUser } from "interfaces/user";
import { addGravatarUrlToResource } from "fleet/helpers";
// @ts-ignore
import Avatar from "components/Avatar";
@ -15,13 +19,7 @@ import HeaderCell from "components/TableContainer/DataTable/HeaderCell/HeaderCel
import PlatformCell from "components/TableContainer/DataTable/PlatformCell";
import TextCell from "components/TableContainer/DataTable/TextCell";
import PillCell from "components/TableContainer/DataTable/PillCell";
import PATHS from "router/paths";
import { IQuery } from "interfaces/query";
import { IUser } from "interfaces/user";
import { addGravatarUrlToResource } from "fleet/helpers";
import QuestionIcon from "../../../../../../assets/images/icon-question-16x16@2x.png";
import TooltipWrapper from "components/TooltipWrapper";
interface IQueryRow {
id: string;
@ -109,33 +107,18 @@ const generateTableHeaders = (currentUser: IUser): IDataColumn[] => {
title: "Performance impact",
Header: () => {
return (
<div>
<div className="column-with-tooltip">
<span className="queries-table__performance-impact-header">
Performance impact
</span>
<span
data-tip
data-for="queries-table__performance-impact-tooltip"
data-tip-disable={false}
>
<img alt="question icon" src={QuestionIcon} />
</span>
<ReactTooltip
className="queries-table__performance-impact-tooltip"
place="bottom"
type="dark"
effect="solid"
backgroundColor="#3e4771"
id="queries-table__performance-impact-tooltip"
data-html
>
<div style={{ textAlign: "center" }}>
<TooltipWrapper
tipContent={`
This is the average <br />
performance impact <br />
across all hosts where this <br />
query was scheduled.
</div>
</ReactTooltip>
query was scheduled.`}
>
Performance impact
</TooltipWrapper>
</span>
</div>
);
},

View file

@ -41,6 +41,12 @@
.queries-table__performance-impact-tooltip {
font-weight: 400;
}
.column-with-tooltip {
height: 0;
position: relative;
top: -10px;
}
}
.no-queries {

View file

@ -1,8 +1,8 @@
import React from "react";
import ReactTooltip from "react-tooltip";
import TooltipWrapper from "components/TooltipWrapper";
import CompatibleIcon from "../../../../../../assets/images/icon-compatible-green-16x16@2x.png";
import IncompatibleIcon from "../../../../../../assets/images/icon-incompatible-red-16x16@2x.png";
import QuestionIcon from "../../../../../../assets/images/icon-question-16x16@2x.png";
const baseClass = "platform-compatibility";
@ -40,33 +40,11 @@ const PlatformCompatibility = ({
compatiblePlatforms = formatPlatformsForDisplay(compatiblePlatforms);
return (
<span className={baseClass}>
<b>Compatible with:</b>
<span className={`tooltip`}>
<span
className={`tooltip__tooltip-icon`}
data-tip
data-for="query-compatibility-tooltip"
data-tip-disable={false}
>
<img alt="question icon" src={QuestionIcon} />
</span>
<ReactTooltip
place="bottom"
type="dark"
effect="solid"
backgroundColor="#3e4771"
id="query-compatibility-tooltip"
data-html
>
<span className={`tooltip__tooltip-text`}>
Estimated compatiblity
<br />
based on the tables used
<br />
in the query
</span>
</ReactTooltip>
</span>
<b>
<TooltipWrapper tipContent="Estimated compatiblity based on the tables used in the query">
Compatible with:
</TooltipWrapper>
</b>
{displayIncompatibilityText(compatiblePlatforms) ||
(!!compatiblePlatforms.length &&
DISPLAY_ORDER.map((platform) => {

View file

@ -31,8 +31,8 @@ import paths from "router/paths";
import Button from "components/buttons/Button";
import Spinner from "components/Spinner";
import TeamsDropdown from "components/TeamsDropdown";
import IconToolTip from "components/IconToolTip";
import TableDataError from "components/TableDataError";
import TooltipWrapper from "components/TooltipWrapper";
import ScheduleListWrapper from "./components/ScheduleListWrapper";
import ScheduleEditorModal from "./components/ScheduleEditorModal";
import RemoveScheduledQueryModal from "./components/RemoveScheduledQueryModal";
@ -526,30 +526,24 @@ const ManageSchedulePage = ({
{selectedTeamId &&
inheritedScheduledQueriesList &&
inheritedScheduledQueriesList.length > 0 ? (
<>
<span>
<Button
variant="unstyled"
className={`${showInheritedQueries ? "upcarat" : "rightcarat"}
${baseClass}__inherited-queries-button`}
onClick={toggleInheritedQueries}
<span>
<Button
variant="unstyled"
className={`${showInheritedQueries ? "upcarat" : "rightcarat"}
${baseClass}__inherited-queries-button`}
onClick={toggleInheritedQueries}
>
<TooltipWrapper
tipContent={
'Queries from the "All teams"<br/>schedule run on this teams hosts.'
}
>
{showInheritedQueries
? `Hide ${inheritedScheduledQueriesList.length} inherited ${inheritedQueryOrQueries}`
: `Show ${inheritedScheduledQueriesList.length} inherited ${inheritedQueryOrQueries}`}
</Button>
</span>
<div className={`${baseClass}__details`}>
<IconToolTip
isHtml
text={
"\
<center><p>Queries from the All teams<br/>schedule run on this teams hosts.</p></center>\
"
}
/>
</div>
</>
</TooltipWrapper>
</Button>
</span>
) : null}
{showInheritedQueries &&
inheritedScheduledQueriesList &&

View file

@ -3,11 +3,9 @@
import React from "react";
import { syntaxHighlight } from "fleet/helpers";
import ReactTooltip from "react-tooltip";
import Modal from "components/Modal";
import Button from "components/buttons/Button";
import QuestionIcon from "../../../../../../assets/images/icon-question-16x16@2x.png";
import TooltipWrapper from "components/TooltipWrapper";
const baseClass = "preview-data-modal";
@ -40,32 +38,12 @@ const PreviewDataModal = ({
<Modal title={"Example data"} onExit={onCancel} className={baseClass}>
<div className={`${baseClass}__preview-modal`}>
<p>
The data sent to your configured log destination will look similar to
the following JSON:{" "}
<span
className={`tooltip__tooltip-icon`}
data-tip
data-for={"preview-tooltip"}
data-tip-disable={false}
<TooltipWrapper
tipContent={`The &quot;snapshot&quot; key includes the query&apos;s results. These will be unique to your query.`}
>
<img alt="preview schedule" src={QuestionIcon} />
</span>
<ReactTooltip
place="bottom"
type="dark"
effect="solid"
backgroundColor="#3e4771"
id={"preview-tooltip"}
data-html
>
<span className={`software-name tooltip__tooltip-text`}>
<p>
The &quot;snapshot&quot; key includes the query&apos;s
<br />
results. These will be unique to your query.
</p>
</span>
</ReactTooltip>
The data sent to your configured log destination will look similar
to the following JSON:
</TooltipWrapper>
</p>
<div className={`${baseClass}__host-status-webhook-preview`}>
<pre dangerouslySetInnerHTML={{ __html: syntaxHighlight(json) }} />

View file

@ -2,7 +2,6 @@
// disable this rule as it was throwing an error in Header and Cell component
// definitions for the selection row for some reason when we dont really need it.
import React from "react";
import ReactTooltip from "react-tooltip";
import { performanceIndicator, secondsToDhms } from "fleet/helpers";
// @ts-ignore
@ -13,7 +12,7 @@ import PillCell from "components/TableContainer/DataTable/PillCell";
import { IDropdownOption } from "interfaces/dropdownOption";
import { IGlobalScheduledQuery } from "interfaces/global_scheduled_query";
import { ITeamScheduledQuery } from "interfaces/team_scheduled_query";
import QuestionIcon from "../../../../../../assets/images/icon-question-16x16@2x.png";
import TooltipWrapper from "components/TooltipWrapper";
interface IGetToggleAllRowsSelectedProps {
checked: boolean;
@ -111,33 +110,18 @@ const generateTableHeaders = (
title: "Performance impact",
Header: () => {
return (
<div>
<div className="column-with-tooltip">
<span className="queries-table__performance-impact-header">
Performance impact
</span>
<span
data-tip
data-for="queries-table__performance-impact-tooltip"
data-tip-disable={false}
>
<img alt="question icon" src={QuestionIcon} />
</span>
<ReactTooltip
className="queries-table__performance-impact-tooltip"
place="bottom"
type="dark"
effect="solid"
backgroundColor="#3e4771"
id="queries-table__performance-impact-tooltip"
data-html
>
<div style={{ textAlign: "center" }}>
<TooltipWrapper
tipContent={`
This is the average <br />
performance impact <br />
across all hosts where this <br />
query was scheduled.
</div>
</ReactTooltip>
query was scheduled.`}
>
Performance impact
</TooltipWrapper>
</span>
</div>
);
},

View file

@ -77,6 +77,12 @@
.queries-table__performance-impact-tooltip {
font-weight: 400;
}
.column-with-tooltip {
height: 0;
position: relative;
top: -10px;
}
}
.no-schedule {

View file

@ -2,9 +2,7 @@ import React, { useCallback, useContext, useEffect, useState } from "react";
import { useQuery } from "react-query";
import { useDispatch } from "react-redux";
import { InjectedRouter } from "react-router/lib/Router";
import ReactTooltip from "react-tooltip";
import { useDebouncedCallback } from "use-debounce/lib";
import formatDistanceToNowStrict from "date-fns/formatDistanceToNowStrict";
import { AppContext } from "context/app";
import { IConfig, IConfigNested } from "interfaces/config";
@ -33,13 +31,12 @@ import TableDataError from "components/TableDataError";
import TeamsDropdownHeader, {
ITeamsDropdownState,
} from "components/PageHeader/TeamsDropdownHeader";
import ExternalLinkIcon from "../../../../assets/images/open-new-tab-12x12@2x.png";
import QuestionIcon from "../../../../assets/images/icon-question-16x16@2x.png";
import renderLastUpdatedText from "components/LastUpdatedText";
import softwareTableHeaders from "./SoftwareTableConfig";
import ManageAutomationsModal from "./components/ManageAutomationsModal";
import EmptySoftware from "../components/EmptySoftware";
import ExternalLinkIcon from "../../../../assets/images/open-new-tab-12x12@2x.png";
interface IManageSoftwarePageProps {
router: InjectedRouter;
@ -320,11 +317,7 @@ const ManageSoftwarePage = ({
const renderSoftwareCount = useCallback(() => {
const count = softwareCount;
const lastUpdatedAt = software?.counts_updated_at
? formatDistanceToNowStrict(new Date(software?.counts_updated_at), {
addSuffix: true,
})
: software?.counts_updated_at;
const lastUpdatedAt = software?.counts_updated_at;
if (!isSoftwareEnabled || !lastUpdatedAt) {
return null;
@ -339,44 +332,20 @@ const ManageSoftwarePage = ({
}
// TODO: Use setInterval to keep last updated time current?
return count !== undefined ? (
<span
className={`${baseClass}__count ${
isFetchingCount ? "count-loading" : ""
}`}
>
{`${count} software item${count === 1 ? "" : "s"}`}
<span className="count-last-updated">
{`Last updated ${lastUpdatedAt}`}{" "}
<span className={`tooltip`}>
<span
className={`tooltip__tooltip-icon`}
data-tip
data-for="last-updated-tooltip"
data-tip-disable={false}
>
<img alt="question icon" src={QuestionIcon} />
</span>
<ReactTooltip
place="top"
type="dark"
effect="solid"
backgroundColor="#3e4771"
id="last-updated-tooltip"
data-html
>
<span className={`tooltip__tooltip-text`}>
Fleet periodically
<br />
queries all hosts
<br />
to retrieve software
</span>
</ReactTooltip>
</span>
</span>
</span>
) : null;
if (count) {
return (
<div
className={`${baseClass}__count ${
isFetchingCount ? "count-loading" : ""
}`}
>
<span>{`${count} software item${count === 1 ? "" : "s"}`}</span>
{renderLastUpdatedText(lastUpdatedAt, "software")}
</div>
);
}
return null;
}, [isFetchingCount, software, softwareCountError, softwareCount]);
// TODO: retool this with react-router location descriptor objects

View file

@ -158,28 +158,17 @@
}
}
&__count {
display: flex;
:first-child {
margin-right: $pad-small;
}
.count-error {
color: $ui-error;
}
.count-loading {
color: $ui-fleet-black-50;
}
.count-last-updated {
color: $ui-fleet-black-75;
font-size: $xx-small;
font-weight: normal;
padding-left: $pad-small;
.tooltip__tooltip-text {
display: flex;
text-align: center;
}
img {
height: 16px;
width: 16px;
vertical-align: text-top;
}
}
}
&__status_dropdown {

View file

@ -6,7 +6,6 @@ import Button from "components/buttons/Button";
import Checkbox from "components/forms/fields/Checkbox";
// @ts-ignore
import InputField from "components/forms/fields/InputField";
import IconToolTip from "components/IconToolTip";
import validURL from "components/forms/validators/valid_url";
import { IWebhookSoftwareVulnerabilities } from "interfaces/webhook";
@ -210,10 +209,7 @@ const ManageAutomationsModal = ({
"For each new vulnerability detected, Fleet will send a JSON payload to this URL with a list of the affected hosts."
}
placeholder={"https://server.com/example"}
/>
<IconToolTip
isHtml
text={"<p>Provide a URL to deliver a<br />webhook request to.</p>"}
tooltip="Provide a URL to deliver a webhook request to."
/>
</div>
<Button