+
);
diff --git a/frontend/components/LastUpdatedText/LastUpdatedText.tests.tsx b/frontend/components/LastUpdatedText/LastUpdatedText.tests.tsx
new file mode 100644
index 0000000000..933c225760
--- /dev/null
+++ b/frontend/components/LastUpdatedText/LastUpdatedText.tests.tsx
@@ -0,0 +1,38 @@
+import React from "react";
+import { render, screen } from "@testing-library/react";
+import { renderWithSetup } from "test/testingUtils";
+
+import LastUpdatedText from "./LastUpdatedText";
+
+describe("Last updated text", () => {
+ it("renders updated text", () => {
+ const currentDate = new Date();
+ currentDate.setDate(currentDate.getDate() - 2);
+ const twoDaysAgo = currentDate.toISOString();
+
+ render(
+
+ );
+
+ const text = screen.getByText("Updated 2 days ago");
+
+ expect(text).toBeInTheDocument();
+ });
+ it("renders never if missing timestamp", () => {
+ render(
);
+
+ const text = screen.getByText("Updated never");
+
+ expect(text).toBeInTheDocument();
+ });
+
+ it("renders tooltip on hover", async () => {
+ const { user } = renderWithSetup(
+
+ );
+
+ await user.hover(screen.getByText("Updated never"));
+
+ expect(screen.getByText(/to retrieve software/i)).toBeInTheDocument();
+ });
+});
diff --git a/frontend/components/LastUpdatedText/LastUpdatedText.tsx b/frontend/components/LastUpdatedText/LastUpdatedText.tsx
index 75031ac983..542bea6b91 100644
--- a/frontend/components/LastUpdatedText/LastUpdatedText.tsx
+++ b/frontend/components/LastUpdatedText/LastUpdatedText.tsx
@@ -7,7 +7,7 @@ import TooltipWrapper from "components/TooltipWrapper";
const baseClass = "component__last-updated-text";
interface ILastUpdatedTextProps {
- lastUpdatedAt: string;
+ lastUpdatedAt?: string;
whatToRetrieve: string;
}
const LastUpdatedText = ({
diff --git a/frontend/components/PlatformCompatibility/PlatformCompatibility.tests.tsx b/frontend/components/PlatformCompatibility/PlatformCompatibility.tests.tsx
new file mode 100644
index 0000000000..b0fc1c950a
--- /dev/null
+++ b/frontend/components/PlatformCompatibility/PlatformCompatibility.tests.tsx
@@ -0,0 +1,50 @@
+import React from "react";
+import { render, screen } from "@testing-library/react";
+
+import PlatformCompatibility from "./PlatformCompatibility";
+
+describe("Platform compatibility", () => {
+ it("renders compatible platforms", () => {
+ render(
+
+ );
+ const macCompatibility = screen.getByText("macOS").firstElementChild;
+ const windowsCompatibility = screen.getByText("Windows").firstElementChild;
+ const linuxCompatibility = screen.getByText("Linux").firstElementChild;
+
+ expect(macCompatibility).toHaveAttribute(
+ "class",
+ "icon compatible-platform"
+ );
+ expect(windowsCompatibility).toHaveAttribute(
+ "class",
+ "icon compatible-platform"
+ );
+ expect(linuxCompatibility).toHaveAttribute(
+ "class",
+ "icon incompatible-platform"
+ );
+ });
+ it("renders empty state", () => {
+ render(
);
+
+ const text = screen.getByText(/No platforms/i);
+
+ expect(text).toBeInTheDocument();
+ });
+ it("renders error state", () => {
+ render(
+
+ );
+
+ const text = screen.getByText(/possible syntax error/i);
+
+ expect(text).toBeInTheDocument();
+ });
+});
diff --git a/frontend/components/PlatformCompatibility/PlatformCompatibility.tsx b/frontend/components/PlatformCompatibility/PlatformCompatibility.tsx
index 43a844cb12..cc44ddc592 100644
--- a/frontend/components/PlatformCompatibility/PlatformCompatibility.tsx
+++ b/frontend/components/PlatformCompatibility/PlatformCompatibility.tsx
@@ -4,8 +4,7 @@ import { IOsqueryPlatform } from "interfaces/platform";
import { PLATFORM_DISPLAY_NAMES } from "utilities/constants";
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 Icon from "components/Icon";
interface IPlatformCompatibilityProps {
compatiblePlatforms: IOsqueryPlatform[] | null;
@@ -51,7 +50,10 @@ const PlatformCompatibility = ({
return (
-
+
Compatible with:
@@ -79,9 +81,12 @@ const PlatformCompatibility = ({
key={`platform-compatibility__${platform}`}
className="platform"
>
-
{platform}
diff --git a/frontend/components/PlatformCompatibility/_styles.scss b/frontend/components/PlatformCompatibility/_styles.scss
index 606371b3bf..7b13d52071 100644
--- a/frontend/components/PlatformCompatibility/_styles.scss
+++ b/frontend/components/PlatformCompatibility/_styles.scss
@@ -5,7 +5,7 @@
padding-top: $pad-medium;
b,
- img,
+ svg,
span {
display: flex;
align-items: center;
@@ -17,12 +17,10 @@
.platform {
padding-left: 0px;
- }
- img {
- height: 16px;
- width: 16px;
- padding-left: 12px;
- padding-right: $pad-xsmall;
+ .icon {
+ padding-left: 12px;
+ padding-right: $pad-xsmall;
+ }
}
}
diff --git a/frontend/components/TableContainer/DataTable/DropdownCell/DropdownCell.tests.tsx b/frontend/components/TableContainer/DataTable/DropdownCell/DropdownCell.tests.tsx
new file mode 100644
index 0000000000..42591b0e0f
--- /dev/null
+++ b/frontend/components/TableContainer/DataTable/DropdownCell/DropdownCell.tests.tsx
@@ -0,0 +1,33 @@
+import React from "react";
+import { screen } from "@testing-library/react";
+import { renderWithSetup } from "test/testingUtils";
+
+import DropdownCell from "./DropdownCell";
+
+const DROPDOWN_OPTIONS = [
+ { disabled: false, label: "Edit", value: "edit-query" },
+ { disabled: false, label: "Show query", value: "show-query" },
+ { disabled: true, label: "Delete", value: "delete-query" },
+];
+const PLACEHOLDER = "Actions";
+const ON_CHANGE = (value: string) => {
+ console.log(value);
+};
+
+describe("Dropdown cell", () => {
+ it("renders dropdown placeholder and options", async () => {
+ const { user } = renderWithSetup(
+
+ );
+
+ await user.click(screen.getByText("Actions"));
+
+ expect(screen.getByText(/edit/i)).toBeInTheDocument();
+ expect(screen.getByText(/show query/i)).toBeInTheDocument();
+ expect(screen.getByText(/delete/i)).toBeInTheDocument();
+ });
+});
diff --git a/frontend/components/TableContainer/DataTable/IssueCell/IssueCell.tests.tsx b/frontend/components/TableContainer/DataTable/IssueCell/IssueCell.tests.tsx
new file mode 100644
index 0000000000..a9dff7c198
--- /dev/null
+++ b/frontend/components/TableContainer/DataTable/IssueCell/IssueCell.tests.tsx
@@ -0,0 +1,28 @@
+import React from "react";
+import { screen } from "@testing-library/react";
+import { createCustomRenderer } from "test/test-utils";
+
+import IssueCell from "./IssueCell";
+
+describe("Issue cell", () => {
+ it("renders icon, total issues, and failing policies tooltip", async () => {
+ const render = createCustomRenderer({});
+
+ const { user } = render(
+
+ );
+
+ const icon = screen.queryByTestId("icon");
+
+ await user.hover(screen.getByText("4"));
+
+ expect(screen.getByText(/failing policies/i)).toBeInTheDocument();
+ expect(icon).toBeInTheDocument();
+ });
+});
diff --git a/frontend/components/TableContainer/DataTable/IssueCell/IssueCell.tsx b/frontend/components/TableContainer/DataTable/IssueCell/IssueCell.tsx
index bcd2d7d8d4..ef58aeacc6 100644
--- a/frontend/components/TableContainer/DataTable/IssueCell/IssueCell.tsx
+++ b/frontend/components/TableContainer/DataTable/IssueCell/IssueCell.tsx
@@ -2,6 +2,8 @@ import React from "react";
import ReactTooltip from "react-tooltip";
import { isEmpty } from "lodash";
+import Icon from "components/Icon";
+
import IssueIcon from "../../../../../assets/images/icon-issue-fleet-black-50-16x16@2x.png";
interface IIssueCellProps
{
@@ -25,7 +27,7 @@ const IssueCell = ({ issues, rowId }: IIssueCellProps): JSX.Element => {
data-for={`host-issue__${rowId.toString()}`}
data-tip-disable={false}
>
-
+
{
+ it("renders text and path", async () => {
+ const { user } = renderWithSetup(
+
+ );
+
+ await user.click(screen.getByText("40 hosts"));
+
+ expect(window.location.pathname).toContain("/hosts");
+ });
+});
diff --git a/frontend/components/TableContainer/DataTable/PillCell/PillCell.tests.tsx b/frontend/components/TableContainer/DataTable/PillCell/PillCell.tests.tsx
new file mode 100644
index 0000000000..7e14065e73
--- /dev/null
+++ b/frontend/components/TableContainer/DataTable/PillCell/PillCell.tests.tsx
@@ -0,0 +1,19 @@
+import React from "react";
+import { screen } from "@testing-library/react";
+import { renderWithSetup } from "test/testingUtils";
+
+import PillCell from "./PillCell";
+
+const PERFORMANCE_IMPACT = { indicator: "Minimal", id: 3 };
+
+describe("Pill cell", () => {
+ it("renders pill text and tooltip on hover", async () => {
+ const { user } = renderWithSetup(
+
+ );
+
+ await user.hover(screen.getByText("Minimal"));
+
+ expect(screen.getByText(/little to no impact/i)).toBeInTheDocument();
+ });
+});
diff --git a/frontend/components/TableContainer/DataTable/PillCell/PillCell.tsx b/frontend/components/TableContainer/DataTable/PillCell/PillCell.tsx
index 7ef21d221d..727fa70637 100644
--- a/frontend/components/TableContainer/DataTable/PillCell/PillCell.tsx
+++ b/frontend/components/TableContainer/DataTable/PillCell/PillCell.tsx
@@ -1,11 +1,11 @@
import React from "react";
import classnames from "classnames";
-import { v4 as uuidv4 } from "uuid";
+import { uniqueId } from "lodash";
import ReactTooltip from "react-tooltip";
interface IPillCellProps {
- value: [string, number];
+ value: { indicator: string; id: number };
customIdPrefix?: string;
hostDetails?: boolean;
}
@@ -19,16 +19,15 @@ const PillCell = ({
customIdPrefix,
hostDetails,
}: IPillCellProps): JSX.Element => {
- const [pillText, id] = value;
-
+ const { indicator, id } = value;
const pillClassName = classnames(
"data-table__pill",
- `data-table__pill--${generateClassTag(pillText)}`,
+ `data-table__pill--${generateClassTag(indicator || "")}`,
"tooltip"
);
const disable = () => {
- switch (pillText) {
+ switch (indicator) {
case "Minimal":
return false;
case "Considerable":
@@ -43,7 +42,7 @@ const PillCell = ({
};
const tooltipText = () => {
- switch (pillText) {
+ switch (indicator) {
case "Minimal":
return (
<>
@@ -85,25 +84,30 @@ const PillCell = ({
return null;
}
};
+ const tooltipId = uniqueId();
return (
<>
- {pillText}
+ {indicator}
-
+
{tooltipText()}
diff --git a/frontend/components/TableContainer/DataTable/PlatformCell/PlatformCell.tests.tsx b/frontend/components/TableContainer/DataTable/PlatformCell/PlatformCell.tests.tsx
new file mode 100644
index 0000000000..08f059e92b
--- /dev/null
+++ b/frontend/components/TableContainer/DataTable/PlatformCell/PlatformCell.tests.tsx
@@ -0,0 +1,31 @@
+import React from "react";
+import { getByTestId, render, screen, within } from "@testing-library/react";
+
+import PlatformCell from "./PlatformCell";
+
+const PLATFORMS = ["windows", "darwin", "linux"];
+
+describe("Platform cell", () => {
+ it("renders platform icons in correct order", () => {
+ render();
+
+ const icons = screen.queryAllByTestId("icon");
+ const appleIcon = screen.queryByTestId("apple-icon");
+ const linuxIcon = screen.queryByTestId("linux-icon");
+ const windowsIcon = screen.queryByTestId("windows-icon");
+
+ expect(icons).toHaveLength(3);
+ expect(icons[0].firstChild).toBe(appleIcon);
+ expect(icons[1].firstChild).toBe(linuxIcon);
+ expect(icons[2].firstChild).toBe(windowsIcon);
+ });
+ it("renders empty state", () => {
+ render();
+
+ const icons = screen.queryAllByTestId("icon");
+ const emptyText = screen.queryByText("---");
+
+ expect(icons).toHaveLength(0);
+ expect(emptyText).toBeInTheDocument();
+ });
+});
diff --git a/frontend/components/TableContainer/DataTable/PlatformCell/PlatformCell.tsx b/frontend/components/TableContainer/DataTable/PlatformCell/PlatformCell.tsx
index 42d09e962b..5a2925a4ac 100644
--- a/frontend/components/TableContainer/DataTable/PlatformCell/PlatformCell.tsx
+++ b/frontend/components/TableContainer/DataTable/PlatformCell/PlatformCell.tsx
@@ -36,6 +36,7 @@ const PlatformCell = ({
className={`${baseClass}__icon`}
name={ICONS[platform]}
size="small"
+ key={ICONS[platform]}
/>
) : null;
})
diff --git a/frontend/components/TableContainer/DataTable/TruncatedTextCell/TruncatedTextCell.tsx b/frontend/components/TableContainer/DataTable/TruncatedTextCell/TruncatedTextCell.tsx
index 110b3b5b13..f61a7bd6f0 100644
--- a/frontend/components/TableContainer/DataTable/TruncatedTextCell/TruncatedTextCell.tsx
+++ b/frontend/components/TableContainer/DataTable/TruncatedTextCell/TruncatedTextCell.tsx
@@ -1,5 +1,5 @@
import React, { useState, useRef, useLayoutEffect } from "react";
-import { v4 as uuidv4 } from "uuid";
+import { uniqueId } from "lodash";
import ReactTooltip from "react-tooltip";
@@ -26,7 +26,7 @@ const TruncatedTextCell = ({
}
}, []);
- const id = uuidv4();
+ const tooltipId = uniqueId();
const tooltipDisabled = offsetWidth === scrollWidth;
return (
@@ -34,7 +34,7 @@ const TruncatedTextCell = ({
diff --git a/frontend/components/ViewAllHostsLink/ViewAllHostsLink.tests.tsx b/frontend/components/ViewAllHostsLink/ViewAllHostsLink.tests.tsx
index 546b93201f..d515bec424 100644
--- a/frontend/components/ViewAllHostsLink/ViewAllHostsLink.tests.tsx
+++ b/frontend/components/ViewAllHostsLink/ViewAllHostsLink.tests.tsx
@@ -7,7 +7,7 @@ describe("ViewAllHostsLink - component", () => {
render();
const text = screen.getByText("View all hosts");
- const icon = screen.getByTestId("Icon");
+ const icon = screen.getByTestId("icon");
expect(text).toBeInTheDocument();
expect(icon).toBeInTheDocument();
diff --git a/frontend/components/buttons/RevealButton/RevealButton.tests.tsx b/frontend/components/buttons/RevealButton/RevealButton.tests.tsx
new file mode 100644
index 0000000000..1c1fb242be
--- /dev/null
+++ b/frontend/components/buttons/RevealButton/RevealButton.tests.tsx
@@ -0,0 +1,92 @@
+import React from "react";
+import { render, screen } from "@testing-library/react";
+import { renderWithSetup } from "test/testingUtils";
+
+import RevealButton from "./RevealButton";
+
+const SHOW_TEXT = "Show advanced options";
+const HIDE_TEXT = "Hide advanced options";
+const TOOLTIP_HTML = "Customize logging type and platforms";
+
+describe("Reveal button", () => {
+ it("renders show text", async () => {
+ render(
+
+ );
+
+ const showText = screen.getByText(SHOW_TEXT);
+ expect(showText).toBeInTheDocument();
+ });
+
+ it("renders hide text", async () => {
+ render(
+
+ );
+
+ const hideText = screen.getByText(HIDE_TEXT);
+ expect(hideText).toBeInTheDocument();
+ });
+
+ it("hides caret by default", async () => {
+ render(
+
+ );
+
+ const icon = screen.queryByTestId("icon");
+
+ expect(icon).toBeNull();
+ });
+
+ it("renders caret on left", async () => {
+ render(
+
+ );
+
+ const icon = screen.queryByTestId("icon");
+ expect(icon?.nextSibling).toHaveTextContent(SHOW_TEXT);
+ });
+
+ it("renders caret on right", async () => {
+ render(
+
+ );
+
+ const icon = screen.queryByTestId("icon");
+
+ expect(icon?.previousSibling).toHaveTextContent(SHOW_TEXT);
+ });
+
+ it("renders tooltip on hover if provided", async () => {
+ const { user } = renderWithSetup(
+
+ );
+
+ await user.hover(screen.getByText(SHOW_TEXT));
+
+ expect(screen.getByText(TOOLTIP_HTML)).toBeInTheDocument();
+ });
+});
diff --git a/frontend/components/buttons/RevealButton/RevealButton.tsx b/frontend/components/buttons/RevealButton/RevealButton.tsx
index 459ca936e2..830e56cebf 100644
--- a/frontend/components/buttons/RevealButton/RevealButton.tsx
+++ b/frontend/components/buttons/RevealButton/RevealButton.tsx
@@ -1,10 +1,12 @@
import React from "react";
+import classnames from "classnames";
import Button from "components/buttons/Button";
import TooltipWrapper from "components/TooltipWrapper";
+import Icon from "components/Icon";
export interface IRevealButtonProps {
isShowing: boolean;
- baseClass: string;
+ className?: string;
hideText: string;
showText: string;
caretPosition?: "before" | "after";
@@ -16,8 +18,11 @@ export interface IRevealButtonProps {
| ((evt: React.MouseEvent) => void);
}
+const baseClass = "reveal-button";
+
const RevealButton = ({
isShowing,
+ className,
hideText,
showText,
caretPosition,
@@ -26,31 +31,47 @@ const RevealButton = ({
tooltipHtml,
onClick,
}: IRevealButtonProps): JSX.Element => {
- const classNameGenerator = () => {
- if (caretPosition === "before") {
- return isShowing ? "reveal upcaretbefore" : "reveal rightcaretbefore";
- }
- if (caretPosition === "after") {
- return isShowing ? "reveal upcaretafter" : "reveal downcaretafter";
- }
- return "reveal";
- };
+ const classNames = classnames(baseClass, className);
- const buttonText = isShowing ? hideText : showText;
+ const buttonContent = () => {
+ const text = isShowing ? hideText : showText;
+
+ const buttonText = tooltipHtml ? (
+ {text}
+ ) : (
+ text
+ );
+
+ return (
+ <>
+ {caretPosition === "before" && (
+
+ )}
+ {buttonText}
+ {caretPosition === "after" && (
+
+ )}
+ >
+ );
+ };
return (
);
};
diff --git a/frontend/components/buttons/RevealButton/_styles.scss b/frontend/components/buttons/RevealButton/_styles.scss
index 6652ec01c6..b31155aef1 100644
--- a/frontend/components/buttons/RevealButton/_styles.scss
+++ b/frontend/components/buttons/RevealButton/_styles.scss
@@ -1,54 +1,6 @@
-.reveal {
- margin: $pad-medium 0 $pad-large;
- color: $core-vibrant-blue;
- font-weight: $bold;
- font-size: $x-small;
-}
-
-.rightcaretbefore {
- &::before {
- content: url("../assets/images/icon-chevron-blue-16x16@2x.png");
- transform: scale(0.5) rotate(-90deg);
- width: 16px;
- padding: 0px;
- padding-right: 10px;
- margin-top: 5px;
- }
-}
-
-.upcaretbefore {
- &::before {
- content: url("../assets/images/icon-chevron-blue-16x16@2x.png");
- transform: scale(0.5) rotate(180deg);
- width: 16px;
- padding: 0px;
- padding-right: 2px;
- margin-right: $pad-small;
- margin-top: 5px;
- position: relative;
- top: -4px;
- left: 6px;
- }
-}
-
-.downcaretafter {
- &::after {
- content: url("../assets/images/icon-chevron-blue-16x16@2x.png");
- transform: scale(0.5);
- width: 16px;
- padding: 0px;
- padding-left: 2px;
- margin-bottom: 2px;
- }
-}
-
-.upcaretafter {
- &::after {
- content: url("../assets/images/icon-chevron-blue-16x16@2x.png");
- transform: scale(0.5) rotate(180deg);
- width: 16px;
- padding: 0px;
- margin-bottom: 2px;
- margin-left: 11px;
- }
+.reveal-button .children-wrapper {
+ display: inline-flex;
+ align-items: center;
+ padding: $pad-small $pad-xxsmall; // larger clickable area
+ gap: $pad-xsmall;
}
diff --git a/frontend/components/forms/fields/SelectTargetsDropdown/SelectTargetsInput/SelectTargetsInput.jsx b/frontend/components/forms/fields/SelectTargetsDropdown/SelectTargetsInput/SelectTargetsInput.jsx
index 960f357290..d5963978fd 100644
--- a/frontend/components/forms/fields/SelectTargetsDropdown/SelectTargetsInput/SelectTargetsInput.jsx
+++ b/frontend/components/forms/fields/SelectTargetsDropdown/SelectTargetsInput/SelectTargetsInput.jsx
@@ -1,9 +1,8 @@
import React, { Component } from "react";
import PropTypes from "prop-types";
-import { difference, isEqual } from "lodash";
+import { difference, isEqual, uniqueId } from "lodash";
import Select from "react-select";
import "react-select/dist/react-select.css";
-import { v4 as uuidv4 } from "uuid";
import debounce from "utilities/debounce";
import targetInterface from "interfaces/target";
@@ -43,7 +42,7 @@ class SelectTargetsInput extends Component {
// must have unique key to select correctly
const uuidTargets = targets.map((target) => ({
...target,
- uuid: uuidv4(),
+ uuid: uniqueId(),
}));
this.setState({ uuidTargets });
@@ -53,7 +52,7 @@ class SelectTargetsInput extends Component {
// must have unique key to deselect correctly
const uuidSelectedTargets = selectedTargets.map((target) => ({
...target,
- uuid: uuidv4(),
+ uuid: uniqueId(),
}));
this.setState({ uuidSelectedTargets });
diff --git a/frontend/components/icons/Apple.tsx b/frontend/components/icons/Apple.tsx
index a6d54e7d2f..68f8bf2199 100644
--- a/frontend/components/icons/Apple.tsx
+++ b/frontend/components/icons/Apple.tsx
@@ -23,6 +23,7 @@ const Apple = ({
fill="none"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
+ data-testid="apple-icon"
>
{
+const Check = ({ color = "core-fleet-blue" }: ICheckProps) => {
return (
setShowQueryEditor(!showQueryEditor)}
diff --git a/frontend/pages/schedule/ManageSchedulePage/ManageSchedulePage.tsx b/frontend/pages/schedule/ManageSchedulePage/ManageSchedulePage.tsx
index 3bebdccf81..3c6d2adf04 100644
--- a/frontend/pages/schedule/ManageSchedulePage/ManageSchedulePage.tsx
+++ b/frontend/pages/schedule/ManageSchedulePage/ManageSchedulePage.tsx
@@ -536,7 +536,7 @@ const ManageSchedulePage = ({
inheritedScheduledQueriesList.length > 0 ? (