From 4ea5ea9f0006823356f6f1fec010da2b1250e0c8 Mon Sep 17 00:00:00 2001 From: RachelElysia <71795832+RachelElysia@users.noreply.github.com> Date: Tue, 7 May 2024 12:47:12 -0400 Subject: [PATCH] Revert "Feature: 15605 merge inherited queries and policies" (#18800) Reverts fleetdm/fleet#18771 (Because of accidental squash and merge of feature dev by 3 of us only has my name on it) --- ...-merge-inherited-and-team-queries-policies | 2 - frontend/__mocks__/policyMock.ts | 85 +--- frontend/components/EmptyTable/_styles.scss | 3 +- .../InheritedBadge/InheritedBadge.tsx | 40 -- .../components/InheritedBadge/_styles.scss | 19 - frontend/components/InheritedBadge/index.ts | 1 - .../TableContainer/DataTable/DataTable.tsx | 5 +- .../TableContainer/TableContainer.tsx | 9 +- .../{utilities => }/TableContainerUtils.ts | 5 +- .../TableContainer/utilities/config_utils.ts | 50 --- .../DropdownOptionTooltipWrapper.tsx | 1 + .../DropdownOptionTooltipWrapper/_styles.scss | 18 +- frontend/components/graphics/EmptyQueries.tsx | 168 ++++---- frontend/context/query.tsx | 4 +- frontend/interfaces/activity.ts | 4 +- frontend/interfaces/policy.ts | 12 +- frontend/interfaces/query.ts | 5 +- frontend/interfaces/query_stats.ts | 10 +- frontend/interfaces/schedulable_query.ts | 22 +- frontend/interfaces/scheduled_query.ts | 11 +- frontend/interfaces/scheduled_query_stats.ts | 17 + .../hosts/ManageHostsPage/ManageHostsPage.tsx | 2 +- .../ManagePoliciesPage/ManagePoliciesPage.tsx | 288 +++++++++----- .../policies/ManagePoliciesPage/_styles.scss | 32 +- .../PoliciesTable/PoliciesTable.tests.tsx | 219 ++--------- .../PoliciesTable/PoliciesTable.tsx | 49 ++- .../PoliciesTable/PoliciesTableConfig.tsx | 138 +++---- .../components/PoliciesTable/_styles.scss | 4 + .../PolicyErrorsTable/PolicyErrorsTable.tsx | 6 +- .../PolicyResultsTable/PolicyResultsTable.tsx | 6 +- .../ManageQueriesPage/ManageQueriesPage.tsx | 209 ++++++---- .../queries/ManageQueriesPage/_styles.scss | 40 +- .../QueriesTable/QueriesTable.tests.tsx | 367 ------------------ .../components/QueriesTable/QueriesTable.tsx | 131 +++++-- .../QueriesTable/QueriesTableConfig.tsx | 93 ++--- frontend/services/entities/queries.ts | 7 +- frontend/services/entities/team_policies.ts | 25 +- .../services/mock_service/mocks/responses.ts | 4 - frontend/styles/var/mixins.scss | 20 - frontend/utilities/helpers.tsx | 4 +- server/datastore/mysql/policies.go | 53 --- server/datastore/mysql/policies_test.go | 82 ---- server/datastore/mysql/queries.go | 12 +- server/datastore/mysql/queries_test.go | 22 +- server/fleet/app.go | 3 - server/fleet/datastore.go | 3 - server/fleet/service.go | 8 +- server/mock/datastore_mock.go | 24 -- server/service/global_schedule.go | 2 +- server/service/integration_core_test.go | 8 +- server/service/integration_enterprise_test.go | 90 +---- server/service/queries.go | 16 +- server/service/queries_test.go | 2 +- server/service/team_policies.go | 23 +- server/service/team_policies_test.go | 2 +- server/service/team_schedule.go | 2 +- 56 files changed, 801 insertions(+), 1686 deletions(-) delete mode 100644 changes/15605-merge-inherited-and-team-queries-policies delete mode 100644 frontend/components/InheritedBadge/InheritedBadge.tsx delete mode 100644 frontend/components/InheritedBadge/_styles.scss delete mode 100644 frontend/components/InheritedBadge/index.ts rename frontend/components/TableContainer/{utilities => }/TableContainerUtils.ts (90%) delete mode 100644 frontend/components/TableContainer/utilities/config_utils.ts create mode 100644 frontend/interfaces/scheduled_query_stats.ts delete mode 100644 frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTable.tests.tsx diff --git a/changes/15605-merge-inherited-and-team-queries-policies b/changes/15605-merge-inherited-and-team-queries-policies deleted file mode 100644 index 0841ea667f..0000000000 --- a/changes/15605-merge-inherited-and-team-queries-policies +++ /dev/null @@ -1,2 +0,0 @@ -- UI Change: Team queries page renders team level and inherited queries in a single table set by a new merge_inherited API parameter -- UI Change: Team policies page renders team level and inherited policies in a single table set by a new merge_inherited API parameter diff --git a/frontend/__mocks__/policyMock.ts b/frontend/__mocks__/policyMock.ts index b14095463e..c66c58a0bc 100644 --- a/frontend/__mocks__/policyMock.ts +++ b/frontend/__mocks__/policyMock.ts @@ -11,7 +11,7 @@ const DEFAULT_POLICY_MOCK: IPolicyStats = { author_id: 1, author_name: "Test User", author_email: "test@user.com", - team_id: null, + team_id: undefined, resolution: "Ensure ClamAV and Freshclam are installed and running.", platform: "linux" as const, created_at: "2023-03-24T22:13:59Z", @@ -29,87 +29,4 @@ const createMockPolicy = (overrides?: Partial): IPolicyStats => { return { ...DEFAULT_POLICY_MOCK, ...overrides }; }; -export const createMockPoliciesResponse = ( - overrides?: Partial -) => { - const MOCK_POLICIES_RESPONSE: { policies: IPolicyStats[] } = { - policies: [ - { - id: 5, - name: "Gatekeeper enabled", - query: "SELECT 1 FROM gatekeeper WHERE assessments_enabled = 1;", - description: "Checks if gatekeeper is enabled on macOS devices", - critical: true, - author_id: 42, - author_name: "John", - author_email: "john@example.com", - team_id: 2, - resolution: "Resolution steps", - platform: "darwin", - created_at: "2021-12-16T14:37:37Z", - updated_at: "2021-12-16T16:39:00Z", - passing_host_count: 2000, - failing_host_count: 300, - host_count_updated_at: "2023-12-20T15:23:57Z", - webhook: "Off", - has_run: true, - next_update_ms: 3600000, - calendar_events_enabled: false, - }, - { - id: 29090, - name: "Windows machines with encrypted hard disks", - query: "SELECT 1 FROM bitlocker_info WHERE protection_status = 1;", - description: "Checks if the hard disk is encrypted on Windows devices", - critical: false, - author_id: 43, - author_name: "Alice", - author_email: "alice@example.com", - team_id: 2, - resolution: "Resolution steps", - platform: "windows", - created_at: "2021-12-16T14:37:37Z", - updated_at: "2021-12-16T16:39:00Z", - passing_host_count: 2300, - failing_host_count: 0, - host_count_updated_at: "2023-12-20T15:23:57Z", - webhook: "Off", - has_run: true, - next_update_ms: 3600000, - calendar_events_enabled: false, - }, - { - id: 136, - name: "Arbitrary Test Policy (all platforms) (all teams)", - query: "SELECT 1 FROM osquery_info WHERE 1=1;", - description: - "If you're seeing this, mostly likely this is because someone is testing out failing policies in dogfood. You can ignore this.", - critical: true, - author_id: 77, - author_name: "Test Admin", - author_email: "test@admin.com", - team_id: null, - resolution: - 'To make it pass, change "1=0" to "1=1". To make it fail, change "1=1" to "1=0".', - platform: "darwin,windows,linux", - created_at: "2022-08-04T19:30:18Z", - updated_at: "2022-08-30T15:08:26Z", - passing_host_count: 10, - failing_host_count: 9, - host_count_updated_at: "2023-12-20T15:23:57Z", - webhook: "Off", - has_run: true, - next_update_ms: 3600000, - calendar_events_enabled: false, - }, - ], - }; - - if (overrides) { - MOCK_POLICIES_RESPONSE.policies.push(createMockPolicy(overrides)); - } - - return MOCK_POLICIES_RESPONSE; -}; - export default createMockPolicy; diff --git a/frontend/components/EmptyTable/_styles.scss b/frontend/components/EmptyTable/_styles.scss index 2aa30b7d68..940afb6945 100644 --- a/frontend/components/EmptyTable/_styles.scss +++ b/frontend/components/EmptyTable/_styles.scss @@ -39,9 +39,10 @@ &__info, &__additional-info { - @include help-text; line-height: 1.5; text-align: center; + color: $core-fleet-blue; + font-size: $x-small; margin: 0; } diff --git a/frontend/components/InheritedBadge/InheritedBadge.tsx b/frontend/components/InheritedBadge/InheritedBadge.tsx deleted file mode 100644 index 0a098317c1..0000000000 --- a/frontend/components/InheritedBadge/InheritedBadge.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { uniqueId } from "lodash"; -import React from "react"; -import { PlacesType, Tooltip as ReactTooltip5 } from "react-tooltip-5"; - -const baseClass = "inherited-badge"; - -interface IInheritedBadgeProps { - tooltipPosition?: PlacesType; - tooltipContent: React.ReactNode; -} - -const InheritedBadge = ({ - tooltipPosition = "top", - tooltipContent, -}: IInheritedBadgeProps) => { - const tooltipId = uniqueId(); - return ( -
- - Inherited - - - {tooltipContent} - -
- ); -}; - -export default InheritedBadge; diff --git a/frontend/components/InheritedBadge/_styles.scss b/frontend/components/InheritedBadge/_styles.scss deleted file mode 100644 index 5c6bdba177..0000000000 --- a/frontend/components/InheritedBadge/_styles.scss +++ /dev/null @@ -1,19 +0,0 @@ -.inherited-badge { - &__element-text { - font-weight: $bold; - font-size: $xxx-small; - color: $core-fleet-black; - line-height: 15px; - border-radius: 4px; - background: $ui-vibrant-blue-10; - padding: 4px; - } - - @include tooltip5-arrow-styles; - - .react-tooltip { - @include tooltip-text; - font-style: normal; - text-align: center; - } -} diff --git a/frontend/components/InheritedBadge/index.ts b/frontend/components/InheritedBadge/index.ts deleted file mode 100644 index 1f70caec54..0000000000 --- a/frontend/components/InheritedBadge/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./InheritedBadge"; diff --git a/frontend/components/TableContainer/DataTable/DataTable.tsx b/frontend/components/TableContainer/DataTable/DataTable.tsx index 89f8709f40..9d299b9867 100644 --- a/frontend/components/TableContainer/DataTable/DataTable.tsx +++ b/frontend/components/TableContainer/DataTable/DataTable.tsx @@ -69,9 +69,8 @@ interface IHeaderGroup extends HeaderGroup { const CLIENT_SIDE_DEFAULT_PAGE_SIZE = 20; -// This data table uses react-table for implementation. The relevant v7 documentation of the library -// can be found here https://react-table-v7-docs.netlify.app/docs/api/usetable - +// This data table uses react-table for implementation. The relevant documentation of the library +// can be found here https://react-table.tanstack.com/docs/api/useTable const DataTable = ({ columns: tableColumns, data: tableData, diff --git a/frontend/components/TableContainer/TableContainer.tsx b/frontend/components/TableContainer/TableContainer.tsx index 68fddb04cf..a78c023c1f 100644 --- a/frontend/components/TableContainer/TableContainer.tsx +++ b/frontend/components/TableContainer/TableContainer.tsx @@ -12,7 +12,7 @@ import Icon from "components/Icon/Icon"; import { COLORS } from "styles/var/colors"; import DataTable from "./DataTable/DataTable"; -import TableContainerUtils from "./utilities/TableContainerUtils"; +import TableContainerUtils from "./TableContainerUtils"; import { IActionButtonProps } from "./DataTable/ActionButton/ActionButton"; export interface ITableQueryData { @@ -100,7 +100,6 @@ interface ITableContainerProps { setExportRows?: (rows: Row[]) => void; resetPageIndex?: boolean; disableTableHeader?: boolean; - show0Count?: boolean; } const baseClass = "table-container"; @@ -157,7 +156,6 @@ const TableContainer = ({ setExportRows, resetPageIndex, disableTableHeader, - show0Count, }: ITableContainerProps) => { const [searchQuery, setSearchQuery] = useState(defaultSearchQuery); const [sortHeader, setSortHeader] = useState(defaultSortHeader || ""); @@ -323,7 +321,7 @@ const TableContainer = ({ )} {!renderCount && !disableCount && - (isMultiColumnFilter || displayCount() || show0Count) ? ( + (isMultiColumnFilter || displayCount()) ? (
({ > {TableContainerUtils.generateResultsCountText( resultsTitle, - displayCount(), - show0Count + displayCount() )} {resultsHtml}
diff --git a/frontend/components/TableContainer/utilities/TableContainerUtils.ts b/frontend/components/TableContainer/TableContainerUtils.ts similarity index 90% rename from frontend/components/TableContainer/utilities/TableContainerUtils.ts rename to frontend/components/TableContainer/TableContainerUtils.ts index e825324eda..4f883b678b 100644 --- a/frontend/components/TableContainer/utilities/TableContainerUtils.ts +++ b/frontend/components/TableContainer/TableContainerUtils.ts @@ -2,10 +2,9 @@ const DEFAULT_RESULTS_NAME = "results"; const generateResultsCountText = ( name: string = DEFAULT_RESULTS_NAME, - resultsCount: number, - show0Count = false + resultsCount: number ): string => { - if (resultsCount === 0 && !show0Count) return `No ${name}`; + if (resultsCount === 0) return `No ${name}`; // If there is 1 result and the last 3 letters in the result // name are "ies," we remove the "ies" and add "y" // to make the name singular diff --git a/frontend/components/TableContainer/utilities/config_utils.ts b/frontend/components/TableContainer/utilities/config_utils.ts deleted file mode 100644 index 7acf97737d..0000000000 --- a/frontend/components/TableContainer/utilities/config_utils.ts +++ /dev/null @@ -1,50 +0,0 @@ -// from https://stackoverflow.com/a/68213902/15458245 - -import { HeaderProps, Row } from "react-table"; - -interface GetConditionalSelectHeaderCheckboxProps { - /** react-table header props */ - headerProps: React.PropsWithChildren>; - checkIfRowIsSelectable: (row: Row) => boolean; -} - -export const getConditionalSelectHeaderCheckboxProps = ({ - headerProps, - checkIfRowIsSelectable, -}: GetConditionalSelectHeaderCheckboxProps) => { - // Define if the checkbox should show as checked or indeterminate - const checkIfAllSelectableRowsSelected = (rows: Row[]) => - rows.filter(checkIfRowIsSelectable).every((row) => row.isSelected); - const allSelectableRowsSelected = checkIfAllSelectableRowsSelected( - headerProps.rows - ); - const indeterminate = - !allSelectableRowsSelected && - headerProps.rows.some((row) => row.isSelected); - - const onChange = () => { - if (checkIfAllSelectableRowsSelected(headerProps.rows)) { - headerProps.rows.forEach((row) => { - headerProps.toggleRowSelected(row.id, false); - }); - } else { - // Otherwise select every selectable row on the page - headerProps.page.forEach((row) => { - const rowChecked = checkIfRowIsSelectable(row); - headerProps.toggleRowSelected(row.id, rowChecked); - }); - } - }; - - // Usual checkbox props - const checkboxProps = headerProps.getToggleAllRowsSelectedProps(); - - return { - ...checkboxProps, - value: allSelectableRowsSelected, - indeterminate, - onChange, - }; -}; - -export default { getConditionalSelectHeaderCheckboxProps }; diff --git a/frontend/components/forms/fields/Dropdown/DropdownOptionTooltipWrapper/DropdownOptionTooltipWrapper.tsx b/frontend/components/forms/fields/Dropdown/DropdownOptionTooltipWrapper/DropdownOptionTooltipWrapper.tsx index 5acdd89f26..8fc4da9dc5 100644 --- a/frontend/components/forms/fields/Dropdown/DropdownOptionTooltipWrapper/DropdownOptionTooltipWrapper.tsx +++ b/frontend/components/forms/fields/Dropdown/DropdownOptionTooltipWrapper/DropdownOptionTooltipWrapper.tsx @@ -56,6 +56,7 @@ const DropdownOptionTooltipWrapper = ({ clickable={clickable} offset={offset} positionStrategy="fixed" + classNameArrow="tooltip-arrow" > {tipContent} diff --git a/frontend/components/forms/fields/Dropdown/DropdownOptionTooltipWrapper/_styles.scss b/frontend/components/forms/fields/Dropdown/DropdownOptionTooltipWrapper/_styles.scss index c617631615..c6f03b9cb7 100644 --- a/frontend/components/forms/fields/Dropdown/DropdownOptionTooltipWrapper/_styles.scss +++ b/frontend/components/forms/fields/Dropdown/DropdownOptionTooltipWrapper/_styles.scss @@ -17,5 +17,21 @@ text-align: center; } - @include tooltip5-arrow-styles; + // arrow styles directly from react-tooltip-5 css + .tooltip-arrow { + width: 8px; + height: 8px; + } + [class*="react-tooltip__place-top"] > .styles-module_arrow__K0L3T { + transform: rotate(45deg); + } + [class*="react-tooltip__place-right"] > .styles-module_arrow__K0L3T { + transform: rotate(135deg); + } + [class*="react-tooltip__place-bottom"] > .styles-module_arrow__K0L3T { + transform: rotate(225deg); + } + [class*="react-tooltip__place-left"] > .styles-module_arrow__K0L3T { + transform: rotate(315deg); + } } diff --git a/frontend/components/graphics/EmptyQueries.tsx b/frontend/components/graphics/EmptyQueries.tsx index 5d97e1a0ba..26f6027b59 100644 --- a/frontend/components/graphics/EmptyQueries.tsx +++ b/frontend/components/graphics/EmptyQueries.tsx @@ -3,135 +3,107 @@ import React from "react"; const EmptyQueries = () => { return ( - - - - + + + + + + - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + - - + - - + + diff --git a/frontend/context/query.tsx b/frontend/context/query.tsx index f7d05f3b91..e117a33bd5 100644 --- a/frontend/context/query.tsx +++ b/frontend/context/query.tsx @@ -29,7 +29,7 @@ type InitialStateType = { lastEditedQueryMinOsqueryVersion: string; lastEditedQueryLoggingType: QueryLoggingOption; lastEditedQueryDiscardData: boolean; - editingExistingQuery?: boolean; + editingExistingQuery: boolean; selectedQueryTargets: ITarget[]; // Mimicks old selectedQueryTargets still used for policies for SelectTargets.tsx and running a live query selectedQueryTargetsByType: ISelectedTargetsByType; // New format by type for cleaner app wide state setLastEditedQueryId: (value: number | null) => void; @@ -63,7 +63,7 @@ const initialState = { lastEditedQueryMinOsqueryVersion: DEFAULT_QUERY.min_osquery_version, lastEditedQueryLoggingType: DEFAULT_QUERY.logging, lastEditedQueryDiscardData: DEFAULT_QUERY.discard_data, - editingExistingQuery: DEFAULT_QUERY.editingExistingQuery ?? false, + editingExistingQuery: DEFAULT_QUERY.editingExistingQuery, selectedQueryTargets: DEFAULT_TARGETS, selectedQueryTargetsByType: DEFAULT_TARGETS_BY_TYPE, setLastEditedQueryId: () => null, diff --git a/frontend/interfaces/activity.ts b/frontend/interfaces/activity.ts index 51f8fe1229..841c326ab2 100644 --- a/frontend/interfaces/activity.ts +++ b/frontend/interfaces/activity.ts @@ -1,6 +1,6 @@ import { IPolicy } from "./policy"; import { IQuery } from "./query"; -import { ISchedulableQueryStats } from "./schedulable_query"; +import { IScheduledQueryStats } from "./scheduled_query_stats"; import { ITeamSummary } from "./team"; import { UserRole } from "./user"; @@ -131,6 +131,6 @@ export interface IActivityDetails { script_name?: string; deadline_days?: number; grace_period_days?: number; - stats?: ISchedulableQueryStats; + stats?: IScheduledQueryStats; host_id?: number; } diff --git a/frontend/interfaces/policy.ts b/frontend/interfaces/policy.ts index 41586ea22d..056ab70406 100644 --- a/frontend/interfaces/policy.ts +++ b/frontend/interfaces/policy.ts @@ -36,7 +36,7 @@ export interface IPolicy { author_email: string; resolution: string; platform: SelectedPlatformString; - team_id: number | null; + team_id?: number; created_at: string; updated_at: string; critical: boolean; @@ -74,16 +74,14 @@ export interface IHostPolicy extends IPolicy { response: PolicyStatusResponse; } -// Policies API can return {} export interface ILoadAllPoliciesResponse { - policies?: IPolicyStats[]; + policies: IPolicyStats[]; } -// Team policies API can return {} export interface ILoadTeamPoliciesResponse { - policies?: IPolicyStats[]; + policies: IPolicyStats[]; + inherited_policies: IPolicyStats[]; } - export interface IPolicyFormData { description?: string | number | boolean | undefined; resolution?: string | number | boolean | undefined; @@ -91,7 +89,7 @@ export interface IPolicyFormData { platform?: SelectedPlatformString; name?: string | number | boolean | undefined; query?: string | number | boolean | undefined; - team_id?: number | null; + team_id?: number; id?: number; calendar_events_enabled?: boolean; } diff --git a/frontend/interfaces/query.ts b/frontend/interfaces/query.ts index 9485f2da9a..cad78f3745 100644 --- a/frontend/interfaces/query.ts +++ b/frontend/interfaces/query.ts @@ -1,6 +1,7 @@ import { IFormField } from "./form_field"; import { IPack } from "./pack"; -import { ISchedulableQuery, ISchedulableQueryStats } from "./schedulable_query"; +import { ISchedulableQuery } from "./schedulable_query"; +import { IScheduledQueryStats } from "./scheduled_query_stats"; export interface IEditQueryFormData { description?: string | number | boolean | undefined; @@ -31,7 +32,7 @@ export interface IQuery { author_email: string; observer_can_run: boolean; packs: IPack[]; - stats?: ISchedulableQueryStats; + stats?: IScheduledQueryStats; } export interface IEditQueryFormFields { diff --git a/frontend/interfaces/query_stats.ts b/frontend/interfaces/query_stats.ts index eaa7f1e159..edc319fa61 100644 --- a/frontend/interfaces/query_stats.ts +++ b/frontend/interfaces/query_stats.ts @@ -1,8 +1,8 @@ import PropTypes, { number } from "prop-types"; -import ILegacySchedulableQueryStats, { - ISchedulableQueryStats, -} from "./schedulable_query"; +import scheduledQueryStatsInterface, { + IScheduledQueryStats, +} from "./scheduled_query_stats"; export default PropTypes.shape({ scheduled_query_name: PropTypes.string, @@ -20,7 +20,7 @@ export default PropTypes.shape({ system_time: PropTypes.number, user_time: PropTypes.number, wall_time: PropTypes.number, - stats: ILegacySchedulableQueryStats, + stats: scheduledQueryStatsInterface, }); export interface IQueryStats { @@ -42,5 +42,5 @@ export interface IQueryStats { system_time: number; user_time: number; wall_time?: number; - stats?: ISchedulableQueryStats; + stats?: IScheduledQueryStats; } diff --git a/frontend/interfaces/schedulable_query.ts b/frontend/interfaces/schedulable_query.ts index 89d56dc726..8e167eeec0 100644 --- a/frontend/interfaces/schedulable_query.ts +++ b/frontend/interfaces/schedulable_query.ts @@ -1,6 +1,3 @@ -// for legacy legacy query stats interface -import PropTypes from "prop-types"; - import { IFormField } from "./form_field"; import { IPack } from "./pack"; import { SelectedPlatformString, SupportedPlatform } from "./platform"; @@ -27,7 +24,7 @@ export interface ISchedulableQuery { discard_data: boolean; packs: IPack[]; stats: ISchedulableQueryStats; - editingExistingQuery?: boolean; + editingExistingQuery: boolean; } export interface IEnhancedQuery extends ISchedulableQuery { @@ -35,22 +32,13 @@ export interface IEnhancedQuery extends ISchedulableQuery { platforms: SupportedPlatform[]; } export interface ISchedulableQueryStats { - user_time_p50?: number | null; - user_time_p95?: number | null; - system_time_p50?: number | null; - system_time_p95?: number | null; + user_time_p50?: number; + user_time_p95?: number; + system_time_p50?: number; + system_time_p95?: number; total_executions?: number; } -// legacy -export default PropTypes.shape({ - user_time_p50: PropTypes.number, - user_time_p95: PropTypes.number, - system_time_p50: PropTypes.number, - system_time_p95: PropTypes.number, - total_executions: PropTypes.number, -}); - // API shapes // Get a query by id diff --git a/frontend/interfaces/scheduled_query.ts b/frontend/interfaces/scheduled_query.ts index 48239007cc..779042a956 100644 --- a/frontend/interfaces/scheduled_query.ts +++ b/frontend/interfaces/scheduled_query.ts @@ -1,8 +1,7 @@ -// legacy interfaces to maintain packs support import PropTypes from "prop-types"; -import ILegacySchedulableQueryStats, { - ISchedulableQueryStats, -} from "interfaces/schedulable_query"; +import scheduledQueryStatsInterface, { + IScheduledQueryStats, +} from "./scheduled_query_stats"; export default PropTypes.shape({ created_at: PropTypes.string, @@ -20,7 +19,7 @@ export default PropTypes.shape({ version: PropTypes.string, shard: PropTypes.number, denylist: PropTypes.bool, - stats: ILegacySchedulableQueryStats, + stats: scheduledQueryStatsInterface, }); export interface IPackQueryFormData { @@ -53,7 +52,7 @@ export interface IScheduledQuery { shard?: number | undefined; denylist?: boolean; logging_type?: string; - stats: ISchedulableQueryStats; + stats: IScheduledQueryStats; team_id?: number; } export interface IEditScheduledQuery extends IScheduledQuery { diff --git a/frontend/interfaces/scheduled_query_stats.ts b/frontend/interfaces/scheduled_query_stats.ts new file mode 100644 index 0000000000..f5e8fb5e75 --- /dev/null +++ b/frontend/interfaces/scheduled_query_stats.ts @@ -0,0 +1,17 @@ +import PropTypes from "prop-types"; + +export default PropTypes.shape({ + user_time_p50: PropTypes.number, + user_time_p95: PropTypes.number, + system_time_p50: PropTypes.number, + system_time_p95: PropTypes.number, + total_executions: PropTypes.number, +}); + +export interface IScheduledQueryStats { + user_time_p50?: number; + user_time_p95?: number; + system_time_p50?: number; + system_time_p95?: number; + total_executions?: number; +} diff --git a/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tsx b/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tsx index 8c6dd2a85f..ef86fd4c19 100644 --- a/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tsx +++ b/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tsx @@ -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 TODO: Improve this type + location: any; // no type in react-router v3 } const CSV_HOSTS_TITLE = "Hosts"; diff --git a/frontend/pages/policies/ManagePoliciesPage/ManagePoliciesPage.tsx b/frontend/pages/policies/ManagePoliciesPage/ManagePoliciesPage.tsx index a17b43fb2d..7223973e29 100644 --- a/frontend/pages/policies/ManagePoliciesPage/ManagePoliciesPage.tsx +++ b/frontend/pages/policies/ManagePoliciesPage/ManagePoliciesPage.tsx @@ -2,7 +2,7 @@ import React, { useCallback, useContext, useEffect, useState } from "react"; import { useQuery } from "react-query"; import { InjectedRouter } from "react-router/lib/Router"; import PATHS from "router/paths"; -import { isEqual } from "lodash"; +import { noop, isEqual } from "lodash"; import { getNextLocationPath } from "utilities/helpers"; @@ -18,7 +18,6 @@ import { ILoadAllPoliciesResponse, ILoadTeamPoliciesResponse, IPoliciesCountResponse, - IPolicy, } from "interfaces/policy"; import { ITeamConfig } from "interfaces/team"; @@ -37,6 +36,7 @@ import { ITableQueryData } from "components/TableContainer/TableContainer"; import Button from "components/buttons/Button"; // @ts-ignore import Dropdown from "components/forms/fields/Dropdown"; +import RevealButton from "components/buttons/RevealButton"; import Spinner from "components/Spinner"; import TeamsDropdown from "components/TeamsDropdown"; import TableDataError from "components/DataError"; @@ -62,6 +62,10 @@ interface IManagePoliciesPageProps { order_key?: string; order_direction?: "asc" | "desc"; page?: string; + inherited_table?: "true"; + inherited_order_key?: string; + inherited_order_direction?: "asc" | "desc"; + inherited_page?: string; }; search: string; }; @@ -134,10 +138,9 @@ const ManagePolicyPage = ({ const [showAddPolicyModal, setShowAddPolicyModal] = useState(false); const [showDeletePolicyModal, setShowDeletePolicyModal] = useState(false); const [showCalendarEventsModal, setShowCalendarEventsModal] = useState(false); - const [ - policiesAvailableToAutomate, - setPoliciesAvailableToAutomate, - ] = useState([]); + + const [teamPolicies, setTeamPolicies] = useState(); + const [inheritedPolicies, setInheritedPolicies] = useState(); // Functions to avoid race conditions const initialSearchQuery = (() => queryParams.query ?? "")(); @@ -149,15 +152,40 @@ const ManagePolicyPage = ({ DEFAULT_SORT_DIRECTION)(); const initialPage = (() => queryParams && queryParams.page ? parseInt(queryParams?.page, 10) : 0)(); + const initialShowInheritedTable = (() => + queryParams && queryParams.inherited_table === "true")(); + const initialInheritedSortHeader = (() => + (queryParams?.inherited_order_key as "name" | "failing_host_count") ?? + DEFAULT_SORT_COLUMN)(); + const initialInheritedSortDirection = (() => + (queryParams?.inherited_order_direction as "asc" | "desc") ?? + DEFAULT_SORT_DIRECTION)(); + const initialInheritedPage = (() => + queryParams && queryParams.inherited_page + ? parseInt(queryParams?.inherited_page, 10) + : 0)(); + + const showInheritedTable = initialShowInheritedTable; // Needs update on location change or table state might not match URL const [searchQuery, setSearchQuery] = useState(initialSearchQuery); const [page, setPage] = useState(initialPage); + const [inheritedPage, setInheritedPage] = useState(initialInheritedPage); const [tableQueryData, setTableQueryData] = useState(); + const [ + inheritedTableQueryData, + setInheritedTableQueryData, + ] = useState(); const [sortHeader, setSortHeader] = useState(initialSortHeader); const [sortDirection, setSortDirection] = useState< "asc" | "desc" | undefined >(initialSortDirection); + const [inheritedSortDirection, setInheritedSortDirection] = useState( + initialInheritedSortDirection + ); + const [inheritedSortHeader, setInheritedSortHeader] = useState( + initialInheritedSortHeader + ); useEffect(() => { setLastEditedQueryPlatform(null); @@ -171,6 +199,9 @@ const ManagePolicyPage = ({ setSearchQuery(initialSearchQuery); setSortHeader(initialSortHeader); setSortDirection(initialSortDirection); + setInheritedPage(initialInheritedPage); + setInheritedSortHeader(initialInheritedSortHeader); + setInheritedSortDirection(initialInheritedSortDirection); }, [location, isRouteOk]); useEffect(() => { @@ -215,11 +246,8 @@ const ManagePolicyPage = ({ }, { enabled: isRouteOk && !isAnyTeamSelected, - select: (data) => data.policies || [], + select: (data) => data.policies, staleTime: 5000, - onSuccess: (data) => { - setPoliciesAvailableToAutomate(data || []); - }, } ); @@ -232,7 +260,7 @@ const ManagePolicyPage = ({ [ { scope: "policiesCount", - query: isAnyTeamSelected ? "" : searchQuery, + query: isAnyTeamSelected ? "" : searchQuery, // Search query not used for inherited count }, ], ({ queryKey }) => globalPoliciesAPI.getCount(queryKey[0]), @@ -246,14 +274,13 @@ const ManagePolicyPage = ({ ); const { - data: teamPolicies, error: teamPoliciesError, isFetching: isFetchingTeamPolicies, refetch: refetchTeamPolicies, } = useQuery< ILoadTeamPoliciesResponse, Error, - IPolicyStats[], + ILoadTeamPoliciesResponse, ITeamPoliciesQueryKey[] >( [ @@ -264,8 +291,11 @@ const ManagePolicyPage = ({ query: searchQuery, orderDirection: sortDirection, orderKey: sortHeader, + inheritedPage: inheritedTableQueryData?.pageIndex, + inheritedPerPage: DEFAULT_PAGE_SIZE, + inheritedOrderDirection: inheritedSortDirection, + inheritedOrderKey: inheritedSortHeader, teamId: teamIdForApi || 0, - mergeInherited: !!teamIdForApi, }, ], ({ queryKey }) => { @@ -273,44 +303,13 @@ const ManagePolicyPage = ({ }, { enabled: isRouteOk && isPremiumTier && !!teamIdForApi, - select: (data: ILoadTeamPoliciesResponse) => data.policies || [], onSuccess: (data) => { - const allPoliciesAvailableToAutomate = data.filter( - (policy: IPolicy) => policy.team_id === currentTeamId - ); - setPoliciesAvailableToAutomate(allPoliciesAvailableToAutomate || []); + setTeamPolicies(data.policies); + setInheritedPolicies(data.inherited_policies); }, } ); - const { - data: teamPoliciesCountMergeInherited, - isFetching: isFetchingTeamCountMergeInherited, - refetch: refetchTeamPoliciesCountMergeInherited, - } = useQuery< - IPoliciesCountResponse, - Error, - number, - ITeamPoliciesCountQueryKey[] - >( - [ - { - scope: "teamPoliciesCountMergeInherited", - query: searchQuery, - teamId: teamIdForApi || 0, // TODO: Fix number/undefined type - mergeInherited: !!teamIdForApi, - }, - ], - ({ queryKey }) => teamPoliciesAPI.getCount(queryKey[0]), - { - enabled: isRouteOk && !!teamIdForApi, - keepPreviousData: true, - refetchOnWindowFocus: false, - retry: 1, - select: (data) => data.count, - } - ); - const { data: teamPoliciesCount, isFetching: isFetchingTeamCount, @@ -326,7 +325,6 @@ const ManagePolicyPage = ({ scope: "teamPoliciesCount", query: searchQuery, teamId: teamIdForApi || 0, // TODO: Fix number/undefined type - mergeInherited: false, }, ], ({ queryKey }) => teamPoliciesAPI.getCount(queryKey[0]), @@ -339,10 +337,9 @@ const ManagePolicyPage = ({ } ); - const canAddOrDeletePolicy = + const canAddOrDeletePolicy: boolean = isGlobalAdmin || isGlobalMaintainer || isTeamMaintainer || isTeamAdmin; - const canManageAutomations = isGlobalAdmin || isTeamAdmin; - const hasPoliciesToAutomateOrDelete = policiesAvailableToAutomate.length > 0; + const canManageAutomations: boolean = isGlobalAdmin || isTeamAdmin; const { data: config, @@ -378,7 +375,6 @@ const ManagePolicyPage = ({ const refetchPolicies = (teamId?: number) => { if (teamId) { refetchTeamPolicies(); - refetchTeamPoliciesCountMergeInherited(); refetchTeamPoliciesCount(); } else { refetchGlobalPolicies(); // Only call on global policies as this is expensive @@ -395,36 +391,72 @@ const ManagePolicyPage = ({ ); // TODO: Look into useDebounceCallback with dependencies + // Inherited table uses the same onQueryChange function but routes to different URL params const onQueryChange = useCallback( async (newTableQuery: ITableQueryData) => { if (!isRouteOk || isEqual(newTableQuery, tableQueryData)) { return; } - setTableQueryData({ ...newTableQuery }); + newTableQuery.editingInheritedTable + ? setInheritedTableQueryData({ ...newTableQuery }) + : setTableQueryData({ ...newTableQuery }); const { pageIndex: newPageIndex, searchQuery: newSearchQuery, sortDirection: newSortDirection, sortHeader: newSortHeader, + editingInheritedTable, } = newTableQuery; // Rebuild queryParams to dispatch new browser location to react-router const newQueryParams: { [key: string]: string | number | undefined } = {}; newQueryParams.query = newSearchQuery; - newQueryParams.order_key = newSortHeader; - newQueryParams.order_direction = newSortDirection; - newQueryParams.page = newPageIndex.toString(); + // Updates main policy table URL params + // No change to inherited policy table URL params + if (!editingInheritedTable) { + newQueryParams.order_key = newSortHeader; + newQueryParams.order_direction = newSortDirection; + newQueryParams.page = newPageIndex.toString(); + if (showInheritedTable) { + newQueryParams.inherited_order_key = inheritedSortHeader; + newQueryParams.inherited_order_direction = inheritedSortDirection; + newQueryParams.inherited_page = inheritedPage.toString(); + } + // Reset page number to 0 for new filters + if ( + newSortDirection !== sortDirection || + newSortHeader !== sortHeader || + newSearchQuery !== searchQuery + ) { + newQueryParams.page = "0"; + } + } - // Reset page number to 0 for new filters - if ( - newSortDirection !== sortDirection || - newSortHeader !== sortHeader || - newSearchQuery !== searchQuery - ) { - newQueryParams.page = "0"; + if (showInheritedTable) { + newQueryParams.inherited_table = + showInheritedTable && showInheritedTable.toString(); + } + + // Updates inherited policy table URL params + // No change to main policy table URL params + if (showInheritedTable && editingInheritedTable) { + newQueryParams.inherited_order_key = newSortHeader; + newQueryParams.inherited_order_direction = newSortDirection; + newQueryParams.inherited_page = newPageIndex.toString(); + newQueryParams.order_key = sortHeader; + newQueryParams.order_direction = sortDirection; + newQueryParams.page = page.toString(); + newQueryParams.query = searchQuery; + // Reset page number to 0 for new filters + if ( + newSortDirection !== inheritedSortDirection || + newSortHeader !== inheritedSortHeader + ) { + newQueryParams.inherited_page = "0"; + } } if (isRouteOk && teamIdForApi !== undefined) { @@ -438,7 +470,14 @@ const ManagePolicyPage = ({ router?.replace(locationPath); }, - [isRouteOk, teamIdForApi, searchQuery, sortDirection] // Other dependencies can cause infinite re-renders as URL is source of truth + [ + isRouteOk, + teamIdForApi, + searchQuery, + showInheritedTable, + inheritedSortDirection, + sortDirection, + ] // Other dependencies can cause infinite re-renders as URL is source of truth ); const toggleOtherWorkflowsModal = () => @@ -465,6 +504,19 @@ const ManagePolicyPage = ({ } }; + const toggleShowInheritedPolicies = () => { + // URL source of truth + const locationPath = getNextLocationPath({ + pathPrefix: PATHS.MANAGE_POLICIES, + queryParams: { + ...queryParams, + inherited_table: showInheritedTable ? undefined : "true", + inherited_page: showInheritedTable ? undefined : "0", + }, + }); + router?.replace(locationPath); + }; + const handleUpdateOtherWorkflows = async (requestBody: { webhook_settings: Pick; integrations: IZendeskJiraIntegrations; @@ -522,7 +574,7 @@ const ManagePolicyPage = ({ // update changed policies calendar events enabled const changedPolicies = formData.policies.filter((formPolicy) => { - const prevPolicyState = policiesAvailableToAutomate.find( + const prevPolicyState = teamPolicies?.find( (policy) => policy.id === formPolicy.id ); return ( @@ -597,6 +649,24 @@ const ManagePolicyPage = ({ } }; + const inheritedPoliciesButtonText = ( + showPolicies: boolean, + count: number + ) => { + return `${showPolicies ? "Hide" : "Show"} ${count} inherited ${ + count > 1 ? "policies" : "policy" + }`; + }; + + const showInheritedPoliciesButton = + isAnyTeamSelected && + !isFetchingTeamPolicies && + !teamPoliciesError && + !!inheritedPolicies?.length; // Returned with team policies + + const availablePoliciesForAutomation = + (isAnyTeamSelected ? teamPolicies : globalPolicies) || []; + const policiesErrors = isAnyTeamSelected ? teamPoliciesError : globalPoliciesError; @@ -661,15 +731,13 @@ const ManagePolicyPage = ({ onAddPolicyClick={onAddPolicyClick} onDeletePolicyClick={onDeletePolicyClick} canAddOrDeletePolicy={canAddOrDeletePolicy} - hasPoliciesToDelete={hasPoliciesToAutomateOrDelete} currentTeam={currentTeamSummary} currentAutomatedPolicies={currentAutomatedPolicies} renderPoliciesCount={() => - (!isFetchingTeamCountMergeInherited && - renderPoliciesCount(teamPoliciesCountMergeInherited)) || - null + !isFetchingTeamCount && renderPoliciesCount(teamPoliciesCount) } isPremiumTier={isPremiumTier} + isSandboxMode={isSandboxMode} searchQuery={searchQuery} sortHeader={sortHeader} sortDirection={sortDirection} @@ -685,14 +753,12 @@ const ManagePolicyPage = ({ onAddPolicyClick={onAddPolicyClick} onDeletePolicyClick={onDeletePolicyClick} canAddOrDeletePolicy={canAddOrDeletePolicy} - hasPoliciesToDelete={hasPoliciesToAutomateOrDelete} currentTeam={currentTeamSummary} currentAutomatedPolicies={currentAutomatedPolicies} isPremiumTier={isPremiumTier} + isSandboxMode={isSandboxMode} renderPoliciesCount={() => - (!isFetchingGlobalCount && - renderPoliciesCount(globalPoliciesCount)) || - null + !isFetchingGlobalCount && renderPoliciesCount(globalPoliciesCount) } searchQuery={searchQuery} sortHeader={sortHeader} @@ -768,19 +834,17 @@ const ManagePolicyPage = ({ {showCtaButtons && (
- {canManageAutomations && - automationsConfig && - hasPoliciesToAutomateOrDelete && ( -
- -
- )} + {canManageAutomations && automationsConfig && ( +
+ +
+ )} {canAddOrDeletePolicy && (
)} @@ -803,11 +867,57 @@ const ManagePolicyPage = ({

{renderMainTable()} + {showInheritedPoliciesButton && globalPoliciesCount && ( + + "All teams" policies are checked +
+ for this team's hosts. + + } + onClick={toggleShowInheritedPolicies} + /> + )} + {showInheritedPoliciesButton && showInheritedTable && ( +
+ {globalPoliciesError && } + {!globalPoliciesError && ( + + renderPoliciesCount(teamPoliciesCount) + } + sortHeader={inheritedSortHeader} + sortDirection={inheritedSortDirection} + page={inheritedPage} + onQueryChange={onQueryChange} + /> + )} +
+ )} {config && automationsConfig && showOtherWorkflowsModal && ( )} diff --git a/frontend/pages/policies/ManagePoliciesPage/_styles.scss b/frontend/pages/policies/ManagePoliciesPage/_styles.scss index ca0978671d..0b7a6b064e 100644 --- a/frontend/pages/policies/ManagePoliciesPage/_styles.scss +++ b/frontend/pages/policies/ManagePoliciesPage/_styles.scss @@ -165,8 +165,8 @@ } } - .critical-tooltip, - .inherited-tooltip { + .critical-tooltip { + text-align: left; font-weight: $regular; } @@ -184,13 +184,8 @@ display: flex; // required for inline icon gap: $pad-xsmall; - // Underlines only the name text - &:hover { - text-decoration: none; - - .policy-name-text { - text-decoration: underline; - } + .tooltip-base { + display: inline-flex; } .policy-name-text { @@ -199,25 +194,6 @@ } } } - - .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 f100e4761b..426de14942 100644 --- a/frontend/pages/policies/ManagePoliciesPage/components/PoliciesTable/PoliciesTable.tests.tsx +++ b/frontend/pages/policies/ManagePoliciesPage/components/PoliciesTable/PoliciesTable.tests.tsx @@ -1,84 +1,15 @@ import React from "react"; -import { screen, waitFor } from "@testing-library/react"; +import { render, screen } 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", () => { - it("Renders the page-wide empty state when no policies are present", async () => { - const render = createCustomRenderer({ - context: { - app: { - isGlobalAdmin: true, - currentUser: createMockUser(), - }, - }, - }); + const testCriticalPolicy = createMockPolicy({ critical: true }); + it("Renders a tooltip including 'Premium feature' copy for a critical policy in Sandbox mode", () => { render( - {}} - currentTeam={{ id: -1, name: "All teams" }} - isPremiumTier - searchQuery="" - page={0} - onQueryChange={noop} - renderPoliciesCount={() => null} - /> - ); - - expect(screen.getByText("You don't have any policies")).toBeInTheDocument(); - expect(screen.queryByText("Name")).toBeNull(); - }); - - 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 searchQuery="" page={0} onQueryChange={noop} - renderPoliciesCount={() => null} + renderPoliciesCount={noop} /> ); - await waitFor(() => { - waitFor(() => { - user.hover(screen.getByTestId("policy-icon")); - }); - - expect( - screen.getByText("This policy has been marked as critical.") - ).toBeInTheDocument(); - }); + expect( + screen.getByText("This policy has been marked as critical.", { + exact: false, + }) + ).toBeInTheDocument(); + expect( + screen.getByText("This is a premium feature.", { exact: false }) + ).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 }); - + it("Renders a tooltip excluding 'Premium feature' copy for a critical policy not in Sandbox mode", () => { render( {}} currentTeam={{ id: -1, name: "All teams" }} isPremiumTier + isSandboxMode={false} searchQuery="" page={0} onQueryChange={noop} - renderPoliciesCount={() => null} + renderPoliciesCount={noop} /> ); - 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); - }); + expect( + screen.getByText("This policy has been marked as critical.", { + exact: false, + }) + ).toBeInTheDocument(); + expect( + screen.queryByText("This is a premium feature.", { exact: false }) + ).toBeNull(); }); }); diff --git a/frontend/pages/policies/ManagePoliciesPage/components/PoliciesTable/PoliciesTable.tsx b/frontend/pages/policies/ManagePoliciesPage/components/PoliciesTable/PoliciesTable.tsx index 9047242c8b..9494e49805 100644 --- a/frontend/pages/policies/ManagePoliciesPage/components/PoliciesTable/PoliciesTable.tsx +++ b/frontend/pages/policies/ManagePoliciesPage/components/PoliciesTable/PoliciesTable.tsx @@ -1,5 +1,6 @@ import React, { useContext } from "react"; import { AppContext } from "context/app"; +import PATHS from "router/paths"; import { IPolicyStats } from "interfaces/policy"; import { ITeamSummary } from "interfaces/team"; @@ -13,6 +14,12 @@ import { generateTableHeaders, generateDataSet } from "./PoliciesTableConfig"; const baseClass = "policies-table"; +const TAGGED_TEMPLATES = { + hostsByTeamRoute: (teamId: number | undefined | null) => { + return `${teamId ? `/?team_id=${teamId}` : ""}`; + }, +}; + const DEFAULT_SORT_DIRECTION = "asc"; const DEFAULT_SORT_HEADER = "name"; @@ -22,11 +29,13 @@ interface IPoliciesTableProps { onAddPolicyClick?: () => void; onDeletePolicyClick: (selectedTableIds: number[]) => void; canAddOrDeletePolicy?: boolean; - hasPoliciesToDelete?: boolean; + tableType?: "inheritedPolicies"; currentTeam: ITeamSummary | undefined; currentAutomatedPolicies?: number[]; isPremiumTier?: boolean; - renderPoliciesCount: () => JSX.Element | null; + isSandboxMode?: boolean; + // onClientSidePaginationChange?: (pageIndex: number) => void; + renderPoliciesCount: any; // TODO: typing onQueryChange: (newTableQuery: ITableQueryData) => void; searchQuery: string; sortHeader?: "name" | "failing_host_count"; @@ -40,11 +49,13 @@ const PoliciesTable = ({ onAddPolicyClick, onDeletePolicyClick, canAddOrDeletePolicy, - hasPoliciesToDelete, + tableType, currentTeam, currentAutomatedPolicies, isPremiumTier, + isSandboxMode, onQueryChange, + // onClientSidePaginationChange, renderPoliciesCount, searchQuery, sortHeader, @@ -53,18 +64,23 @@ const PoliciesTable = ({ }: IPoliciesTableProps): JSX.Element => { const { config } = useContext(AppContext); + // Inherited table uses the same onQueryChange but require different URL params const onTableQueryChange = (newTableQuery: ITableQueryData) => { onQueryChange({ ...newTableQuery, + editingInheritedTable: tableType === "inheritedPolicies", }); }; const emptyState = () => { const emptyPolicies: IEmptyTableProps = { graphicName: "empty-policies", - header: "You don't have any policies", - info: - "Add policies to detect device health issues and trigger automations.", + header: <>You don't have any policies, + info: ( + <> + Add policies to detect device health issues and trigger automations. + + ), }; if (canAddOrDeletePolicy) { emptyPolicies.primaryButton = ( @@ -80,8 +96,9 @@ const PoliciesTable = ({ if (searchQuery) { delete emptyPolicies.graphicName; delete emptyPolicies.primaryButton; - emptyPolicies.header = "No matching policies"; - emptyPolicies.info = "No policies match the current filters."; + emptyPolicies.header = "No policies match the current search criteria."; + emptyPolicies.info = + "Expecting to see policies? Try again in a few seconds as the system catches up."; } return emptyPolicies; @@ -89,20 +106,23 @@ const PoliciesTable = ({ const searchable = !(policiesList?.length === 0 && searchQuery === ""); - const hasPermissionAndPoliciesToDelete = - canAddOrDeletePolicy && hasPoliciesToDelete; - return ( -
+
{ const getTooltip = (osqueryPolicyMs: number): JSX.Element => { return ( - <> + Fleet is collecting policy results. Try again
in about {getPolicyRefreshTime(osqueryPolicyMs)} as the system catches up. - +
); }; @@ -100,14 +101,15 @@ const getTooltip = (osqueryPolicyMs: number): JSX.Element => { const generateTableHeaders = ( options: { selectedTeamId?: number | null; - hasPermissionAndPoliciesToDelete?: boolean; + canAddOrDeletePolicy?: boolean; tableType?: string; }, policiesList: IPolicyStats[] = [], - isPremiumTier?: boolean + isPremiumTier?: boolean, + isSandboxMode?: boolean ): IDataColumn[] => { - const { selectedTeamId, hasPermissionAndPoliciesToDelete } = options; - const viewingTeamPolicies = selectedTeamId !== -1; + const { selectedTeamId, tableType, canAddOrDeletePolicy } = options; + // Figure the time since the host counts were updated. // First, find first policy item with host_count_updated_at. const updatedAt = @@ -142,10 +144,11 @@ const generateTableHeaders = ( <>
{cellProps.cell.value}
{isPremiumTier && cellProps.row.original.critical && ( -
+ <> - This policy has been marked as critical. - -
- )} - {viewingTeamPolicies && !cellProps.row.original.team_id && ( - + {isSandboxMode && ( + <> +
+ This is a premium feature. + + )} + + )} } @@ -203,24 +208,24 @@ const generateTableHeaders = ( ); } return ( -
+ <> --- - {getTooltip(cellProps.row.original.next_update_ms)} - -
+ + ); }, }, @@ -254,73 +259,52 @@ const generateTableHeaders = ( ); } return ( -
+ <> --- - {getTooltip(cellProps.row.original.next_update_ms)} - -
+ + ); }, sortType: "caseInsensitive", }, ]; - if (hasPermissionAndPoliciesToDelete) { + if (tableType !== "inheritedPolicies") { + if (!canAddOrDeletePolicy) { + return tableHeaders; + } + tableHeaders.unshift({ id: "selection", - Header: (headerProps: any) => { - // When viewing team policies select all checkbox accounts for not selecting inherited policies - const teamCheckboxProps = getConditionalSelectHeaderCheckboxProps({ - headerProps, - checkIfRowIsSelectable: (row) => row.original.team_id !== null, - }); - - // Regular table selection logic - const { - getToggleAllRowsSelectedProps, - toggleAllRowsSelected, - } = headerProps; - const { checked, indeterminate } = getToggleAllRowsSelectedProps(); - - const regularCheckboxProps = { - value: checked, - indeterminate, - onChange: () => { - toggleAllRowsSelected(); - }, + Header: (cellProps: IHeaderProps) => { + const props = cellProps.getToggleAllRowsSelectedProps(); + const checkboxProps = { + value: props.checked, + indeterminate: props.indeterminate, + onChange: () => cellProps.toggleAllRowsSelected(), }; - - const checkboxProps = viewingTeamPolicies - ? teamCheckboxProps - : regularCheckboxProps; return ; }, Cell: (cellProps: ICellProps): JSX.Element => { - const inheritedPolicy = !cellProps.row.original.team_id; const props = cellProps.row.getToggleRowSelectedProps(); const checkboxProps = { value: props.checked, onChange: () => cellProps.row.toggleRowSelected(), }; - - // When viewing team policies and a row is an inherited policy, do not render checkbox - if (viewingTeamPolicies && inheritedPolicy) { - return <>; - } - return ; }, disableHidden: true, diff --git a/frontend/pages/policies/ManagePoliciesPage/components/PoliciesTable/_styles.scss b/frontend/pages/policies/ManagePoliciesPage/components/PoliciesTable/_styles.scss index 1d8354c466..9913e4d229 100644 --- a/frontend/pages/policies/ManagePoliciesPage/components/PoliciesTable/_styles.scss +++ b/frontend/pages/policies/ManagePoliciesPage/components/PoliciesTable/_styles.scss @@ -26,6 +26,10 @@ } } +.has-not-run { + width: 20px; +} + .no-team-policy { border: 1px solid #e2e4ea; box-sizing: border-box; diff --git a/frontend/pages/policies/PolicyPage/components/PolicyErrorsTable/PolicyErrorsTable.tsx b/frontend/pages/policies/PolicyPage/components/PolicyErrorsTable/PolicyErrorsTable.tsx index 0b38f9a628..7948d73c66 100644 --- a/frontend/pages/policies/PolicyPage/components/PolicyErrorsTable/PolicyErrorsTable.tsx +++ b/frontend/pages/policies/PolicyPage/components/PolicyErrorsTable/PolicyErrorsTable.tsx @@ -26,7 +26,11 @@ const PolicyErrorsTable = ({ canAddOrDeletePolicy, }: IPolicyErrorsTableProps): JSX.Element => { return ( -
+
{ return ( -
+
{ return platforms ?? []; }; -export const enhanceQuery = (q: ISchedulableQuery): IEnhancedQuery => { +const enhanceQuery = (q: ISchedulableQuery): IEnhancedQuery => { return { ...q, performance: getPerformanceImpactDescription( @@ -116,16 +120,14 @@ const ManageQueriesPage = ({ ); const [showPreviewDataModal, setShowPreviewDataModal] = useState(false); const [isUpdatingQueries, setIsUpdatingQueries] = useState(false); + const [showInheritedQueries, setShowInheritedQueries] = useState(false); const [isUpdatingAutomations, setIsUpdatingAutomations] = useState(false); - const [queriesAvailableToAutomate, setQueriesAvailableToAutomate] = useState< - IEnhancedQuery[] | [] - >([]); const { - data: enhancedQueries, - error: queriesError, - isFetching: isFetchingQueries, - refetch: refetchQueries, + data: curTeamEnhancedQueries, + error: curTeamQueriesError, + isFetching: isFetchingCurTeamQueries, + refetch: refetchCurTeamQueries, } = useQuery< IEnhancedQuery[], Error, @@ -135,41 +137,46 @@ const ManageQueriesPage = ({ [{ scope: "queries", teamId: teamIdForApi }], ({ queryKey: [{ teamId }] }) => queriesAPI - .loadAll(teamId, teamId !== API_ALL_TEAMS_ID) + .loadAll(teamId) .then(({ queries }) => queries.map(enhanceQuery)), { refetchOnWindowFocus: false, enabled: isRouteOk, staleTime: 5000, - onSuccess: (data) => { - if (data) { - const enhancedAllQueries = data.map(enhanceQuery); - - const allQueriesAvailableToAutomate = teamIdForApi - ? enhancedAllQueries.filter( - (query: IEnhancedQuery) => query.team_id === currentTeamId - ) - : enhancedAllQueries; - - setQueriesAvailableToAutomate(allQueriesAvailableToAutomate); - } - }, } ); - const onlyInheritedQueries = useMemo(() => { - if (teamIdForApi === API_ALL_TEAMS_ID) { - // global scope - return false; + // If a team is selected, inherit global queries + const { + data: globalEnhancedQueries, + error: globalQueriesError, + isFetching: isFetchingGlobalQueries, + refetch: refetchGlobalQueries, + } = useQuery< + IEnhancedQuery[], + Error, + IEnhancedQuery[], + IQueryKeyQueriesLoadAll[] + >( + [{ scope: "queries", teamId: API_ALL_TEAMS_ID }], + ({ queryKey: [{ teamId }] }) => + queriesAPI + .loadAll(teamId) + .then(({ queries }) => queries.map(enhanceQuery)), + { + refetchOnWindowFocus: false, + enabled: isRouteOk && isAnyTeamSelected, + staleTime: 5000, } - return !enhancedQueries?.some((query) => query.team_id === teamIdForApi); - }, [teamIdForApi, enhancedQueries]); + ); const automatedQueryIds = useMemo(() => { - return queriesAvailableToAutomate - .filter((query) => query.automations_enabled) - .map((query) => query.id); - }, [queriesAvailableToAutomate]); + return curTeamEnhancedQueries + ? curTeamEnhancedQueries + .filter((query) => query.automations_enabled) + .map((query) => query.id) + : []; + }, [curTeamEnhancedQueries]); useEffect(() => { const path = location.pathname + location.search; @@ -197,6 +204,11 @@ const ManageQueriesPage = ({ setSelectedQueryIds(selectedTableQueryIds); }; + const refetchAllQueries = useCallback(() => { + refetchCurTeamQueries(); + refetchGlobalQueries(); + }, [refetchCurTeamQueries, refetchGlobalQueries]); + const toggleManageAutomationsModal = useCallback(() => { setShowManageAutomationsModal(!showManageAutomationsModal); }, [showManageAutomationsModal, setShowManageAutomationsModal]); @@ -225,7 +237,7 @@ const ManageQueriesPage = ({ `Successfully deleted ${bulk ? "queries" : "query"}.` ); setResetSelectedRows(true); - refetchQueries(); + refetchAllQueries(); } catch (errorResponse) { renderFlash( "error", @@ -237,7 +249,7 @@ const ManageQueriesPage = ({ toggleDeleteQueryModal(); setIsUpdatingQueries(false); } - }, [refetchQueries, selectedQueryIds, toggleDeleteQueryModal]); + }, [refetchAllQueries, selectedQueryIds, toggleDeleteQueryModal]); const renderHeader = () => { if (isPremiumTier) { @@ -259,18 +271,17 @@ const ManageQueriesPage = ({ return

Queries

; }; - const renderQueriesTable = () => { - if (isFetchingQueries) { + const renderCurrentScopeQueriesTable = () => { + if (isFetchingCurTeamQueries) { return ; } - if (queriesError) { + if (curTeamQueriesError) { return ; } return ( ); }; + const renderShowInheritedQueriesTableButton = () => { + const inheritedQueryCount = globalEnhancedQueries?.length; + return ( + + Queries from the "All teams" +
+ schedule run on this team's hosts. + + } + onClick={() => { + setShowInheritedQueries(!showInheritedQueries); + }} + /> + ); + }; + + const renderInheritedQueriesTable = () => { + if (isFetchingGlobalQueries) { + return ; + } + if (globalQueriesError) { + return ; + } + return ( + + ); + }; + + const renderInheritedQueriesSection = () => { + return ( + <> + {renderShowInheritedQueriesTableButton()} + {showInheritedQueries && renderInheritedQueriesTable()} + + ); + }; + const onSaveQueryAutomations = useCallback( async (newAutomatedQueryIds: any) => { setIsUpdatingAutomations(true); @@ -312,7 +382,7 @@ const ManageQueriesPage = ({ try { await Promise.all(updateAutomatedQueries).then(() => { renderFlash("success", `Successfully updated query automations.`); - refetchQueries(); + refetchAllQueries(); }); } catch (errorResponse) { renderFlash( @@ -324,7 +394,7 @@ const ManageQueriesPage = ({ setIsUpdatingAutomations(false); } }, - [refetchQueries, automatedQueryIds, toggleManageAutomationsModal] + [refetchAllQueries, automatedQueryIds, toggleManageAutomationsModal] ); const renderModals = () => { @@ -344,7 +414,7 @@ const ManageQueriesPage = ({ onCancel={toggleManageAutomationsModal} isShowingPreviewDataModal={showPreviewDataModal} togglePreviewDataModal={togglePreviewDataModal} - availableQueries={queriesAvailableToAutomate} + availableQueries={curTeamEnhancedQueries} automatedQueryIds={automatedQueryIds} logDestination={config?.logging.result.plugin || ""} /> @@ -365,28 +435,29 @@ const ManageQueriesPage = ({
{renderHeader()}
- {!!enhancedQueries?.length && ( -
- {(isGlobalAdmin || isTeamAdmin) && !onlyInheritedQueries && ( - +
+ {(isGlobalAdmin || isTeamAdmin) && ( + + )} + {(!isOnlyObserver || isObserverPlus || isAnyTeamObserverPlus) && + !!curTeamEnhancedQueries?.length && ( + <> + + )} - {(!isOnlyObserver || isObserverPlus || isAnyTeamObserverPlus) && ( - - )} -
- )} +

@@ -395,7 +466,11 @@ const ManageQueriesPage = ({ : "Gather data about all hosts."}

- {renderQueriesTable()} + {renderCurrentScopeQueriesTable()} + {isAnyTeamSelected && + globalEnhancedQueries && + globalEnhancedQueries?.length > 0 && + renderInheritedQueriesSection()} {renderModals()}
diff --git a/frontend/pages/queries/ManageQueriesPage/_styles.scss b/frontend/pages/queries/ManageQueriesPage/_styles.scss index 3f7cd25150..8db063b32d 100644 --- a/frontend/pages/queries/ManageQueriesPage/_styles.scss +++ b/frontend/pages/queries/ManageQueriesPage/_styles.scss @@ -96,8 +96,6 @@ } &__table { thead { - // maintain height when select header is removed - height: 52.3833px; .name__header { width: auto; } @@ -133,33 +131,15 @@ .query-name-cell { display: flex; // required for inline icon gap: $pad-xsmall; - text-decoration: none; - align-items: center; - &:hover { + + .children-wrapper { .query-name-text { - text-decoration: underline; - } - } - - .query-name-text { - text-overflow: ellipsis; - overflow: hidden; - } - - .inherited-badge { - overflow: initial; - } - .observer-can-run-badge { - @include tooltip5-arrow-styles; - - .react-tooltip { - @include tooltip-text; - font-style: normal; - text-align: center; + text-overflow: ellipsis; + overflow: hidden; } } } - .observer-can-run-query-icon { + .query-icon { display: block; } @@ -167,6 +147,9 @@ display: flex; gap: $pad-xsmall; } + .observer-can-run-tooltip { + font-weight: $regular; + } } @media (max-width: $break-md) { @@ -201,13 +184,6 @@ } } } - .empty-table { - &__additional-info { - * { - font-size: $xx-small; - } - } - } } .reveal-button { .component__tooltip-wrapper__underline { diff --git a/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTable.tests.tsx b/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTable.tests.tsx deleted file mode 100644 index 3269ebcc1e..0000000000 --- a/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTable.tests.tsx +++ /dev/null @@ -1,367 +0,0 @@ -import React from "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"; -import { enhanceQuery } from "../../ManageQueriesPage"; - -const testRawGlobalQueries: ISchedulableQuery[] = [ - { - created_at: "2024-03-22T19:01:20Z", - updated_at: "2024-03-22T19:01:20Z", - id: 1, - team_id: null, - interval: 0, - platform: "linux", - min_osquery_version: "", - automations_enabled: false, - logging: "snapshot", - name: "Global query 1", - description: "Retrieves the OpenSSL version.", - query: - "SELECT name AS name, version AS version, 'deb_packages' AS source FROM deb_packages WHERE name LIKE 'openssl%' UNION SELECT name AS name, version AS version, 'apt_sources' AS source FROM apt_sources WHERE name LIKE 'openssl%' UNION SELECT name AS name, version AS version, 'rpm_packages' AS source FROM rpm_packages WHERE name LIKE 'openssl%';", - saved: true, - observer_can_run: true, - author_id: 1, - author_name: "Tess Tuser", - author_email: "tess@fake.com", - packs: [], - stats: { - system_time_p50: null, - system_time_p95: null, - user_time_p50: null, - user_time_p95: null, - total_executions: 0, - }, - discard_data: false, - }, - { - created_at: "2024-03-22T19:01:20Z", - updated_at: "2024-03-22T19:01:20Z", - id: 2, - team_id: null, - interval: 0, - platform: "linux", - min_osquery_version: "", - automations_enabled: false, - logging: "snapshot", - name: "Global query 2", - description: "Retrieves the OpenSSL version.", - query: - "SELECT name AS name, version AS version, 'deb_packages' AS source FROM deb_packages WHERE name LIKE 'openssl%' UNION SELECT name AS name, version AS version, 'apt_sources' AS source FROM apt_sources WHERE name LIKE 'openssl%' UNION SELECT name AS name, version AS version, 'rpm_packages' AS source FROM rpm_packages WHERE name LIKE 'openssl%';", - saved: true, - observer_can_run: true, - author_id: 1, - author_name: "Tess Tuser", - author_email: "tess@fake.com", - packs: [], - stats: { - system_time_p50: null, - system_time_p95: null, - user_time_p50: null, - user_time_p95: null, - total_executions: 0, - }, - discard_data: false, - }, -]; - -const testRawTeamQueries: ISchedulableQuery[] = [ - { - created_at: "2024-04-25T04:16:09Z", - updated_at: "2024-04-25T04:16:09Z", - id: 3, - team_id: 1, - interval: 3600, - platform: "", - min_osquery_version: "", - automations_enabled: false, - logging: "snapshot", - name: "Team query 1", - description: "", - query: "SELECT * FROM osquery_info;", - saved: true, - observer_can_run: false, - author_id: 1, - author_name: "Tess Tuser", - author_email: "tess@fake.com", - packs: [], - stats: { - system_time_p50: null, - system_time_p95: null, - user_time_p50: null, - user_time_p95: null, - total_executions: 0, - }, - discard_data: false, - }, - { - created_at: "2024-04-25T04:16:09Z", - updated_at: "2024-04-25T04:16:09Z", - id: 4, - team_id: 1, - interval: 3600, - platform: "", - min_osquery_version: "", - automations_enabled: false, - logging: "snapshot", - name: "Team query 2", - description: "", - query: "SELECT * FROM osquery_info;", - saved: true, - observer_can_run: true, - author_id: 1, - author_name: "Tess Tuser", - author_email: "tess@fake.com", - packs: [], - stats: { - system_time_p50: null, - system_time_p95: null, - user_time_p50: null, - user_time_p95: null, - total_executions: 0, - }, - discard_data: false, - }, -]; - -const testGlobalQueries = testRawGlobalQueries.map(enhanceQuery); -const testTeamQueries = testRawTeamQueries.map(enhanceQuery); - -const renderAsPremiumGlobalAdmin = createCustomRenderer({ - context: { - app: { - isPremiumTier: true, - isGlobalAdmin: true, - currentUser: createMockUser(), - }, - }, -}); -describe("QueriesTable", () => { - it("Renders the page-wide empty state when no queries are present", () => { - const testData: IQueriesTableProps[] = [ - { - queriesList: [], - onlyInheritedQueries: false, - isLoading: false, - onDeleteQueryClick: jest.fn(), - onCreateQueryClick: jest.fn(), - isOnlyObserver: false, - isObserverPlus: false, - isAnyTeamObserverPlus: false, - currentTeamId: undefined, - }, - ]; - - testData.forEach((tableProps) => { - renderAsPremiumGlobalAdmin(); - expect( - screen.getByText("You don't have any queries") - ).toBeInTheDocument(); - expect(screen.queryByText("Frequency")).toBeNull(); - }); - }); - it("Renders inherited global queries and team queries when viewing a team, then renders the 'no-matching' empty state when a search string is entered that matches no queries", async () => { - const testData: IQueriesTableProps[] = [ - { - queriesList: [...testGlobalQueries, ...testTeamQueries], - onlyInheritedQueries: false, - isLoading: false, - onDeleteQueryClick: jest.fn(), - onCreateQueryClick: jest.fn(), - isOnlyObserver: false, - isObserverPlus: false, - isAnyTeamObserverPlus: false, - currentTeamId: 1, - }, - ]; - const dataStrings = [ - "Global query 1", - "Global query 2", - "Inherited", - "Frequency", - "Team query 1", - "Team query 2", - ]; - - testData.forEach(async (tableProps) => { - // will have no context to get current user from - const { user } = renderAsPremiumGlobalAdmin( - - ); - dataStrings.forEach((val) => { - expect(screen.getAllByText(val)[0]).toBeInTheDocument(); - }); - - await user.type( - screen.getByPlaceholderText("Search by name"), - "shouldn't match anything" - ); - expect(screen.getByText("No matching queries")).toBeInTheDocument(); - dataStrings.forEach((val) => { - expect(screen.getAllByText(val)).toHaveLength(0); - }); - }); - }); - - 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/QueriesTable.tsx b/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTable.tsx index ccc864cc1c..d58bd75bed 100644 --- a/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTable.tsx +++ b/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTable.tsx @@ -3,8 +3,8 @@ import React, { useContext, useCallback, useMemo } from "react"; import { InjectedRouter } from "react-router"; import { AppContext } from "context/app"; +import { IQuery } from "interfaces/query"; import { IEmptyTableProps } from "interfaces/empty_table"; -import { IEnhancedQuery } from "interfaces/schedulable_query"; import { ITableQueryData } from "components/TableContainer/TableContainer"; import PATHS from "router/paths"; import { getNextLocationPath } from "utilities/helpers"; @@ -17,9 +17,13 @@ import Dropdown from "components/forms/fields/Dropdown"; import generateColumnConfigs from "./QueriesTableConfig"; const baseClass = "queries-table"; -export interface IQueriesTableProps { - queriesList: IEnhancedQuery[] | null; - onlyInheritedQueries: boolean; + +interface IQueryTableData extends IQuery { + performance: string; + platforms: string[]; +} +interface IQueriesTableProps { + queriesList: IQueryTableData[] | null; isLoading: boolean; onDeleteQueryClick: (selectedTableQueryIds: number[]) => void; onCreateQueryClick: () => void; @@ -34,7 +38,11 @@ export interface IQueriesTableProps { order_key?: string; order_direction?: "asc" | "desc"; team_id?: string; + inherited_order_key?: string; + inherited_order_direction?: "asc" | "desc"; + inherited_page?: string; }; + isInherited?: boolean; currentTeamId?: number; } @@ -78,7 +86,6 @@ const PLATFORM_FILTER_OPTIONS = [ const QueriesTable = ({ queriesList, - onlyInheritedQueries, isLoading, onDeleteQueryClick, onCreateQueryClick, @@ -87,6 +94,7 @@ const QueriesTable = ({ isAnyTeamObserverPlus, router, queryParams, + isInherited = false, currentTeamId, }: IQueriesTableProps): JSX.Element | null => { const { currentUser } = useContext(AppContext); @@ -104,14 +112,28 @@ const QueriesTable = ({ DEFAULT_PLATFORM)(); const initialPage = (() => queryParams && queryParams.page ? parseInt(queryParams?.page, 10) : 0)(); + const initialInheritedSortHeader = (() => + (queryParams?.inherited_order_key as "name" | "failing_host_count") ?? + DEFAULT_SORT_HEADER)(); + const initialInheritedSortDirection = (() => + (queryParams?.inherited_order_direction as "asc" | "desc") ?? + DEFAULT_SORT_DIRECTION)(); + const initialInheritedPage = (() => + queryParams && queryParams.inherited_page + ? parseInt(queryParams?.inherited_page, 10) + : 0)(); // Source of truth is state held within TableContainer. That state is initialized using URL // params, then subsquent updates to that state are pushed to the URL. const searchQuery = initialSearchQuery; const platform = initialPlatform; - const page = initialPage; - const sortDirection = initialSortDirection; - const sortHeader = initialSortHeader; + const page = isInherited ? initialInheritedPage : initialPage; + const sortDirection = isInherited + ? initialInheritedSortDirection + : initialSortDirection; + const sortHeader = isInherited + ? initialInheritedSortHeader + : initialSortHeader; // TODO: Look into useDebounceCallback with dependencies const onQueryChange = useCallback( @@ -126,19 +148,37 @@ const QueriesTable = ({ // Rebuild queryParams to dispatch new browser location to react-router const newQueryParams: { [key: string]: string | number | undefined } = {}; - // Updates URL params - newQueryParams.order_key = newSortHeader; - newQueryParams.order_direction = newSortDirection; - newQueryParams.platform = platform; // must set from URL - newQueryParams.page = newPageIndex; - newQueryParams.query = newSearchQuery; - // Reset page number to 0 for new filters - if ( - newSortDirection !== sortDirection || - newSortHeader !== sortHeader || - newSearchQuery !== searchQuery - ) { - newQueryParams.page = "0"; + // Updates main query table URL params + // No change to inherited query table URL params + if (!isInherited) { + newQueryParams.order_key = newSortHeader; + newQueryParams.order_direction = newSortDirection; + newQueryParams.platform = platform; // must set from URL + newQueryParams.page = newPageIndex; + newQueryParams.query = newSearchQuery; + // Reset page number to 0 for new filters + if ( + newSortDirection !== sortDirection || + newSortHeader !== sortHeader || + newSearchQuery !== searchQuery + ) { + newQueryParams.page = "0"; + } + } + + // Updates inherited query table URL params + // No change to main query table URL params + if (isInherited) { + newQueryParams.inherited_order_key = newSortHeader; + newQueryParams.inherited_order_direction = newSortDirection; + newQueryParams.inherited_page = newPageIndex; + // Reset page number to 0 for new filters + if ( + newSortDirection !== initialInheritedSortDirection || + newSortHeader !== initialInheritedSortHeader + ) { + newQueryParams.inherited_page = "0"; + } } newQueryParams.team_id = queryParams?.team_id; @@ -154,11 +194,17 @@ const QueriesTable = ({ const onClientSidePaginationChange = useCallback( (pageIndex: number) => { - const newQueryParams = { - ...queryParams, - page: pageIndex, // update main table index - query: searchQuery, - }; + const newQueryParams = isInherited + ? { + ...queryParams, + inherited_page: pageIndex, // update inherited page index + query: searchQuery, + } + : { + ...queryParams, + page: pageIndex, // update main table index + query: searchQuery, + }; const locationPath = getNextLocationPath({ pathPrefix: PATHS.MANAGE_QUERIES, @@ -173,18 +219,20 @@ const QueriesTable = ({ const emptyQueries: IEmptyTableProps = { graphicName: "empty-queries", header: "You don't have any queries", + info: "A query is a specific question you can ask about your devices.", }; if (searchQuery) { delete emptyQueries.graphicName; - emptyQueries.header = "No matching queries"; - emptyQueries.info = "No queries match the current filters."; + emptyQueries.header = "No queries match the current search criteria"; + emptyQueries.info = + "Expecting to see queries? Try again in a few seconds as the system catches up."; } else if (!isOnlyObserver || isObserverPlus || isAnyTeamObserverPlus) { emptyQueries.additionalInfo = ( <> Create a new query, or{" "} @@ -195,7 +243,7 @@ const QueriesTable = ({ className={`${baseClass}__create-button`} onClick={onCreateQueryClick} > - Add query + Create new query ); } @@ -232,28 +280,26 @@ const QueriesTable = ({ const columnConfigs = useMemo( () => currentUser && - generateColumnConfigs({ - currentUser, - currentTeamId, - omitSelectionColumn: onlyInheritedQueries, - }), - [currentUser, currentTeamId, onlyInheritedQueries] + generateColumnConfigs({ currentUser, isInherited, currentTeamId }), + [currentUser, isInherited, currentTeamId] ); - const searchable = !(queriesList?.length === 0 && searchQuery === ""); + const searchable = + !(queriesList?.length === 0 && searchQuery === "") && !isInherited; const trimmedSearchQuery = searchQuery.trim(); return columnConfigs && !isLoading ? (
) : ( diff --git a/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTableConfig.tsx b/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTableConfig.tsx index 32b712ef82..a7467bdf3b 100644 --- a/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTableConfig.tsx +++ b/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTableConfig.tsx @@ -14,19 +14,16 @@ import { ISchedulableQuery, } from "interfaces/schedulable_query"; import { SupportedPlatform } from "interfaces/platform"; -import { API_ALL_TEAMS_ID } from "interfaces/team"; import Icon from "components/Icon"; import Checkbox from "components/forms/fields/Checkbox"; -import { getConditionalSelectHeaderCheckboxProps } from "components/TableContainer/utilities/config_utils"; import LinkCell from "components/TableContainer/DataTable/LinkCell/LinkCell"; import HeaderCell from "components/TableContainer/DataTable/HeaderCell/HeaderCell"; import PlatformCell from "components/TableContainer/DataTable/PlatformCell"; 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 { Tooltip as ReactTooltip5 } from "react-tooltip-5"; +import { COLORS } from "styles/var/colors"; import QueryAutomationsStatusIndicator from "../QueryAutomationsStatusIndicator"; interface IQueryRow { @@ -104,19 +101,18 @@ interface IDataColumn { interface IGenerateTableHeaders { currentUser: IUser; + isInherited?: boolean; currentTeamId?: number; - omitSelectionColumn?: boolean; } // NOTE: cellProps come from react-table // more info here https://react-table.tanstack.com/docs/api/useTable#cell-properties const generateTableHeaders = ({ currentUser, + isInherited = false, currentTeamId, - omitSelectionColumn = false, }: IGenerateTableHeaders): IDataColumn[] => { const isOnlyObserver = permissionsUtils.isOnlyObserver(currentUser); - const viewingTeamScope = currentTeamId !== API_ALL_TEAMS_ID; const tableHeaders: IDataColumn[] = [ { @@ -136,56 +132,26 @@ const generateTableHeaders = ({ <>
{cellProps.cell.value}
{!isOnlyObserver && cellProps.row.original.observer_can_run && ( -
+ <> - + - Observers can run this query. - -
- - // <> - // - // - // - // - // Observers can run this query. - // - // + + )} - {viewingTeamScope && - // inherited - cellProps.row.original.team_id !== currentTeamId && ( - - )} } path={PATHS.QUERY_DETAILS( @@ -280,27 +246,26 @@ const generateTableHeaders = ({ ), }, ]; - if (!isOnlyObserver && !omitSelectionColumn) { - tableHeaders.unshift({ + if (!isOnlyObserver && !isInherited) { + tableHeaders.splice(0, 0, { id: "selection", - // TODO - improve typing of IHeaderProps instead of using any - // Header: (headerProps: IHeaderProps): JSX.Element => { - Header: (headerProps: any): JSX.Element => { - const checkboxProps = getConditionalSelectHeaderCheckboxProps({ - headerProps, - checkIfRowIsSelectable: (row) => - (row.original.team_id ?? undefined) === currentTeamId, - }); + Header: (cellProps: IHeaderProps): JSX.Element => { + const { + getToggleAllRowsSelectedProps, + toggleAllRowsSelected, + } = cellProps; + const { checked, indeterminate } = getToggleAllRowsSelectedProps(); + const checkboxProps = { + value: checked, + indeterminate, + onChange: () => { + toggleAllRowsSelected(); + }, + }; return ; }, Cell: (cellProps: ICellProps): JSX.Element => { - const isInheritedQuery = - (cellProps.row.original.team_id ?? undefined) !== currentTeamId; - if (viewingTeamScope && isInheritedQuery) { - // disallow selecting inherited queries - return <>; - } const { row } = cellProps; const { checked } = row.getToggleRowSelectedProps(); const checkboxProps = { diff --git a/frontend/services/entities/queries.ts b/frontend/services/entities/queries.ts index a5c35dc765..bf30178745 100644 --- a/frontend/services/entities/queries.ts +++ b/frontend/services/entities/queries.ts @@ -35,12 +35,9 @@ export default { return sendRequest("GET", path); }, - loadAll: (teamId?: number, mergeInherited = false) => { + loadAll: (teamId?: number) => { const { QUERIES } = endpoints; - const queryString = buildQueryStringFromParams({ - team_id: teamId, - merge_inherited: mergeInherited || null, - }); + const queryString = buildQueryStringFromParams({ team_id: teamId }); const path = `${QUERIES}`; return sendRequest( diff --git a/frontend/services/entities/team_policies.ts b/frontend/services/entities/team_policies.ts index 0380f2106c..2858938dc7 100644 --- a/frontend/services/entities/team_policies.ts +++ b/frontend/services/entities/team_policies.ts @@ -17,11 +17,14 @@ interface IPoliciesApiQueryParams { orderKey?: string; orderDirection?: "asc" | "desc"; query?: string; + inheritedPage?: number; + inheritedPerPage?: number; + inheritedOrderKey?: string; + inheritedOrderDirection?: "asc" | "desc"; } export interface IPoliciesApiParams extends IPoliciesApiQueryParams { teamId: number; - mergeInherited?: boolean; } export interface ITeamPoliciesQueryKey extends IPoliciesApiParams { @@ -29,14 +32,13 @@ export interface ITeamPoliciesQueryKey extends IPoliciesApiParams { } export interface ITeamPoliciesCountQueryKey - extends Pick { - scope: "teamPoliciesCountMergeInherited" | "teamPoliciesCount"; + extends Pick { + scope: "teamPoliciesCount"; } interface IPoliciesCountApiParams { teamId: number; query?: string; - mergeInherited?: boolean; } const ORDER_KEY = "name"; @@ -116,6 +118,7 @@ export default { load: (team_id: number, id: number) => { const { TEAMS } = endpoints; const path = `${TEAMS}/${team_id}/policies/${id}`; + return sendRequest("GET", path); }, loadAll: (team_id?: number): Promise => { @@ -134,7 +137,10 @@ export default { orderKey = ORDER_KEY, orderDirection: orderDir = ORDER_DIRECTION, query, - mergeInherited, + inheritedPage, + inheritedPerPage, + inheritedOrderKey = ORDER_KEY, + inheritedOrderDirection: inheritedOrderDir = ORDER_DIRECTION, }: IPoliciesApiParams): Promise => { const { TEAMS } = endpoints; @@ -144,7 +150,10 @@ export default { orderKey, orderDirection: orderDir, query, - mergeInherited, + inheritedPage, + inheritedPerPage, + inheritedOrderKey, + inheritedOrderDirection: inheritedOrderDir, }; const snakeCaseParams = convertParamsToSnakeCase(queryParams); @@ -159,16 +168,14 @@ export default { getCount: async ({ query, teamId, - mergeInherited = false, }: Pick< IPoliciesCountApiParams, - "query" | "teamId" | "mergeInherited" + "query" | "teamId" >): Promise => { const { TEAM_POLICIES } = endpoints; const path = `${TEAM_POLICIES(teamId)}/count`; const queryParams = { query, - mergeInherited, }; const snakeCaseParams = convertParamsToSnakeCase(queryParams); const queryString = buildQueryStringFromParams(snakeCaseParams); diff --git a/frontend/services/mock_service/mocks/responses.ts b/frontend/services/mock_service/mocks/responses.ts index 709abc3f8b..57977ef922 100644 --- a/frontend/services/mock_service/mocks/responses.ts +++ b/frontend/services/mock_service/mocks/responses.ts @@ -4,8 +4,6 @@ * Also please check the README for how to use the mock service :) */ -import { createMockPoliciesResponse } from "__mocks__/policyMock"; - const count = { targets_count: 1, targets_online: 0, @@ -10592,7 +10590,6 @@ const globalQuery5 = { query: globalQueries.queries[5] }; const globalQuery6 = { query: globalQueries.queries[6] }; const teamQuery1 = { query: teamQueries.queries[0] }; const teamQuery2 = { query: teamQueries.queries[1] }; -const teamPolicy1 = createMockPoliciesResponse(); const aiAutofillPolicy = { description: @@ -10617,5 +10614,4 @@ export default { teamQuery1, teamQuery2, aiAutofillPolicy, - teamPolicy1, }; diff --git a/frontend/styles/var/mixins.scss b/frontend/styles/var/mixins.scss index 16fa29b45d..8f13df5d32 100644 --- a/frontend/styles/var/mixins.scss +++ b/frontend/styles/var/mixins.scss @@ -341,23 +341,3 @@ $max-width: 2560px; } } } - -@mixin tooltip5-arrow-styles { - // arrow styles directly from react-tooltip-5 css - .react-tooltip-arrow { - width: 8px; - height: 8px; - } - [class*="react-tooltip__place-top"] > .styles-module_arrow__K0L3T { - transform: rotate(45deg); - } - [class*="react-tooltip__place-right"] > .styles-module_arrow__K0L3T { - transform: rotate(135deg); - } - [class*="react-tooltip__place-bottom"] > .styles-module_arrow__K0L3T { - transform: rotate(225deg); - } - [class*="react-tooltip__place-left"] > .styles-module_arrow__K0L3T { - transform: rotate(315deg); - } -} diff --git a/frontend/utilities/helpers.tsx b/frontend/utilities/helpers.tsx index 906f249ab0..e18ee526ec 100644 --- a/frontend/utilities/helpers.tsx +++ b/frontend/utilities/helpers.tsx @@ -53,7 +53,7 @@ import { PLATFORM_LABEL_DISPLAY_TYPES, isPlatformLabelNameFromAPI, } from "utilities/constants"; -import { ISchedulableQueryStats } from "interfaces/schedulable_query"; +import { IScheduledQueryStats } from "interfaces/scheduled_query_stats"; import { IDropdownOption } from "interfaces/dropdownOption"; const ORG_INFO_ATTRS = ["org_name", "org_logo_url"]; @@ -686,7 +686,7 @@ export const readableDate = (date: string) => { }; export const getPerformanceImpactDescription = ( - scheduledQueryStats: ISchedulableQueryStats + scheduledQueryStats: IScheduledQueryStats ) => { if ( !scheduledQueryStats.total_executions || diff --git a/server/datastore/mysql/policies.go b/server/datastore/mysql/policies.go index bff19b5b0d..3701c84b48 100644 --- a/server/datastore/mysql/policies.go +++ b/server/datastore/mysql/policies.go @@ -451,25 +451,6 @@ func (ds *Datastore) CountPolicies(ctx context.Context, teamID *uint, matchQuery return count, nil } -func (ds *Datastore) CountMergedTeamPolicies(ctx context.Context, teamID uint, matchQuery string) (int, error) { - var args []interface{} - - query := `SELECT count(*) FROM policies p WHERE p.team_id = ? OR p.team_id IS NULL` - args = append(args, teamID) - - // We must normalize the name for full Unicode support (Unicode equivalence). - match := norm.NFC.String(matchQuery) - query, args = searchLike(query, args, match, policySearchColumns...) - - var count int - err := sqlx.GetContext(ctx, ds.reader(ctx), &count, query, args...) - if err != nil { - return 0, ctxerr.Wrap(ctx, err, "counting merged team policies") - } - - return count, nil -} - func (ds *Datastore) PoliciesByID(ctx context.Context, ids []uint) (map[uint]*fleet.Policy, error) { sql := `SELECT ` + policyCols + `, COALESCE(u.name, '') AS author_name, @@ -627,40 +608,6 @@ func (ds *Datastore) ListTeamPolicies(ctx context.Context, teamID uint, opts fle return teamPolicies, inheritedPolicies, err } -func (ds *Datastore) ListMergedTeamPolicies(ctx context.Context, teamID uint, opts fleet.ListOptions) ([]*fleet.Policy, error) { - var args []interface{} - - query := ` - SELECT - ` + policyCols + `, - COALESCE(u.name, '') AS author_name, - COALESCE(u.email, '') AS author_email, - ps.updated_at as host_count_updated_at, - COALESCE(ps.passing_host_count, 0) as passing_host_count, - COALESCE(ps.failing_host_count, 0) as failing_host_count - FROM policies p - LEFT JOIN users u ON p.author_id = u.id - LEFT JOIN policy_stats ps ON p.id = ps.policy_id - AND ps.inherited_team_id = COALESCE(p.team_id, 0) - WHERE (p.team_id = ? OR p.team_id IS NULL) - ` - - args = append(args, teamID) - - // We must normalize the name for full Unicode support (Unicode equivalence). - match := norm.NFC.String(opts.MatchQuery) - query, args = searchLike(query, args, match, policySearchColumns...) - query, _ = appendListOptionsToSQL(query, &opts) - - var policies []*fleet.Policy - err := sqlx.SelectContext(ctx, ds.reader(ctx), &policies, query, args...) - if err != nil { - return nil, ctxerr.Wrap(ctx, err, "listing merged team policies") - } - - return policies, nil -} - func (ds *Datastore) DeleteTeamPolicies(ctx context.Context, teamID uint, ids []uint) ([]uint, error) { return deletePolicyDB(ctx, ds.writer(ctx), ids, &teamID) } diff --git a/server/datastore/mysql/policies_test.go b/server/datastore/mysql/policies_test.go index b5be1af776..1e6980736e 100644 --- a/server/datastore/mysql/policies_test.go +++ b/server/datastore/mysql/policies_test.go @@ -35,7 +35,6 @@ func TestPolicies(t *testing.T) { {"MembershipViewNotDeferred", func(t *testing.T, ds *Datastore) { testPoliciesMembershipView(false, t, ds) }}, {"TeamPolicyLegacy", testTeamPolicyLegacy}, {"TeamPolicyProprietary", testTeamPolicyProprietary}, - {"ListMergedTeamPolicies", testListMergedTeamPolicies}, {"PolicyQueriesForHost", testPolicyQueriesForHost}, {"PolicyQueriesForHostPlatforms", testPolicyQueriesForHostPlatforms}, {"PoliciesByID", testPoliciesByID}, @@ -710,75 +709,6 @@ func testTeamPolicyProprietary(t *testing.T, ds *Datastore) { require.Equal(t, user1.ID, *team2Policies[0].AuthorID) } -func testListMergedTeamPolicies(t *testing.T, ds *Datastore) { - ctx := context.Background() - gpol, err := ds.NewGlobalPolicy(ctx, nil, fleet.PolicyPayload{ - Name: "query1 global", - Query: "select 1;", - Description: "query1 desc", - Resolution: "query1 resolution", - }) - require.NoError(t, err) - - team1, err := ds.NewTeam(ctx, &fleet.Team{Name: "team1"}) - require.NoError(t, err) - - p, err := ds.NewTeamPolicy(ctx, team1.ID, nil, fleet.PolicyPayload{ - Name: "query2 team1", - Query: "select 1;", - Description: "query1 desc", - Resolution: "query1 resolution", - }) - require.NoError(t, err) - - team2, err := ds.NewTeam(ctx, &fleet.Team{Name: "team2"}) - require.NoError(t, err) - - _, err = ds.NewTeamPolicy(ctx, team2.ID, nil, fleet.PolicyPayload{ - Name: "query3 team2", - Query: "select 2;", - Description: "query2 desc", - Resolution: "query2 resolution", - }) - require.NoError(t, err) - - merged, err := ds.ListMergedTeamPolicies(ctx, team1.ID, fleet.ListOptions{ - OrderKey: "name", - OrderDirection: fleet.OrderAscending, - }) - require.NoError(t, err) - - require.Len(t, merged, 2) - assert.Equal(t, gpol.ID, merged[0].ID) - assert.Equal(t, p.ID, merged[1].ID) - - // Test list options affect both global and team policies - merged, err = ds.ListMergedTeamPolicies(ctx, team1.ID, fleet.ListOptions{ - OrderKey: "name", - OrderDirection: fleet.OrderDescending, - }) - require.NoError(t, err) - - require.Len(t, merged, 2) - assert.Equal(t, p.ID, merged[0].ID) - assert.Equal(t, gpol.ID, merged[1].ID) - - // Test filter - merged, err = ds.ListMergedTeamPolicies(ctx, team1.ID, fleet.ListOptions{ - MatchQuery: "query1", - }) - require.NoError(t, err) - require.Len(t, merged, 1) - assert.Equal(t, gpol.ID, merged[0].ID) - - merged, err = ds.ListMergedTeamPolicies(ctx, team1.ID, fleet.ListOptions{ - MatchQuery: "query2", - }) - require.NoError(t, err) - require.Len(t, merged, 1) - assert.Equal(t, p.ID, merged[0].ID) -} - func newTestHostWithPlatform(t *testing.T, ds *Datastore, hostname, platform string, teamID *uint) *fleet.Host { nodeKey, err := server.GenerateRandomText(32) require.NoError(t, err) @@ -2969,10 +2899,6 @@ func testCountPolicies(t *testing.T, ds *Datastore) { require.NoError(t, err) assert.Equal(t, 0, teamCount) - mergedCount, err := ds.CountMergedTeamPolicies(ctx, tm.ID, "") - require.NoError(t, err) - assert.Equal(t, 0, mergedCount) - // 10 global policies for i := 0; i < 10; i++ { _, err := ds.NewGlobalPolicy(ctx, nil, fleet.PolicyPayload{Name: fmt.Sprintf("global policy %d", i)}) @@ -2987,10 +2913,6 @@ func testCountPolicies(t *testing.T, ds *Datastore) { require.NoError(t, err) assert.Equal(t, 0, teamCount) - mergedCount, err = ds.CountMergedTeamPolicies(ctx, tm.ID, "") - require.NoError(t, err) - assert.Equal(t, 10, mergedCount) - // add 5 team policies for i := 0; i < 5; i++ { _, err := ds.NewTeamPolicy(ctx, tm.ID, nil, fleet.PolicyPayload{Name: fmt.Sprintf("team policy %d", i)}) @@ -3004,10 +2926,6 @@ func testCountPolicies(t *testing.T, ds *Datastore) { globalCount, err = ds.CountPolicies(ctx, nil, "") require.NoError(t, err) assert.Equal(t, 10, globalCount) - - mergedCount, err = ds.CountMergedTeamPolicies(ctx, tm.ID, "") - require.NoError(t, err) - assert.Equal(t, 15, mergedCount) } func testUpdatePolicyHostCounts(t *testing.T, ds *Datastore) { diff --git a/server/datastore/mysql/queries.go b/server/datastore/mysql/queries.go index 2ad343922c..2a26884985 100644 --- a/server/datastore/mysql/queries.go +++ b/server/datastore/mysql/queries.go @@ -4,12 +4,11 @@ import ( "context" "database/sql" "fmt" - "strings" - "github.com/fleetdm/fleet/v4/server/contexts/ctxerr" "github.com/fleetdm/fleet/v4/server/fleet" "github.com/go-kit/log/level" "github.com/jmoiron/sqlx" + "strings" ) const ( @@ -415,6 +414,7 @@ func (ds *Datastore) deleteQueryStats(ctx context.Context, queryIDs []uint) { level.Error(ds.logger).Log("msg", "error deleting aggregated stats", "err", err) } } + } // Query returns a single Query identified by id, if such exists. @@ -504,14 +504,10 @@ func (ds *Datastore) ListQueries(ctx context.Context, opt fleet.ListQueryOptions args := []interface{}{false, fleet.AggregatedStatsTypeScheduledQuery} whereClauses := "WHERE saved = true" - switch { - case opt.TeamID != nil && opt.MergeInherited: - args = append(args, *opt.TeamID) - whereClauses += " AND (team_id = ? OR team_id IS NULL)" - case opt.TeamID != nil: + if opt.TeamID != nil { args = append(args, *opt.TeamID) whereClauses += " AND team_id = ?" - default: + } else { whereClauses += " AND team_id IS NULL" } diff --git a/server/datastore/mysql/queries_test.go b/server/datastore/mysql/queries_test.go index f43c07fe7e..2096683477 100644 --- a/server/datastore/mysql/queries_test.go +++ b/server/datastore/mysql/queries_test.go @@ -219,6 +219,7 @@ func testQueriesDelete(t *testing.T, ds *Datastore) { case <-time.After(10 * time.Second): t.Error("Timeout: stats not deleted for testQueriesDelete") } + } func testQueriesGetByName(t *testing.T, ds *Datastore) { @@ -764,27 +765,6 @@ func testListQueriesFiltersByTeamID(t *testing.T, ds *Datastore) { ) require.NoError(t, err) test.QueryElementsMatch(t, queries, []*fleet.Query{teamQ1, teamQ2, teamQ3}) - - // test merge inherited - queries, err = ds.ListQueries( - context.Background(), - fleet.ListQueryOptions{ - TeamID: &team.ID, - MergeInherited: true, - }, - ) - require.NoError(t, err) - test.QueryElementsMatch(t, queries, []*fleet.Query{globalQ1, globalQ2, globalQ3, teamQ1, teamQ2, teamQ3}) - - // merge inherited ignored for global queries - queries, err = ds.ListQueries( - context.Background(), - fleet.ListQueryOptions{ - MergeInherited: true, - }, - ) - require.NoError(t, err) - test.QueryElementsMatch(t, queries, []*fleet.Query{globalQ1, globalQ2, globalQ3}) } func testListQueriesFiltersByIsScheduled(t *testing.T, ds *Datastore) { diff --git a/server/fleet/app.go b/server/fleet/app.go index ef5b06e5b2..6f99244500 100644 --- a/server/fleet/app.go +++ b/server/fleet/app.go @@ -1016,9 +1016,6 @@ type ListQueryOptions struct { TeamID *uint // IsScheduled filters queries that are meant to run at a set interval. IsScheduled *bool - // MergeInherited merges inherited global queries into the team list. Is only valid when TeamID - // is set. - MergeInherited bool } type ListActivitiesOptions struct { diff --git a/server/fleet/datastore.go b/server/fleet/datastore.go index 5d1bf64534..2070ad98ad 100644 --- a/server/fleet/datastore.go +++ b/server/fleet/datastore.go @@ -612,7 +612,6 @@ type Datastore interface { PoliciesByID(ctx context.Context, ids []uint) (map[uint]*Policy, error) DeleteGlobalPolicies(ctx context.Context, ids []uint) ([]uint, error) CountPolicies(ctx context.Context, teamID *uint, matchQuery string) (int, error) - CountMergedTeamPolicies(ctx context.Context, teamID uint, matchQuery string) (int, error) UpdateHostPolicyCounts(ctx context.Context) error PolicyQueriesForHost(ctx context.Context, host *Host) (map[string]string, error) @@ -662,8 +661,6 @@ type Datastore interface { NewTeamPolicy(ctx context.Context, teamID uint, authorID *uint, args PolicyPayload) (*Policy, error) ListTeamPolicies(ctx context.Context, teamID uint, opts ListOptions, iopts ListOptions) (teamPolicies, inheritedPolicies []*Policy, err error) - ListMergedTeamPolicies(ctx context.Context, teamID uint, opts ListOptions) ([]*Policy, error) - DeleteTeamPolicies(ctx context.Context, teamID uint, ids []uint) ([]uint, error) TeamPolicy(ctx context.Context, teamID uint, policyID uint) (*Policy, error) diff --git a/server/fleet/service.go b/server/fleet/service.go index f5616443ea..47d0e5fc5e 100644 --- a/server/fleet/service.go +++ b/server/fleet/service.go @@ -270,9 +270,7 @@ type Service interface { // for distributed queries but not saved should not be returned). // When is set to scheduled != nil, then only scheduled queries will be returned if `*scheduled == true` // and only non-scheduled queries will be returned if `*scheduled == false`. - // If mergeInherited is true and a teamID is provided, then queries from the global team will be - // included in the results. - ListQueries(ctx context.Context, opt ListOptions, teamID *uint, scheduled *bool, mergeInherited bool) ([]*Query, error) + ListQueries(ctx context.Context, opt ListOptions, teamID *uint, scheduled *bool) ([]*Query, error) GetQuery(ctx context.Context, id uint) (*Query, error) // GetQueryReportResults returns all the stored results of a query for hosts the requestor has access to GetQueryReportResults(ctx context.Context, id uint) ([]HostQueryResultRow, error) @@ -642,11 +640,11 @@ type Service interface { // Team Policies NewTeamPolicy(ctx context.Context, teamID uint, p PolicyPayload) (*Policy, error) - ListTeamPolicies(ctx context.Context, teamID uint, opts ListOptions, iopts ListOptions, mergeInherited bool) (teamPolicies, inheritedPolicies []*Policy, err error) + ListTeamPolicies(ctx context.Context, teamID uint, opts ListOptions, iopts ListOptions) (teamPolicies, inheritedPolicies []*Policy, err error) DeleteTeamPolicies(ctx context.Context, teamID uint, ids []uint) ([]uint, error) ModifyTeamPolicy(ctx context.Context, teamID uint, id uint, p ModifyPolicyPayload) (*Policy, error) GetTeamPolicyByIDQueries(ctx context.Context, teamID uint, policyID uint) (*Policy, error) - CountTeamPolicies(ctx context.Context, teamID uint, matchQuery string, mergeInherited bool) (int, error) + CountTeamPolicies(ctx context.Context, teamID uint, matchQuery string) (int, error) // ///////////////////////////////////////////////////////////////////////////// // Geolocation diff --git a/server/mock/datastore_mock.go b/server/mock/datastore_mock.go index 834bb4499c..157695382b 100644 --- a/server/mock/datastore_mock.go +++ b/server/mock/datastore_mock.go @@ -447,8 +447,6 @@ type DeleteGlobalPoliciesFunc func(ctx context.Context, ids []uint) ([]uint, err type CountPoliciesFunc func(ctx context.Context, teamID *uint, matchQuery string) (int, error) -type CountMergedTeamPoliciesFunc func(ctx context.Context, teamID uint, matchQuery string) (int, error) - type UpdateHostPolicyCountsFunc func(ctx context.Context) error type PolicyQueriesForHostFunc func(ctx context.Context, host *fleet.Host) (map[string]string, error) @@ -497,8 +495,6 @@ type NewTeamPolicyFunc func(ctx context.Context, teamID uint, authorID *uint, ar type ListTeamPoliciesFunc func(ctx context.Context, teamID uint, opts fleet.ListOptions, iopts fleet.ListOptions) (teamPolicies []*fleet.Policy, inheritedPolicies []*fleet.Policy, err error) -type ListMergedTeamPoliciesFunc func(ctx context.Context, teamID uint, opts fleet.ListOptions) ([]*fleet.Policy, error) - type DeleteTeamPoliciesFunc func(ctx context.Context, teamID uint, ids []uint) ([]uint, error) type TeamPolicyFunc func(ctx context.Context, teamID uint, policyID uint) (*fleet.Policy, error) @@ -1568,9 +1564,6 @@ type DataStore struct { CountPoliciesFunc CountPoliciesFunc CountPoliciesFuncInvoked bool - CountMergedTeamPoliciesFunc CountMergedTeamPoliciesFunc - CountMergedTeamPoliciesFuncInvoked bool - UpdateHostPolicyCountsFunc UpdateHostPolicyCountsFunc UpdateHostPolicyCountsFuncInvoked bool @@ -1643,9 +1636,6 @@ type DataStore struct { ListTeamPoliciesFunc ListTeamPoliciesFunc ListTeamPoliciesFuncInvoked bool - ListMergedTeamPoliciesFunc ListMergedTeamPoliciesFunc - ListMergedTeamPoliciesFuncInvoked bool - DeleteTeamPoliciesFunc DeleteTeamPoliciesFunc DeleteTeamPoliciesFuncInvoked bool @@ -3786,13 +3776,6 @@ func (s *DataStore) CountPolicies(ctx context.Context, teamID *uint, matchQuery return s.CountPoliciesFunc(ctx, teamID, matchQuery) } -func (s *DataStore) CountMergedTeamPolicies(ctx context.Context, teamID uint, matchQuery string) (int, error) { - s.mu.Lock() - s.CountMergedTeamPoliciesFuncInvoked = true - s.mu.Unlock() - return s.CountMergedTeamPoliciesFunc(ctx, teamID, matchQuery) -} - func (s *DataStore) UpdateHostPolicyCounts(ctx context.Context) error { s.mu.Lock() s.UpdateHostPolicyCountsFuncInvoked = true @@ -3961,13 +3944,6 @@ func (s *DataStore) ListTeamPolicies(ctx context.Context, teamID uint, opts flee return s.ListTeamPoliciesFunc(ctx, teamID, opts, iopts) } -func (s *DataStore) ListMergedTeamPolicies(ctx context.Context, teamID uint, opts fleet.ListOptions) ([]*fleet.Policy, error) { - s.mu.Lock() - s.ListMergedTeamPoliciesFuncInvoked = true - s.mu.Unlock() - return s.ListMergedTeamPoliciesFunc(ctx, teamID, opts) -} - func (s *DataStore) DeleteTeamPolicies(ctx context.Context, teamID uint, ids []uint) ([]uint, error) { s.mu.Lock() s.DeleteTeamPoliciesFuncInvoked = true diff --git a/server/service/global_schedule.go b/server/service/global_schedule.go index c75d860486..a8efa4c87d 100644 --- a/server/service/global_schedule.go +++ b/server/service/global_schedule.go @@ -37,7 +37,7 @@ func getGlobalScheduleEndpoint(ctx context.Context, request interface{}, svc fle } func (svc *Service) GetGlobalScheduledQueries(ctx context.Context, opts fleet.ListOptions) ([]*fleet.ScheduledQuery, error) { - queries, err := svc.ListQueries(ctx, opts, nil, ptr.Bool(true), false) // teamID == nil means global + queries, err := svc.ListQueries(ctx, opts, nil, ptr.Bool(true)) // teamID == nil means global if err != nil { return nil, err } diff --git a/server/service/integration_core_test.go b/server/service/integration_core_test.go index 4a739e189e..a9e4c6ffc7 100644 --- a/server/service/integration_core_test.go +++ b/server/service/integration_core_test.go @@ -263,7 +263,7 @@ func (s *integrationTestSuite) TestQueryCreationLogsActivity() { } var createQueryResp createQueryResponse s.DoJSON("POST", "/api/latest/fleet/queries", ¶ms, http.StatusOK, &createQueryResp) - defer s.cleanupQuery(createQueryResp.Query.ID) + defer cleanupQuery(s, createQueryResp.Query.ID) activities := listActivitiesResponse{} s.DoJSON("GET", "/api/latest/fleet/activities", nil, http.StatusOK, &activities) @@ -1579,7 +1579,7 @@ func (s *integrationTestSuite) TestListHosts() { user1 := test.NewUser(t, s.ds, "Alice", "alice@example.com", true) q := test.NewQuery(t, s.ds, nil, "query1", "select 1", 0, true) - defer s.cleanupQuery(q.ID) + defer cleanupQuery(s, q.ID) globalPolicy0, err := s.ds.NewGlobalPolicy( context.Background(), &user1.ID, fleet.PolicyPayload{ QueryID: &q.ID, @@ -5791,7 +5791,7 @@ func (s *integrationTestSuite) TestQueriesBadRequests() { s.DoJSON("POST", "/api/latest/fleet/queries", reqQuery, http.StatusOK, &createQueryResp) require.NotNil(t, createQueryResp.Query) existingQueryID := createQueryResp.Query.ID - defer s.cleanupQuery(existingQueryID) + defer cleanupQuery(s, existingQueryID) for _, tc := range []struct { tname string @@ -9011,7 +9011,7 @@ func createSession(t *testing.T, uid uint, ds fleet.Datastore) *fleet.Session { return ssn } -func (s *integrationTestSuite) cleanupQuery(queryID uint) { +func cleanupQuery(s *integrationTestSuite, queryID uint) { var delResp deleteQueryByIDResponse s.DoJSON("DELETE", fmt.Sprintf("/api/latest/fleet/queries/id/%d", queryID), nil, http.StatusOK, &delResp) } diff --git a/server/service/integration_enterprise_test.go b/server/service/integration_enterprise_test.go index d7905364f5..b50bd25c7b 100644 --- a/server/service/integration_enterprise_test.go +++ b/server/service/integration_enterprise_test.go @@ -844,35 +844,6 @@ func (s *integrationEnterpriseTestSuite) TestTeamPolicies() { assert.Equal(t, gpol.Name, ts.InheritedPolicies[0].Name) assert.Equal(t, gpol.ID, ts.InheritedPolicies[0].ID) - tc := countTeamPoliciesResponse{} - s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/teams/%d/policies/count", team1.ID), nil, http.StatusOK, &tc) - require.Nil(t, tc.Err) - require.Equal(t, 1, tc.Count) - - gc := countGlobalPoliciesResponse{} - s.DoJSON("GET", "/api/latest/fleet/policies/count", nil, http.StatusOK, &gc) - require.Nil(t, gc.Err) - require.Equal(t, 1, gc.Count) - - // Test merge inherited - ts = listTeamPoliciesResponse{} - s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/teams/%d/policies", team1.ID), nil, http.StatusOK, &ts, "merge_inherited", "true", "order_key", "team_id", "order_direction", "desc") - require.Len(t, ts.Policies, 2) - require.Nil(t, ts.InheritedPolicies) - assert.Equal(t, "TestQuery2", ts.Policies[0].Name) - assert.Equal(t, "select * from osquery;", ts.Policies[0].Query) - assert.Equal(t, "Some description", ts.Policies[0].Description) - require.NotNil(t, ts.Policies[0].Resolution) - assert.Equal(t, "some team resolution", *ts.Policies[0].Resolution) - assert.Equal(t, gpol.Name, ts.Policies[1].Name) - assert.Equal(t, gpol.ID, ts.Policies[1].ID) - - countResp := countTeamPoliciesResponse{} - s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/teams/%d/policies/count", team1.ID), nil, http.StatusOK, &countResp, "merge_inherited", "true") - require.Nil(t, countResp.Err) - require.Equal(t, 2, countResp.Count) - - // Test delete deletePolicyParams := deleteTeamPoliciesRequest{IDs: []uint{ts.Policies[0].ID}} deletePolicyResp := deleteTeamPoliciesResponse{} s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/teams/%d/policies/delete", team1.ID), deletePolicyParams, http.StatusOK, &deletePolicyResp) @@ -882,53 +853,6 @@ func (s *integrationEnterpriseTestSuite) TestTeamPolicies() { require.Len(t, ts.Policies, 0) } -func (s *integrationEnterpriseTestSuite) TestTeamQueries() { - t := s.T() - - team1, err := s.ds.NewTeam(context.Background(), &fleet.Team{ - ID: 42, - Name: "team1" + t.Name(), - Description: "desc team1", - }) - require.NoError(t, err) - - oldToken := s.token - t.Cleanup(func() { - s.token = oldToken - }) - - // create global query - params := fleet.QueryPayload{ - Name: ptr.String("global1"), - Query: ptr.String("select * from time;"), - } - var createQueryResp createQueryResponse - s.DoJSON("POST", "/api/latest/fleet/queries", ¶ms, http.StatusOK, &createQueryResp) - defer s.cleanupQuery(createQueryResp.Query.ID) - - // create team query - params = fleet.QueryPayload{ - Name: ptr.String("team1"), - Query: ptr.String("select * from time;"), - TeamID: ptr.Uint(team1.ID), - } - createQueryResp = createQueryResponse{} - s.DoJSON("POST", "/api/latest/fleet/queries", ¶ms, http.StatusOK, &createQueryResp) - defer s.cleanupQuery(createQueryResp.Query.ID) - - // list team queries - var listQueriesResp listQueriesResponse - s.DoJSON("GET", "/api/latest/fleet/queries", nil, http.StatusOK, &listQueriesResp, "team_id", fmt.Sprint(team1.ID)) - require.Len(t, listQueriesResp.Queries, 1) - assert.Equal(t, "team1", listQueriesResp.Queries[0].Name) - - // list merged team queries - s.DoJSON("GET", "/api/latest/fleet/queries", nil, http.StatusOK, &listQueriesResp, "team_id", fmt.Sprint(team1.ID), "merge_inherited", "true", "order_key", "team_id", "order_direction", "desc") - require.Len(t, listQueriesResp.Queries, 2) - assert.Equal(t, "team1", listQueriesResp.Queries[0].Name) - assert.Equal(t, "global1", listQueriesResp.Queries[1].Name) -} - func (s *integrationEnterpriseTestSuite) TestModifyTeamEnrollSecrets() { t := s.T() @@ -2916,8 +2840,7 @@ func (s *integrationEnterpriseTestSuite) TestMDMMacOSUpdates() { // edited macos min version activity got created s.lastActivityMatches(fleet.ActivityTypeEditedMacOSMinVersion{}.ActivityName(), `{"deadline":"2022-01-01", "minimum_version":"12.3.1", "team_id": null, "team_name": null}`, 0) s.assertMacOSUpdatesDeclaration(nil, &fleet.MacOSUpdates{ - MinimumVersion: optjson.SetString("12.3.1"), Deadline: optjson.SetString("2022-01-01"), - }) + MinimumVersion: optjson.SetString("12.3.1"), Deadline: optjson.SetString("2022-01-01")}) // get the appconfig acResp = appConfigResponse{} @@ -2941,8 +2864,7 @@ func (s *integrationEnterpriseTestSuite) TestMDMMacOSUpdates() { // another edited macos min version activity got created lastActivity = s.lastActivityMatches(fleet.ActivityTypeEditedMacOSMinVersion{}.ActivityName(), `{"deadline":"2024-01-01", "minimum_version":"12.3.1", "team_id": null, "team_name": null}`, 0) s.assertMacOSUpdatesDeclaration(nil, &fleet.MacOSUpdates{ - MinimumVersion: optjson.SetString("12.3.1"), Deadline: optjson.SetString("2024-01-01"), - }) + MinimumVersion: optjson.SetString("12.3.1"), Deadline: optjson.SetString("2024-01-01")}) // update something unrelated - the transparency url acResp = appConfigResponse{} @@ -2953,8 +2875,7 @@ func (s *integrationEnterpriseTestSuite) TestMDMMacOSUpdates() { // no activity got created s.lastActivityMatches("", ``, lastActivity) s.assertMacOSUpdatesDeclaration(nil, &fleet.MacOSUpdates{ - MinimumVersion: optjson.SetString("12.3.1"), Deadline: optjson.SetString("2024-01-01"), - }) + MinimumVersion: optjson.SetString("12.3.1"), Deadline: optjson.SetString("2024-01-01")}) // clear the macos requirement acResp = appConfigResponse{} @@ -8732,8 +8653,3 @@ func triggerAndWait(ctx context.Context, t *testing.T, ds fleet.Datastore, s *sc } } } - -func (s *integrationEnterpriseTestSuite) cleanupQuery(queryID uint) { - var delResp deleteQueryByIDResponse - s.DoJSON("DELETE", fmt.Sprintf("/api/latest/fleet/queries/id/%d", queryID), nil, http.StatusOK, &delResp) -} diff --git a/server/service/queries.go b/server/service/queries.go index 988f3d232b..a4d39fef83 100644 --- a/server/service/queries.go +++ b/server/service/queries.go @@ -58,8 +58,7 @@ func (svc *Service) GetQuery(ctx context.Context, id uint) (*fleet.Query, error) type listQueriesRequest struct { ListOptions fleet.ListOptions `url:"list_options"` // TeamID url argument set to 0 means global. - TeamID uint `query:"team_id,optional"` - MergeInherited bool `query:"merge_inherited,optional"` + TeamID uint `query:"team_id,optional"` } type listQueriesResponse struct { @@ -77,7 +76,7 @@ func listQueriesEndpoint(ctx context.Context, request interface{}, svc fleet.Ser teamID = &req.TeamID } - queries, err := svc.ListQueries(ctx, req.ListOptions, teamID, nil, req.MergeInherited) + queries, err := svc.ListQueries(ctx, req.ListOptions, teamID, nil) if err != nil { return listQueriesResponse{Err: err}, nil } @@ -91,7 +90,7 @@ func listQueriesEndpoint(ctx context.Context, request interface{}, svc fleet.Ser }, nil } -func (svc *Service) ListQueries(ctx context.Context, opt fleet.ListOptions, teamID *uint, scheduled *bool, mergeInherited bool) ([]*fleet.Query, error) { +func (svc *Service) ListQueries(ctx context.Context, opt fleet.ListOptions, teamID *uint, scheduled *bool) ([]*fleet.Query, error) { // Check the user is allowed to list queries on the given team. if err := svc.authz.Authorize(ctx, &fleet.Query{ TeamID: teamID, @@ -100,10 +99,9 @@ func (svc *Service) ListQueries(ctx context.Context, opt fleet.ListOptions, team } queries, err := svc.ds.ListQueries(ctx, fleet.ListQueryOptions{ - ListOptions: opt, - TeamID: teamID, - IsScheduled: scheduled, - MergeInherited: mergeInherited, + ListOptions: opt, + TeamID: teamID, + IsScheduled: scheduled, }) if err != nil { return nil, err @@ -735,7 +733,7 @@ func getQuerySpecsEndpoint(ctx context.Context, request interface{}, svc fleet.S } func (svc *Service) GetQuerySpecs(ctx context.Context, teamID *uint) ([]*fleet.QuerySpec, error) { - queries, err := svc.ListQueries(ctx, fleet.ListOptions{}, teamID, nil, false) + queries, err := svc.ListQueries(ctx, fleet.ListOptions{}, teamID, nil) if err != nil { return nil, ctxerr.Wrap(ctx, err, "getting queries") } diff --git a/server/service/queries_test.go b/server/service/queries_test.go index 9b9cdfb1c9..0fc1a44aec 100644 --- a/server/service/queries_test.go +++ b/server/service/queries_test.go @@ -632,7 +632,7 @@ func TestQueryAuth(t *testing.T) { _, err = svc.QueryReportIsClipped(ctx, tt.qid) checkAuthErr(t, tt.shouldFailRead, err) - _, err = svc.ListQueries(ctx, fleet.ListOptions{}, query.TeamID, nil, false) + _, err = svc.ListQueries(ctx, fleet.ListOptions{}, query.TeamID, nil) checkAuthErr(t, tt.shouldFailRead, err) teamName := "" diff --git a/server/service/team_policies.go b/server/service/team_policies.go index a2698c145e..75cbe3ae96 100644 --- a/server/service/team_policies.go +++ b/server/service/team_policies.go @@ -106,7 +106,6 @@ type listTeamPoliciesRequest struct { InheritedPerPage uint `query:"inherited_per_page,optional"` InheritedOrderDirection fleet.OrderDirection `query:"inherited_order_direction,optional"` InheritedOrderKey string `query:"inherited_order_key,optional"` - MergeInherited bool `query:"merge_inherited,optional"` } type listTeamPoliciesResponse struct { @@ -127,14 +126,14 @@ func listTeamPoliciesEndpoint(ctx context.Context, request interface{}, svc flee OrderKey: req.InheritedOrderKey, } - tmPols, inheritedPols, err := svc.ListTeamPolicies(ctx, req.TeamID, req.Opts, inheritedListOptions, req.MergeInherited) + tmPols, inheritedPols, err := svc.ListTeamPolicies(ctx, req.TeamID, req.Opts, inheritedListOptions) if err != nil { return listTeamPoliciesResponse{Err: err}, nil } return listTeamPoliciesResponse{Policies: tmPols, InheritedPolicies: inheritedPols}, nil } -func (svc *Service) ListTeamPolicies(ctx context.Context, teamID uint, opts fleet.ListOptions, iopts fleet.ListOptions, mergeInherited bool) (teamPolicies, inheritedPolicies []*fleet.Policy, err error) { +func (svc *Service) ListTeamPolicies(ctx context.Context, teamID uint, opts fleet.ListOptions, iopts fleet.ListOptions) (teamPolicies, inheritedPolicies []*fleet.Policy, err error) { if err := svc.authz.Authorize(ctx, &fleet.Policy{ PolicyData: fleet.PolicyData{ TeamID: ptr.Uint(teamID), @@ -147,11 +146,6 @@ func (svc *Service) ListTeamPolicies(ctx context.Context, teamID uint, opts flee return nil, nil, ctxerr.Wrapf(ctx, err, "loading team %d", teamID) } - if mergeInherited { - p, err := svc.ds.ListMergedTeamPolicies(ctx, teamID, opts) - return p, nil, err - } - return svc.ds.ListTeamPolicies(ctx, teamID, opts, iopts) } @@ -160,9 +154,8 @@ func (svc *Service) ListTeamPolicies(ctx context.Context, teamID uint, opts flee ///////////////////////////////////////////////////////////////////////////////// type countTeamPoliciesRequest struct { - ListOptions fleet.ListOptions `url:"list_options"` - TeamID uint `url:"team_id"` - MergeInherited bool `query:"merge_inherited,optional"` + ListOptions fleet.ListOptions `url:"list_options"` + TeamID uint `url:"team_id"` } type countTeamPoliciesResponse struct { @@ -174,14 +167,14 @@ func (r countTeamPoliciesResponse) error() error { return r.Err } func countTeamPoliciesEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { req := request.(*countTeamPoliciesRequest) - resp, err := svc.CountTeamPolicies(ctx, req.TeamID, req.ListOptions.MatchQuery, req.MergeInherited) + resp, err := svc.CountTeamPolicies(ctx, req.TeamID, req.ListOptions.MatchQuery) if err != nil { return countTeamPoliciesResponse{Err: err}, nil } return countTeamPoliciesResponse{Count: resp}, nil } -func (svc *Service) CountTeamPolicies(ctx context.Context, teamID uint, matchQuery string, mergeInherited bool) (int, error) { +func (svc *Service) CountTeamPolicies(ctx context.Context, teamID uint, matchQuery string) (int, error) { if err := svc.authz.Authorize(ctx, &fleet.Policy{ PolicyData: fleet.PolicyData{ TeamID: ptr.Uint(teamID), @@ -194,10 +187,6 @@ func (svc *Service) CountTeamPolicies(ctx context.Context, teamID uint, matchQue return 0, ctxerr.Wrapf(ctx, err, "loading team %d", teamID) } - if mergeInherited { - return svc.ds.CountMergedTeamPolicies(ctx, teamID, matchQuery) - } - return svc.ds.CountPolicies(ctx, &teamID, matchQuery) } diff --git a/server/service/team_policies_test.go b/server/service/team_policies_test.go index 6a2d35d4ff..9e1a502f67 100644 --- a/server/service/team_policies_test.go +++ b/server/service/team_policies_test.go @@ -149,7 +149,7 @@ func TestTeamPoliciesAuth(t *testing.T) { }) checkAuthErr(t, tt.shouldFailWrite, err) - _, _, err = svc.ListTeamPolicies(ctx, 1, fleet.ListOptions{}, fleet.ListOptions{}, false) + _, _, err = svc.ListTeamPolicies(ctx, 1, fleet.ListOptions{}, fleet.ListOptions{}) checkAuthErr(t, tt.shouldFailRead, err) _, err = svc.GetTeamPolicyByIDQueries(ctx, 1, 1) diff --git a/server/service/team_schedule.go b/server/service/team_schedule.go index da31740a77..24ad3cde3b 100644 --- a/server/service/team_schedule.go +++ b/server/service/team_schedule.go @@ -47,7 +47,7 @@ func (svc Service) GetTeamScheduledQueries(ctx context.Context, teamID uint, opt if teamID != 0 { teamID_ = &teamID } - queries, err := svc.ListQueries(ctx, opts, teamID_, ptr.Bool(true), false) + queries, err := svc.ListQueries(ctx, opts, teamID_, ptr.Bool(true)) if err != nil { return nil, err }