Fleet UI: Merge queries/policies tests and polish (#18737)

This commit is contained in:
RachelElysia 2024-05-06 12:03:00 -04:00 committed by RachelElysia
parent eb7ac35071
commit 486657d08e
11 changed files with 459 additions and 138 deletions

View file

@ -15,7 +15,7 @@ const InheritedBadge = ({
}: IInheritedBadgeProps) => {
const tooltipId = uniqueId();
return (
<div className={`${baseClass}`}>
<div className={baseClass}>
<span
className={`${baseClass}__element-text`}
data-tooltip-id={tooltipId}

View file

@ -112,7 +112,7 @@ interface IManageHostsProps {
router: InjectedRouter;
params: Params;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
location: any; // no type in react-router v3
location: any; // no type in react-router v3 TODO: Improve this type
}
const CSV_HOSTS_TITLE = "Hosts";

View file

@ -664,7 +664,6 @@ const ManagePolicyPage = ({
null
}
isPremiumTier={isPremiumTier}
isSandboxMode={isSandboxMode}
searchQuery={searchQuery}
sortHeader={sortHeader}
sortDirection={sortDirection}
@ -684,7 +683,6 @@ const ManagePolicyPage = ({
currentTeam={currentTeamSummary}
currentAutomatedPolicies={currentAutomatedPolicies}
isPremiumTier={isPremiumTier}
isSandboxMode={isSandboxMode}
renderPoliciesCount={() =>
(!isFetchingGlobalCount &&
renderPoliciesCount(globalPoliciesCount)) ||

View file

@ -193,30 +193,31 @@
}
}
.critical-badge,
.inherited-badge {
display: inline-flex;
}
.inherited-badge {
display: flex;
padding: 4px;
justify-content: center;
align-items: center;
gap: 4px;
font-weight: $bold;
font-size: $xxx-small;
color: $core-fleet-black;
border-radius: 4px;
background: $ui-vibrant-blue-10;
}
.policy-name-text {
text-overflow: ellipsis;
overflow: hidden;
}
}
}
.critical-badge,
.policy-has-not-run {
.critical-badge-icon {
display: inline-flex;
}
@include tooltip5-arrow-styles;
.react-tooltip {
@include tooltip-text;
font-style: normal;
text-align: center;
}
}
.inherited-badge {
overflow: initial;
}
}
}
}

View file

