diff --git a/frontend/components/InheritedBadge/InheritedBadge.tsx b/frontend/components/InheritedBadge/InheritedBadge.tsx index 7394063f45..0a098317c1 100644 --- a/frontend/components/InheritedBadge/InheritedBadge.tsx +++ b/frontend/components/InheritedBadge/InheritedBadge.tsx @@ -15,7 +15,7 @@ const InheritedBadge = ({ }: IInheritedBadgeProps) => { const tooltipId = uniqueId(); return ( -
+
(!isFetchingGlobalCount && renderPoliciesCount(globalPoliciesCount)) || diff --git a/frontend/pages/policies/ManagePoliciesPage/_styles.scss b/frontend/pages/policies/ManagePoliciesPage/_styles.scss index caae6f8f4d..ca0978671d 100644 --- a/frontend/pages/policies/ManagePoliciesPage/_styles.scss +++ b/frontend/pages/policies/ManagePoliciesPage/_styles.scss @@ -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; + } } } } diff --git a/frontend/pages/policies/ManagePoliciesPage/components/PoliciesTable/PoliciesTable.tests.tsx b/frontend/pages/policies/ManagePoliciesPage/components/PoliciesTable/PoliciesTable.tests.tsx index 14ff10e161..f100e4761b 100644 --- a/frontend/pages/policies/ManagePoliciesPage/components/PoliciesTable/PoliciesTable.tests.tsx +++ b/frontend/pages/policies/ManagePoliciesPage/components/PoliciesTable/PoliciesTable.tests.tsx @@ -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( {}} 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( + {}} + 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( { 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( + {}} + 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( + {}} + 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( + {}} + 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); + }); }); }); diff --git a/frontend/pages/policies/ManagePoliciesPage/components/PoliciesTable/PoliciesTable.tsx b/frontend/pages/policies/ManagePoliciesPage/components/PoliciesTable/PoliciesTable.tsx index 36c027256a..9047242c8b 100644 --- a/frontend/pages/policies/ManagePoliciesPage/components/PoliciesTable/PoliciesTable.tsx +++ b/frontend/pages/policies/ManagePoliciesPage/components/PoliciesTable/PoliciesTable.tsx @@ -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, diff --git a/frontend/pages/policies/ManagePoliciesPage/components/PoliciesTable/PoliciesTableConfig.tsx b/frontend/pages/policies/ManagePoliciesPage/components/PoliciesTable/PoliciesTableConfig.tsx index dff531ab77..3d395c64c2 100644 --- a/frontend/pages/policies/ManagePoliciesPage/components/PoliciesTable/PoliciesTableConfig.tsx +++ b/frontend/pages/policies/ManagePoliciesPage/components/PoliciesTable/PoliciesTableConfig.tsx @@ -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 ( - + <> Fleet is collecting policy results. Try again
in about {getPolicyRefreshTime(osqueryPolicyMs)} as the system catches up. -
+ ); }; @@ -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 = ( <>
{cellProps.cell.value}
{isPremiumTier && cellProps.row.original.critical && ( - <> +
- This policy has been marked as critical. - {isSandboxMode && ( - <> -
- This is a premium feature. - - )} -
- + +
)} {viewingTeamPolicies && !cellProps.row.original.team_id && ( - <> - - Inherited - - - This policy runs on all hosts. - - + )} } @@ -228,24 +203,24 @@ const generateTableHeaders = ( ); } return ( - <> +
--- - {getTooltip(cellProps.row.original.next_update_ms)} - - + +
); }, }, @@ -279,33 +254,30 @@ const generateTableHeaders = ( ); } return ( - <> +
--- - {getTooltip(cellProps.row.original.next_update_ms)} - - + +
); }, sortType: "caseInsensitive", }, ]; - console.log( - "hasPermissionAndPoliciesToDelete", - hasPermissionAndPoliciesToDelete - ); + if (hasPermissionAndPoliciesToDelete) { tableHeaders.unshift({ id: "selection", diff --git a/frontend/pages/policies/ManagePoliciesPage/components/PoliciesTable/_styles.scss b/frontend/pages/policies/ManagePoliciesPage/components/PoliciesTable/_styles.scss index 9913e4d229..1d8354c466 100644 --- a/frontend/pages/policies/ManagePoliciesPage/components/PoliciesTable/_styles.scss +++ b/frontend/pages/policies/ManagePoliciesPage/components/PoliciesTable/_styles.scss @@ -26,10 +26,6 @@ } } -.has-not-run { - width: 20px; -} - .no-team-policy { border: 1px solid #e2e4ea; box-sizing: border-box; diff --git a/frontend/pages/queries/ManageQueriesPage/_styles.scss b/frontend/pages/queries/ManageQueriesPage/_styles.scss index 8ec52b7d29..3f7cd25150 100644 --- a/frontend/pages/queries/ManageQueriesPage/_styles.scss +++ b/frontend/pages/queries/ManageQueriesPage/_styles.scss @@ -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) { diff --git a/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTable.tests.tsx b/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTable.tests.tsx index 3d7e560bf7..3269ebcc1e 100644 --- a/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTable.tests.tsx +++ b/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTable.tests.tsx @@ -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( + + ); + + 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( + + ); + + 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( + + ); + + 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( + + ); + + 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); + }); + }); }); diff --git a/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTableConfig.tsx b/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTableConfig.tsx index 0b03dad9b3..32b712ef82 100644 --- a/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTableConfig.tsx +++ b/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTableConfig.tsx @@ -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 = ({ <>
{cellProps.cell.value}
{!isOnlyObserver && cellProps.row.original.observer_can_run && ( - <> +
- + - Observers can run this query. - - + +
+ + // <> + // + // + // + // + // Observers can run this query. + // + // )} {viewingTeamScope && // inherited