@ -1,23 +1,31 @@
import React from "react";
import { render, screen } from "@testing-library/react";
import { screen, waitFor } from "@testing-library/react";
import { noop } from "lodash";
import { createCustomRenderer } from "test/test-utils";
import createMockUser from "__mocks__/userMock";
import createMockPolicy from "__mocks__/policyMock";
import PoliciesTable from "./PoliciesTable";
describe("Policies table", () => {
const testCriticalPolicy = createMockPolicy({ critical: true });
it("Renders the page-wide empty state when no policies are present", async () => {
const render = createCustomRenderer({
context: {
app: {
isGlobalAdmin: true,
currentUser: createMockUser(),
},
},
});
it("Renders a tooltip including 'Premium feature' copy for a critical policy in Sandbox mode", () => {
render(
<PoliciesTable
policiesList={[testCriticalPolicy]}
policiesList={[]}
isLoading={false}
// eslint-disable-next-line @typescript-eslint/no-empty-function
onDeletePolicyClick={() => {}}
currentTeam={{ id: -1, name: "All teams" }}
isPremiumTier
isSandboxMode
searchQuery=""
page={0}
onQueryChange={noop}
@ -25,18 +33,52 @@ describe("Policies table", () => {
/>
);
expect(
screen.getByText("This policy has been marked as critical.", {
exact: false,
})
).toBeInTheDocument();
expect(
screen.getByText("This is a premium feature.", { exact: false })
).toBeInTheDocument();
expect(screen.getByText("You don't have any policies")).toBeInTheDocument();
expect(screen.queryByText("Name")).toBeNull();
});
it("Renders a tooltip excluding 'Premium feature' copy for a critical policy not in Sandbox mode", () => {
it("Renders the empty search state when search query exists for server side search with no results", async () => {
const render = createCustomRenderer({
context: {
app: {
isGlobalAdmin: true,
currentUser: createMockUser(),
},
},
});
render(
<PoliciesTable
policiesList={[]}
isLoading={false}
// eslint-disable-next-line @typescript-eslint/no-empty-function
onDeletePolicyClick={() => {}}
currentTeam={{ id: -1, name: "All teams" }}
isPremiumTier
searchQuery="shouldn't match anything"
page={0}
onQueryChange={noop}
renderPoliciesCount={() => null}
/>
);
expect(screen.getByText("No matching policies")).toBeInTheDocument();
expect(screen.queryByText("Name")).toBeNull();
});
it("Renders a critical badge and tooltip for a critical policy", async () => {
const render = createCustomRenderer({
context: {
app: {
isGlobalAdmin: true,
currentUser: createMockUser(),
},
},
});
const testCriticalPolicy = createMockPolicy({ critical: true });
const { user } = render(
<PoliciesTable
policiesList={[testCriticalPolicy]}
isLoading={false}
@ -44,7 +86,6 @@ describe("Policies table", () => {
onDeletePolicyClick={() => {}}
currentTeam={{ id: -1, name: "All teams" }}
isPremiumTier
isSandboxMode={false}
searchQuery=""
page={0}
onQueryChange={noop}
@ -52,13 +93,141 @@ describe("Policies table", () => {
/>
);
expect(
screen.getByText("This policy has been marked as critical.", {
exact: false,
})
).toBeInTheDocument();
expect(
screen.queryByText("This is a premium feature.", { exact: false })
).toBeNull();
await waitFor(() => {
waitFor(() => {
user.hover(screen.getByTestId("policy-icon"));
});
expect(
screen.getByText("This policy has been marked as critical.")
).toBeInTheDocument();
});
});
it("Renders an inherited badge and tooltip for inherited policy on a team's policies page", async () => {
const render = createCustomRenderer({
context: {
app: {
isGlobalAdmin: true,
currentUser: createMockUser(),
},
},
});
const testInheritedPolicy = createMockPolicy({ team_id: null });
const { user } = render(
<PoliciesTable
policiesList={[testInheritedPolicy]}
isLoading={false}
// eslint-disable-next-line @typescript-eslint/no-empty-function
onDeletePolicyClick={() => {}}
currentTeam={{ id: 2, name: "Team 2" }}
isPremiumTier
searchQuery=""
page={0}
onQueryChange={noop}
renderPoliciesCount={() => null}
/>
);
await waitFor(() => {
waitFor(() => {
user.hover(screen.getByText("Inherited"));
});
expect(
screen.getByText("This policy runs on all hosts.")
).toBeInTheDocument();
});
});
it("Does not render an inherited badge and tooltip for global policy on the All teams's policies page", () => {
const render = createCustomRenderer({
context: {
app: {
isGlobalAdmin: true,
currentUser: createMockUser(),
},
},
});
const testGlobalPolicy = createMockPolicy({ team_id: null });
render(
<PoliciesTable
policiesList={[testGlobalPolicy]}
isLoading={false}
// eslint-disable-next-line @typescript-eslint/no-empty-function
onDeletePolicyClick={() => {}}
currentTeam={{ id: -1, name: "All teams" }}
isPremiumTier
searchQuery=""
page={0}
onQueryChange={noop}
renderPoliciesCount={() => null}
/>
);
expect(screen.queryByText("Inherited")).not.toBeInTheDocument();
});
it("Renders the correct number of checkboxes for team policies and not inherited policies on a team's policies page and can check select all box", async () => {
const render = createCustomRenderer({
context: {
app: {
isGlobalAdmin: true,
currentUser: createMockUser(),
},
},
});
const testInheritedPolicies = [
createMockPolicy({ team_id: null, name: "Inherited policy 1" }),
createMockPolicy({ id: 2, team_id: null, name: "Inherited policy 2" }),
createMockPolicy({ id: 3, team_id: null, name: "Inherited policy 3" }),
];
const testTeamPolicies = [
createMockPolicy({ id: 4, team_id: 2, name: "Team policy 1" }),
createMockPolicy({ id: 5, team_id: 2, name: "Team policy 2" }),
];
const { container, user } = render(
<PoliciesTable
policiesList={[...testInheritedPolicies, ...testTeamPolicies]}
isLoading={false}
// eslint-disable-next-line @typescript-eslint/no-empty-function
onDeletePolicyClick={() => {}}
currentTeam={{ id: 2, name: "Team 2" }}
isPremiumTier
searchQuery=""
page={0}
onQueryChange={noop}
renderPoliciesCount={() => null}
canAddOrDeletePolicy
hasPoliciesToDelete
/>
);
const numberOfCheckboxes = container.querySelectorAll(
"input[type='checkbox']"
).length;
expect(numberOfCheckboxes).toBe(
testTeamPolicies.length + 1 // +1 for Select all checkbox
);
const checkbox = container.querySelectorAll(
"input[type='checkbox']"
)[0] as HTMLInputElement;
await waitFor(() => {
waitFor(() => {
user.click(checkbox);
});
expect(checkbox.checked).toBe(true);
});
});
});

View file

@ -26,7 +26,6 @@ interface IPoliciesTableProps {
currentTeam: ITeamSummary | undefined;
currentAutomatedPolicies?: number[];
isPremiumTier?: boolean;
isSandboxMode?: boolean;
renderPoliciesCount: () => JSX.Element | null;
onQueryChange: (newTableQuery: ITableQueryData) => void;
searchQuery: string;
@ -45,7 +44,6 @@ const PoliciesTable = ({
currentTeam,
currentAutomatedPolicies,
isPremiumTier,
isSandboxMode,
onQueryChange,
renderPoliciesCount,
searchQuery,
@ -104,8 +102,7 @@ const PoliciesTable = ({
hasPermissionAndPoliciesToDelete,
},
policiesList,
isPremiumTier,
isSandboxMode
isPremiumTier
)}
data={generateDataSet(
policiesList,

View file

@ -7,7 +7,7 @@ import {
millisecondsToHours,
millisecondsToMinutes,
} from "date-fns";
import ReactTooltip from "react-tooltip";
import { Tooltip as ReactTooltip5 } from "react-tooltip-5";
// @ts-ignore
import Checkbox from "components/forms/fields/Checkbox";
import HeaderCell from "components/TableContainer/DataTable/HeaderCell";
@ -18,7 +18,7 @@ import PATHS from "router/paths";
import sortUtils from "utilities/sort";
import { PolicyResponse } from "utilities/constants";
import { buildQueryStringFromParams } from "utilities/url";
import { COLORS } from "styles/var/colors";
import InheritedBadge from "components/InheritedBadge";
import { getConditionalSelectHeaderCheckboxProps } from "components/TableContainer/utilities/config_utils";
import PassingColumnHeader from "../PassingColumnHeader";
@ -87,11 +87,11 @@ const getPolicyRefreshTime = (ms: number): string => {
const getTooltip = (osqueryPolicyMs: number): JSX.Element => {
return (
<span className={`tooltip__tooltip-text`}>
<>
Fleet is collecting policy results. Try again
<br />
in about {getPolicyRefreshTime(osqueryPolicyMs)} as the system catches up.
</span>
</>
);
};
@ -104,8 +104,7 @@ const generateTableHeaders = (
tableType?: string;
},
policiesList: IPolicyStats[] = [],
isPremiumTier?: boolean,
isSandboxMode?: boolean
isPremiumTier?: boolean
): IDataColumn[] => {
const { selectedTeamId, hasPermissionAndPoliciesToDelete } = options;
const viewingTeamPolicies = selectedTeamId !== -1;
@ -143,11 +142,10 @@ const generateTableHeaders = (
<>
<div className="policy-name-text">{cellProps.cell.value}</div>
{isPremiumTier && cellProps.row.original.critical && (
<>
<div className="critical-badge">
<span
className="critical-badge"
data-tip
data-for={`critical-tooltip-${cellProps.row.original.id}`}
className="critical-badge-icon"
data-tooltip-id={`critical-tooltip-${cellProps.row.original.id}`}
>
<Icon
className="critical-policy-icon"
@ -156,44 +154,21 @@ const generateTableHeaders = (
color="core-fleet-blue"
/>
</span>
<ReactTooltip
<ReactTooltip5
className="critical-tooltip"
disableStyleInjection
place="top"
type="dark"
effect="solid"
opacity={1}
id={`critical-tooltip-${cellProps.row.original.id}`}
backgroundColor={COLORS["tooltip-bg"]}
offset={8}
positionStrategy="fixed"
>
This policy has been marked as critical.
{isSandboxMode && (
<>
<br />
This is a premium feature.
</>
)}
</ReactTooltip>
</>
</ReactTooltip5>
</div>
)}
{viewingTeamPolicies && !cellProps.row.original.team_id && (
<>
<span
className="inherited-badge"
data-tip
data-for={`inherited-tooltip-${cellProps.row.original.id}`}
>
Inherited
</span>
<ReactTooltip
className="inherited-tooltip"
place="top"
type="dark"
effect="solid"
id={`inherited-tooltip-${cellProps.row.original.id}`}
backgroundColor={COLORS["tooltip-bg"]}
>
This policy runs on all hosts.
</ReactTooltip>
</>
<InheritedBadge tooltipContent="This policy runs on all hosts." />
)}
</>
}
@ -228,24 +203,24 @@ const generateTableHeaders = (
);
}
return (
<>
<div className="policy-has-not-run">
<span
className="text-cell text-muted has-not-run tooltip"
data-tip
data-for={`passing_${cellProps.row.original.id.toString()}`}
data-tooltip-id={`passing_${cellProps.row.original.id.toString()}`}
>
---
</span>
<ReactTooltip
place="bottom"
effect="solid"
backgroundColor={COLORS["tooltip-bg"]}
<ReactTooltip5
className="policy-has-not-run-tooltip"
disableStyleInjection
place="top"
opacity={1}
id={`passing_${cellProps.row.original.id.toString()}`}
data-html
offset={8}
positionStrategy="fixed"
>
{getTooltip(cellProps.row.original.next_update_ms)}
</ReactTooltip>
</>
</ReactTooltip5>
</div>
);
},
},
@ -279,33 +254,30 @@ const generateTableHeaders = (
);
}
return (
<>
<div className="policy-has-not-run">
<span
className="text-cell text-muted has-not-run tooltip"
data-tip
data-for={`failing_${cellProps.row.original.id.toString()}`}
data-tooltip-id={`passing_${cellProps.row.original.id.toString()}`}
>
---
</span>
<ReactTooltip
place="bottom"
effect="solid"
backgroundColor={COLORS["tooltip-bg"]}
id={`failing_${cellProps.row.original.id.toString()}`}
data-html
<ReactTooltip5
className="policy-has-not-run-tooltip"
disableStyleInjection
place="top"
opacity={1}
id={`passing_${cellProps.row.original.id.toString()}`}
offset={8}
positionStrategy="fixed"
>
{getTooltip(cellProps.row.original.next_update_ms)}
</ReactTooltip>
</>
</ReactTooltip5>
</div>
);
},
sortType: "caseInsensitive",
},
];
console.log(
"hasPermissionAndPoliciesToDelete",
hasPermissionAndPoliciesToDelete
);
if (hasPermissionAndPoliciesToDelete) {
tableHeaders.unshift({
id: "selection",

View file

@ -26,10 +26,6 @@
}
}
.has-not-run {
width: 20px;
}
.no-team-policy {
border: 1px solid #e2e4ea;
box-sizing: border-box;

View file

@ -149,8 +149,17 @@
.inherited-badge {
overflow: initial;
}
.observer-can-run-badge {
@include tooltip5-arrow-styles;
.react-tooltip {
@include tooltip-text;
font-style: normal;
text-align: center;
}
}
}
.query-icon {
.observer-can-run-query-icon {
display: block;
}
@ -158,9 +167,6 @@
display: flex;
gap: $pad-xsmall;
}
.observer-can-run-tooltip {
font-weight: $regular;
}
}
@media (max-width: $break-md) {

View file

@ -1,9 +1,10 @@
import React from "react";
import { screen } from "@testing-library/react";
import { screen, waitFor } from "@testing-library/react";
import { createCustomRenderer } from "test/test-utils";
import createMockUser from "__mocks__/userMock";
import createMockQuery from "__mocks__/queryMock";
import { ISchedulableQuery } from "interfaces/schedulable_query";
import QueriesTable, { IQueriesTableProps } from "./QueriesTable";
@ -207,4 +208,160 @@ describe("QueriesTable", () => {
});
});
});
it("Renders an observer can run badge and tooltip for a observer can run query", async () => {
const render = createCustomRenderer({
context: {
app: {
isGlobalAdmin: true,
currentUser: createMockUser(),
},
},
});
const testObserverCanRunQuery = [
createMockQuery({
observer_can_run: true,
}),
];
const testQueries = testObserverCanRunQuery.map(enhanceQuery);
const { user } = render(
<QueriesTable
queriesList={testQueries}
onlyInheritedQueries={false}
isLoading={false}
onDeleteQueryClick={jest.fn()}
onCreateQueryClick={jest.fn()}
isOnlyObserver={false}
isObserverPlus={false}
isAnyTeamObserverPlus={false}
currentTeamId={1}
/>
);
await waitFor(() => {
waitFor(() => {
user.hover(screen.getByTestId("query-icon"));
});
expect(
screen.getByText("Observers can run this query.")
).toBeInTheDocument();
});
});
it("Renders an inherited badge and tooltip for inherited query on a team's queries page", async () => {
const render = createCustomRenderer({
context: {
app: {
isGlobalAdmin: true,
currentUser: createMockUser(),
},
},
});
const testInheritedQuery = [createMockQuery()];
const testQueries = testInheritedQuery.map(enhanceQuery);
const { user } = render(
<QueriesTable
queriesList={testQueries}
onlyInheritedQueries={false}
isLoading={false}
onDeleteQueryClick={jest.fn()}
onCreateQueryClick={jest.fn()}
isOnlyObserver={false}
isObserverPlus={false}
isAnyTeamObserverPlus={false}
currentTeamId={1}
/>
);
await waitFor(() => {
waitFor(() => {
user.hover(screen.getByText("Inherited"));
});
expect(
screen.getByText("This query runs on all hosts.")
).toBeInTheDocument();
});
});
it("Does not render an inherited badge and tooltip for global query on the All team's queries page", () => {
const render = createCustomRenderer({
context: {
app: {
isGlobalAdmin: true,
currentUser: createMockUser(),
},
},
});
const testGlobalQuery = [createMockQuery()];
const testQueries = testGlobalQuery.map(enhanceQuery);
render(
<QueriesTable
queriesList={testQueries}
onlyInheritedQueries={false}
isLoading={false}
onDeleteQueryClick={jest.fn()}
onCreateQueryClick={jest.fn()}
isOnlyObserver={false}
isObserverPlus={false}
isAnyTeamObserverPlus={false}
currentTeamId={undefined}
/>
);
expect(screen.queryByText("Inherited")).not.toBeInTheDocument();
});
it("Renders the correct number of checkboxes for team queries and not inherited queries on a team's queries page and can check select all box", async () => {
const render = createCustomRenderer({
context: {
app: {
isGlobalAdmin: true,
currentUser: createMockUser(),
},
},
});
const { container, user } = render(
<QueriesTable
queriesList={[...testTeamQueries, ...testGlobalQueries]}
onlyInheritedQueries={false}
isLoading={false}
onDeleteQueryClick={jest.fn()}
onCreateQueryClick={jest.fn()}
isOnlyObserver={false}
isObserverPlus={false}
isAnyTeamObserverPlus={false}
currentTeamId={1}
/>
);
const numberOfCheckboxes = container.querySelectorAll(
"input[type='checkbox']"
).length;
expect(numberOfCheckboxes).toBe(
testTeamQueries.length + 1 // +1 for Select all checkbox
);
const checkbox = container.querySelectorAll(
"input[type='checkbox']"
)[0] as HTMLInputElement;
await waitFor(() => {
waitFor(() => {
user.click(checkbox);
});
expect(checkbox.checked).toBe(true);
});
});
});

View file

@ -26,7 +26,7 @@ import TextCell from "components/TableContainer/DataTable/TextCell";
import PerformanceImpactCell from "components/TableContainer/DataTable/PerformanceImpactCell";
import TooltipWrapper from "components/TooltipWrapper";
import InheritedBadge from "components/InheritedBadge";
import { COLORS } from "styles/var/colors";
import { Tooltip as ReactTooltip5 } from "react-tooltip-5";
import QueryAutomationsStatusIndicator from "../QueryAutomationsStatusIndicator";
interface IQueryRow {
@ -136,25 +136,50 @@ const generateTableHeaders = ({
<>
<div className="query-name-text">{cellProps.cell.value}</div>
{!isOnlyObserver && cellProps.row.original.observer_can_run && (
<>
<div className="observer-can-run-badge">
<span
className="tooltip-base"
data-tip
data-for={`observer-can-run-tooltip-${cellProps.row.original.id}`}
className="observer-can-run-icon"
data-tooltip-id={`observer-can-run-tooltip-${cellProps.row.original.id}`}
>
<Icon className="query-icon" name="query" size="small" />
<Icon
className="observer-can-run-query-icon"
name="query"
size="small"
color="core-fleet-blue"
/>
</span>
<ReactTooltip
<ReactTooltip5
className="observer-can-run-tooltip"
disableStyleInjection
place="top"
type="dark"
effect="solid"
opacity={1}
id={`observer-can-run-tooltip-${cellProps.row.original.id}`}
backgroundColor={COLORS["tooltip-bg"]}
offset={8}
positionStrategy="fixed"
>
Observers can run this query.
</ReactTooltip>
</>
</ReactTooltip5>
</div>
// <>
// <span
// className="tooltip-base"
// data-tip
// data-for={`observer-can-run-tooltip-${cellProps.row.original.id}`}
// >
// <Icon className="query-icon" name="query" size="small" />
// </span>
// <ReactTooltip
// className="observer-can-run-tooltip"
// place="top"
// type="dark"
// effect="solid"
// id={`observer-can-run-tooltip-${cellProps.row.original.id}`}
// backgroundColor={COLORS["tooltip-bg"]}
// >
// Observers can run this query.
// </ReactTooltip>
// </>
)}
{viewingTeamScope &&
// inherited