mirror of
https://github.com/fleetdm/fleet
synced 2026-05-23 17:08:53 +00:00
15605 Merge Inherited Queries and Policies (#18813)
This commit is contained in:
commit
15ab9a4ed8
56 changed files with 1688 additions and 803 deletions
2
changes/15605-merge-inherited-and-team-queries-policies
Normal file
2
changes/15605-merge-inherited-and-team-queries-policies
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
- 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
|
||||
|
|
@ -11,7 +11,7 @@ const DEFAULT_POLICY_MOCK: IPolicyStats = {
|
|||
author_id: 1,
|
||||
author_name: "Test User",
|
||||
author_email: "test@user.com",
|
||||
team_id: undefined,
|
||||
team_id: null,
|
||||
resolution: "Ensure ClamAV and Freshclam are installed and running.",
|
||||
platform: "linux" as const,
|
||||
created_at: "2023-03-24T22:13:59Z",
|
||||
|
|
@ -29,4 +29,87 @@ const createMockPolicy = (overrides?: Partial<IPolicyStats>): IPolicyStats => {
|
|||
return { ...DEFAULT_POLICY_MOCK, ...overrides };
|
||||
};
|
||||
|
||||
export const createMockPoliciesResponse = (
|
||||
overrides?: Partial<IPolicyStats>
|
||||
) => {
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -39,10 +39,9 @@
|
|||
|
||||
&__info,
|
||||
&__additional-info {
|
||||
@include help-text;
|
||||
line-height: 1.5;
|
||||
text-align: center;
|
||||
color: $core-fleet-blue;
|
||||
font-size: $x-small;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
|
|
|
|||
40
frontend/components/InheritedBadge/InheritedBadge.tsx
Normal file
40
frontend/components/InheritedBadge/InheritedBadge.tsx
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
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 (
|
||||
<div className={baseClass}>
|
||||
<span
|
||||
className={`${baseClass}__element-text`}
|
||||
data-tooltip-id={tooltipId}
|
||||
>
|
||||
Inherited
|
||||
</span>
|
||||
<ReactTooltip5
|
||||
className={`${baseClass}__tooltip-text`}
|
||||
disableStyleInjection
|
||||
place={tooltipPosition}
|
||||
opacity={1}
|
||||
id={tooltipId}
|
||||
offset={8}
|
||||
positionStrategy="fixed"
|
||||
>
|
||||
{tooltipContent}
|
||||
</ReactTooltip5>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default InheritedBadge;
|
||||
19
frontend/components/InheritedBadge/_styles.scss
Normal file
19
frontend/components/InheritedBadge/_styles.scss
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
.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;
|
||||
}
|
||||
}
|
||||
1
frontend/components/InheritedBadge/index.ts
Normal file
1
frontend/components/InheritedBadge/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { default } from "./InheritedBadge";
|
||||
|
|
@ -69,8 +69,9 @@ interface IHeaderGroup extends HeaderGroup {
|
|||
|
||||
const CLIENT_SIDE_DEFAULT_PAGE_SIZE = 20;
|
||||
|
||||
// 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
|
||||
// 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
|
||||
|
||||
const DataTable = ({
|
||||
columns: tableColumns,
|
||||
data: tableData,
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import Icon from "components/Icon/Icon";
|
|||
import { COLORS } from "styles/var/colors";
|
||||
|
||||
import DataTable from "./DataTable/DataTable";
|
||||
import TableContainerUtils from "./TableContainerUtils";
|
||||
import TableContainerUtils from "./utilities/TableContainerUtils";
|
||||
import { IActionButtonProps } from "./DataTable/ActionButton/ActionButton";
|
||||
|
||||
export interface ITableQueryData {
|
||||
|
|
@ -100,6 +100,7 @@ interface ITableContainerProps<T = any> {
|
|||
setExportRows?: (rows: Row[]) => void;
|
||||
resetPageIndex?: boolean;
|
||||
disableTableHeader?: boolean;
|
||||
show0Count?: boolean;
|
||||
}
|
||||
|
||||
const baseClass = "table-container";
|
||||
|
|
@ -156,6 +157,7 @@ const TableContainer = <T,>({
|
|||
setExportRows,
|
||||
resetPageIndex,
|
||||
disableTableHeader,
|
||||
show0Count,
|
||||
}: ITableContainerProps<T>) => {
|
||||
const [searchQuery, setSearchQuery] = useState(defaultSearchQuery);
|
||||
const [sortHeader, setSortHeader] = useState(defaultSortHeader || "");
|
||||
|
|
@ -321,7 +323,7 @@ const TableContainer = <T,>({
|
|||
)}
|
||||
{!renderCount &&
|
||||
!disableCount &&
|
||||
(isMultiColumnFilter || displayCount()) ? (
|
||||
(isMultiColumnFilter || displayCount() || show0Count) ? (
|
||||
<div
|
||||
className={`${baseClass}__results-count ${
|
||||
stackControls ? "stack-table-controls" : ""
|
||||
|
|
@ -330,7 +332,8 @@ const TableContainer = <T,>({
|
|||
>
|
||||
{TableContainerUtils.generateResultsCountText(
|
||||
resultsTitle,
|
||||
displayCount()
|
||||
displayCount(),
|
||||
show0Count
|
||||
)}
|
||||
{resultsHtml}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -2,9 +2,10 @@ const DEFAULT_RESULTS_NAME = "results";
|
|||
|
||||
const generateResultsCountText = (
|
||||
name: string = DEFAULT_RESULTS_NAME,
|
||||
resultsCount: number
|
||||
resultsCount: number,
|
||||
show0Count = false
|
||||
): string => {
|
||||
if (resultsCount === 0) return `No ${name}`;
|
||||
if (resultsCount === 0 && !show0Count) 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
|
||||
50
frontend/components/TableContainer/utilities/config_utils.ts
Normal file
50
frontend/components/TableContainer/utilities/config_utils.ts
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
// from https://stackoverflow.com/a/68213902/15458245
|
||||
|
||||
import { HeaderProps, Row } from "react-table";
|
||||
|
||||
interface GetConditionalSelectHeaderCheckboxProps {
|
||||
/** react-table header props */
|
||||
headerProps: React.PropsWithChildren<HeaderProps<any>>;
|
||||
checkIfRowIsSelectable: (row: Row<any>) => boolean;
|
||||
}
|
||||
|
||||
export const getConditionalSelectHeaderCheckboxProps = ({
|
||||
headerProps,
|
||||
checkIfRowIsSelectable,
|
||||
}: GetConditionalSelectHeaderCheckboxProps) => {
|
||||
// Define if the checkbox should show as checked or indeterminate
|
||||
const checkIfAllSelectableRowsSelected = (rows: Row<any>[]) =>
|
||||
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 };
|
||||
|
|
@ -56,7 +56,6 @@ const DropdownOptionTooltipWrapper = ({
|
|||
clickable={clickable}
|
||||
offset={offset}
|
||||
positionStrategy="fixed"
|
||||
classNameArrow="tooltip-arrow"
|
||||
>
|
||||
{tipContent}
|
||||
</ReactTooltip5>
|
||||
|
|
|
|||
|
|
@ -17,21 +17,5 @@
|
|||
text-align: center;
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
@include tooltip5-arrow-styles;
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -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,
|
||||
editingExistingQuery: DEFAULT_QUERY.editingExistingQuery ?? false,
|
||||
selectedQueryTargets: DEFAULT_TARGETS,
|
||||
selectedQueryTargetsByType: DEFAULT_TARGETS_BY_TYPE,
|
||||
setLastEditedQueryId: () => null,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { IPolicy } from "./policy";
|
||||
import { IQuery } from "./query";
|
||||
import { IScheduledQueryStats } from "./scheduled_query_stats";
|
||||
import { ISchedulableQueryStats } from "./schedulable_query";
|
||||
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?: IScheduledQueryStats;
|
||||
stats?: ISchedulableQueryStats;
|
||||
host_id?: number;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ export interface IPolicy {
|
|||
author_email: string;
|
||||
resolution: string;
|
||||
platform: SelectedPlatformString;
|
||||
team_id?: number;
|
||||
team_id: number | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
critical: boolean;
|
||||
|
|
@ -74,14 +74,16 @@ 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[];
|
||||
inherited_policies: IPolicyStats[];
|
||||
policies?: IPolicyStats[];
|
||||
}
|
||||
|
||||
export interface IPolicyFormData {
|
||||
description?: string | number | boolean | undefined;
|
||||
resolution?: string | number | boolean | undefined;
|
||||
|
|
@ -89,7 +91,7 @@ export interface IPolicyFormData {
|
|||
platform?: SelectedPlatformString;
|
||||
name?: string | number | boolean | undefined;
|
||||
query?: string | number | boolean | undefined;
|
||||
team_id?: number;
|
||||
team_id?: number | null;
|
||||
id?: number;
|
||||
calendar_events_enabled?: boolean;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { IFormField } from "./form_field";
|
||||
import { IPack } from "./pack";
|
||||
import { ISchedulableQuery } from "./schedulable_query";
|
||||
import { IScheduledQueryStats } from "./scheduled_query_stats";
|
||||
import { ISchedulableQuery, ISchedulableQueryStats } from "./schedulable_query";
|
||||
|
||||
export interface IEditQueryFormData {
|
||||
description?: string | number | boolean | undefined;
|
||||
|
|
@ -32,7 +31,7 @@ export interface IQuery {
|
|||
author_email: string;
|
||||
observer_can_run: boolean;
|
||||
packs: IPack[];
|
||||
stats?: IScheduledQueryStats;
|
||||
stats?: ISchedulableQueryStats;
|
||||
}
|
||||
|
||||
export interface IEditQueryFormFields {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import PropTypes, { number } from "prop-types";
|
||||
|
||||
import scheduledQueryStatsInterface, {
|
||||
IScheduledQueryStats,
|
||||
} from "./scheduled_query_stats";
|
||||
import ILegacySchedulableQueryStats, {
|
||||
ISchedulableQueryStats,
|
||||
} from "./schedulable_query";
|
||||
|
||||
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: scheduledQueryStatsInterface,
|
||||
stats: ILegacySchedulableQueryStats,
|
||||
});
|
||||
|
||||
export interface IQueryStats {
|
||||
|
|
@ -42,5 +42,5 @@ export interface IQueryStats {
|
|||
system_time: number;
|
||||
user_time: number;
|
||||
wall_time?: number;
|
||||
stats?: IScheduledQueryStats;
|
||||
stats?: ISchedulableQueryStats;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,6 @@
|
|||
// 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";
|
||||
|
|
@ -24,7 +27,7 @@ export interface ISchedulableQuery {
|
|||
discard_data: boolean;
|
||||
packs: IPack[];
|
||||
stats: ISchedulableQueryStats;
|
||||
editingExistingQuery: boolean;
|
||||
editingExistingQuery?: boolean;
|
||||
}
|
||||
|
||||
export interface IEnhancedQuery extends ISchedulableQuery {
|
||||
|
|
@ -32,13 +35,22 @@ export interface IEnhancedQuery extends ISchedulableQuery {
|
|||
platforms: SupportedPlatform[];
|
||||
}
|
||||
export interface ISchedulableQueryStats {
|
||||
user_time_p50?: number;
|
||||
user_time_p95?: number;
|
||||
system_time_p50?: number;
|
||||
system_time_p95?: number;
|
||||
user_time_p50?: number | null;
|
||||
user_time_p95?: number | null;
|
||||
system_time_p50?: number | null;
|
||||
system_time_p95?: number | null;
|
||||
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
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
// legacy interfaces to maintain packs support
|
||||
import PropTypes from "prop-types";
|
||||
import scheduledQueryStatsInterface, {
|
||||
IScheduledQueryStats,
|
||||
} from "./scheduled_query_stats";
|
||||
import ILegacySchedulableQueryStats, {
|
||||
ISchedulableQueryStats,
|
||||
} from "interfaces/schedulable_query";
|
||||
|
||||
export default PropTypes.shape({
|
||||
created_at: PropTypes.string,
|
||||
|
|
@ -19,7 +20,7 @@ export default PropTypes.shape({
|
|||
version: PropTypes.string,
|
||||
shard: PropTypes.number,
|
||||
denylist: PropTypes.bool,
|
||||
stats: scheduledQueryStatsInterface,
|
||||
stats: ILegacySchedulableQueryStats,
|
||||
});
|
||||
|
||||
export interface IPackQueryFormData {
|
||||
|
|
@ -52,7 +53,7 @@ export interface IScheduledQuery {
|
|||
shard?: number | undefined;
|
||||
denylist?: boolean;
|
||||
logging_type?: string;
|
||||
stats: IScheduledQueryStats;
|
||||
stats: ISchedulableQueryStats;
|
||||
team_id?: number;
|
||||
}
|
||||
export interface IEditScheduledQuery extends IScheduledQuery {
|
||||
|
|
|
|||
|
|
@ -1,17 +0,0 @@
|
|||
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;
|
||||
}
|
||||
|
|
@ -112,7 +112,7 @@ interface IManageHostsProps {
|
|||
router: InjectedRouter;
|
||||
params: Params;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
location: any; // no type in react-router v3
|
||||
location: any; // no type in react-router v3 TODO: Improve this type
|
||||
}
|
||||
|
||||
const CSV_HOSTS_TITLE = "Hosts";
|
||||
|
|
|
|||
|
|
@ -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 { noop, isEqual } from "lodash";
|
||||
import { isEqual } from "lodash";
|
||||
|
||||
import { getNextLocationPath } from "utilities/helpers";
|
||||
|
||||
|
|
@ -18,6 +18,7 @@ import {
|
|||
ILoadAllPoliciesResponse,
|
||||
ILoadTeamPoliciesResponse,
|
||||
IPoliciesCountResponse,
|
||||
IPolicy,
|
||||
} from "interfaces/policy";
|
||||
import { ITeamConfig } from "interfaces/team";
|
||||
|
||||
|
|
@ -36,7 +37,6 @@ 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,10 +62,6 @@ 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;
|
||||
};
|
||||
|
|
@ -138,9 +134,10 @@ const ManagePolicyPage = ({
|
|||
const [showAddPolicyModal, setShowAddPolicyModal] = useState(false);
|
||||
const [showDeletePolicyModal, setShowDeletePolicyModal] = useState(false);
|
||||
const [showCalendarEventsModal, setShowCalendarEventsModal] = useState(false);
|
||||
|
||||
const [teamPolicies, setTeamPolicies] = useState<IPolicyStats[]>();
|
||||
const [inheritedPolicies, setInheritedPolicies] = useState<IPolicyStats[]>();
|
||||
const [
|
||||
policiesAvailableToAutomate,
|
||||
setPoliciesAvailableToAutomate,
|
||||
] = useState<IPolicyStats[]>([]);
|
||||
|
||||
// Functions to avoid race conditions
|
||||
const initialSearchQuery = (() => queryParams.query ?? "")();
|
||||
|
|
@ -152,40 +149,15 @@ 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<ITableQueryData>();
|
||||
const [
|
||||
inheritedTableQueryData,
|
||||
setInheritedTableQueryData,
|
||||
] = useState<ITableQueryData>();
|
||||
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);
|
||||
|
|
@ -199,9 +171,6 @@ const ManagePolicyPage = ({
|
|||
setSearchQuery(initialSearchQuery);
|
||||
setSortHeader(initialSortHeader);
|
||||
setSortDirection(initialSortDirection);
|
||||
setInheritedPage(initialInheritedPage);
|
||||
setInheritedSortHeader(initialInheritedSortHeader);
|
||||
setInheritedSortDirection(initialInheritedSortDirection);
|
||||
}, [location, isRouteOk]);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -246,8 +215,11 @@ const ManagePolicyPage = ({
|
|||
},
|
||||
{
|
||||
enabled: isRouteOk && !isAnyTeamSelected,
|
||||
select: (data) => data.policies,
|
||||
select: (data) => data.policies || [],
|
||||
staleTime: 5000,
|
||||
onSuccess: (data) => {
|
||||
setPoliciesAvailableToAutomate(data || []);
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
|
|
@ -260,7 +232,7 @@ const ManagePolicyPage = ({
|
|||
[
|
||||
{
|
||||
scope: "policiesCount",
|
||||
query: isAnyTeamSelected ? "" : searchQuery, // Search query not used for inherited count
|
||||
query: isAnyTeamSelected ? "" : searchQuery,
|
||||
},
|
||||
],
|
||||
({ queryKey }) => globalPoliciesAPI.getCount(queryKey[0]),
|
||||
|
|
@ -274,13 +246,14 @@ const ManagePolicyPage = ({
|
|||
);
|
||||
|
||||
const {
|
||||
data: teamPolicies,
|
||||
error: teamPoliciesError,
|
||||
isFetching: isFetchingTeamPolicies,
|
||||
refetch: refetchTeamPolicies,
|
||||
} = useQuery<
|
||||
ILoadTeamPoliciesResponse,
|
||||
Error,
|
||||
ILoadTeamPoliciesResponse,
|
||||
IPolicyStats[],
|
||||
ITeamPoliciesQueryKey[]
|
||||
>(
|
||||
[
|
||||
|
|
@ -291,11 +264,8 @@ 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 }) => {
|
||||
|
|
@ -303,13 +273,44 @@ const ManagePolicyPage = ({
|
|||
},
|
||||
{
|
||||
enabled: isRouteOk && isPremiumTier && !!teamIdForApi,
|
||||
select: (data: ILoadTeamPoliciesResponse) => data.policies || [],
|
||||
onSuccess: (data) => {
|
||||
setTeamPolicies(data.policies);
|
||||
setInheritedPolicies(data.inherited_policies);
|
||||
const allPoliciesAvailableToAutomate = data.filter(
|
||||
(policy: IPolicy) => policy.team_id === currentTeamId
|
||||
);
|
||||
setPoliciesAvailableToAutomate(allPoliciesAvailableToAutomate || []);
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
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,
|
||||
|
|
@ -325,6 +326,7 @@ const ManagePolicyPage = ({
|
|||
scope: "teamPoliciesCount",
|
||||
query: searchQuery,
|
||||
teamId: teamIdForApi || 0, // TODO: Fix number/undefined type
|
||||
mergeInherited: false,
|
||||
},
|
||||
],
|
||||
({ queryKey }) => teamPoliciesAPI.getCount(queryKey[0]),
|
||||
|
|
@ -337,9 +339,10 @@ const ManagePolicyPage = ({
|
|||
}
|
||||
);
|
||||
|
||||
const canAddOrDeletePolicy: boolean =
|
||||
const canAddOrDeletePolicy =
|
||||
isGlobalAdmin || isGlobalMaintainer || isTeamMaintainer || isTeamAdmin;
|
||||
const canManageAutomations: boolean = isGlobalAdmin || isTeamAdmin;
|
||||
const canManageAutomations = isGlobalAdmin || isTeamAdmin;
|
||||
const hasPoliciesToAutomateOrDelete = policiesAvailableToAutomate.length > 0;
|
||||
|
||||
const {
|
||||
data: config,
|
||||
|
|
@ -375,6 +378,7 @@ const ManagePolicyPage = ({
|
|||
const refetchPolicies = (teamId?: number) => {
|
||||
if (teamId) {
|
||||
refetchTeamPolicies();
|
||||
refetchTeamPoliciesCountMergeInherited();
|
||||
refetchTeamPoliciesCount();
|
||||
} else {
|
||||
refetchGlobalPolicies(); // Only call on global policies as this is expensive
|
||||
|
|
@ -391,72 +395,36 @@ 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;
|
||||
}
|
||||
|
||||
newTableQuery.editingInheritedTable
|
||||
? setInheritedTableQueryData({ ...newTableQuery })
|
||||
: setTableQueryData({ ...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;
|
||||
|
||||
// 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";
|
||||
}
|
||||
}
|
||||
newQueryParams.order_key = newSortHeader;
|
||||
newQueryParams.order_direction = newSortDirection;
|
||||
newQueryParams.page = newPageIndex.toString();
|
||||
|
||||
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";
|
||||
}
|
||||
// Reset page number to 0 for new filters
|
||||
if (
|
||||
newSortDirection !== sortDirection ||
|
||||
newSortHeader !== sortHeader ||
|
||||
newSearchQuery !== searchQuery
|
||||
) {
|
||||
newQueryParams.page = "0";
|
||||
}
|
||||
|
||||
if (isRouteOk && teamIdForApi !== undefined) {
|
||||
|
|
@ -470,14 +438,7 @@ const ManagePolicyPage = ({
|
|||
|
||||
router?.replace(locationPath);
|
||||
},
|
||||
[
|
||||
isRouteOk,
|
||||
teamIdForApi,
|
||||
searchQuery,
|
||||
showInheritedTable,
|
||||
inheritedSortDirection,
|
||||
sortDirection,
|
||||
] // Other dependencies can cause infinite re-renders as URL is source of truth
|
||||
[isRouteOk, teamIdForApi, searchQuery, sortDirection] // Other dependencies can cause infinite re-renders as URL is source of truth
|
||||
);
|
||||
|
||||
const toggleOtherWorkflowsModal = () =>
|
||||
|
|
@ -504,19 +465,6 @@ 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<IWebhookSettings, "failing_policies_webhook">;
|
||||
integrations: IZendeskJiraIntegrations;
|
||||
|
|
@ -574,7 +522,7 @@ const ManagePolicyPage = ({
|
|||
|
||||
// update changed policies calendar events enabled
|
||||
const changedPolicies = formData.policies.filter((formPolicy) => {
|
||||
const prevPolicyState = teamPolicies?.find(
|
||||
const prevPolicyState = policiesAvailableToAutomate.find(
|
||||
(policy) => policy.id === formPolicy.id
|
||||
);
|
||||
return (
|
||||
|
|
@ -649,24 +597,6 @@ 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;
|
||||
|
|
@ -731,13 +661,15 @@ const ManagePolicyPage = ({
|
|||
onAddPolicyClick={onAddPolicyClick}
|
||||
onDeletePolicyClick={onDeletePolicyClick}
|
||||
canAddOrDeletePolicy={canAddOrDeletePolicy}
|
||||
hasPoliciesToDelete={hasPoliciesToAutomateOrDelete}
|
||||
currentTeam={currentTeamSummary}
|
||||
currentAutomatedPolicies={currentAutomatedPolicies}
|
||||
renderPoliciesCount={() =>
|
||||
!isFetchingTeamCount && renderPoliciesCount(teamPoliciesCount)
|
||||
(!isFetchingTeamCountMergeInherited &&
|
||||
renderPoliciesCount(teamPoliciesCountMergeInherited)) ||
|
||||
null
|
||||
}
|
||||
isPremiumTier={isPremiumTier}
|
||||
isSandboxMode={isSandboxMode}
|
||||
searchQuery={searchQuery}
|
||||
sortHeader={sortHeader}
|
||||
sortDirection={sortDirection}
|
||||
|
|
@ -753,12 +685,14 @@ const ManagePolicyPage = ({
|
|||
onAddPolicyClick={onAddPolicyClick}
|
||||
onDeletePolicyClick={onDeletePolicyClick}
|
||||
canAddOrDeletePolicy={canAddOrDeletePolicy}
|
||||
hasPoliciesToDelete={hasPoliciesToAutomateOrDelete}
|
||||
currentTeam={currentTeamSummary}
|
||||
currentAutomatedPolicies={currentAutomatedPolicies}
|
||||
isPremiumTier={isPremiumTier}
|
||||
isSandboxMode={isSandboxMode}
|
||||
renderPoliciesCount={() =>
|
||||
!isFetchingGlobalCount && renderPoliciesCount(globalPoliciesCount)
|
||||
(!isFetchingGlobalCount &&
|
||||
renderPoliciesCount(globalPoliciesCount)) ||
|
||||
null
|
||||
}
|
||||
searchQuery={searchQuery}
|
||||
sortHeader={sortHeader}
|
||||
|
|
@ -834,17 +768,19 @@ const ManagePolicyPage = ({
|
|||
</div>
|
||||
{showCtaButtons && (
|
||||
<div className={`${baseClass} button-wrap`}>
|
||||
{canManageAutomations && automationsConfig && (
|
||||
<div className={`${baseClass}__manage-automations-wrapper`}>
|
||||
<Dropdown
|
||||
className={`${baseClass}__manage-automations-dropdown`}
|
||||
onChange={onSelectAutomationOption}
|
||||
placeholder="Manage automations"
|
||||
searchable={false}
|
||||
options={getAutomationsDropdownOptions()}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{canManageAutomations &&
|
||||
automationsConfig &&
|
||||
hasPoliciesToAutomateOrDelete && (
|
||||
<div className={`${baseClass}__manage-automations-wrapper`}>
|
||||
<Dropdown
|
||||
className={`${baseClass}__manage-automations-dropdown`}
|
||||
onChange={onSelectAutomationOption}
|
||||
placeholder="Manage automations"
|
||||
searchable={false}
|
||||
options={getAutomationsDropdownOptions()}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{canAddOrDeletePolicy && (
|
||||
<div className={`${baseClass}__action-button-container`}>
|
||||
<Button
|
||||
|
|
@ -852,7 +788,7 @@ const ManagePolicyPage = ({
|
|||
className={`${baseClass}__select-policy-button`}
|
||||
onClick={onAddPolicyClick}
|
||||
>
|
||||
Add a policy
|
||||
Add policy
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -867,57 +803,11 @@ const ManagePolicyPage = ({
|
|||
</p>
|
||||
</div>
|
||||
{renderMainTable()}
|
||||
{showInheritedPoliciesButton && globalPoliciesCount && (
|
||||
<RevealButton
|
||||
isShowing={showInheritedTable}
|
||||
className={baseClass}
|
||||
hideText={inheritedPoliciesButtonText(
|
||||
showInheritedTable,
|
||||
globalPoliciesCount
|
||||
)}
|
||||
showText={inheritedPoliciesButtonText(
|
||||
showInheritedTable,
|
||||
globalPoliciesCount
|
||||
)}
|
||||
caretPosition="before"
|
||||
tooltipContent={
|
||||
<>
|
||||
"All teams" policies are checked
|
||||
<br />
|
||||
for this team's hosts.
|
||||
</>
|
||||
}
|
||||
onClick={toggleShowInheritedPolicies}
|
||||
/>
|
||||
)}
|
||||
{showInheritedPoliciesButton && showInheritedTable && (
|
||||
<div className={`${baseClass}__inherited-policies-table`}>
|
||||
{globalPoliciesError && <TableDataError />}
|
||||
{!globalPoliciesError && (
|
||||
<PoliciesTable
|
||||
isLoading={isFetchingTeamPolicies}
|
||||
policiesList={inheritedPolicies || []}
|
||||
onDeletePolicyClick={noop}
|
||||
canAddOrDeletePolicy={canAddOrDeletePolicy}
|
||||
tableType="inheritedPolicies"
|
||||
currentTeam={currentTeamSummary}
|
||||
searchQuery=""
|
||||
renderPoliciesCount={() =>
|
||||
renderPoliciesCount(teamPoliciesCount)
|
||||
}
|
||||
sortHeader={inheritedSortHeader}
|
||||
sortDirection={inheritedSortDirection}
|
||||
page={inheritedPage}
|
||||
onQueryChange={onQueryChange}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{config && automationsConfig && showOtherWorkflowsModal && (
|
||||
<OtherWorkflowsModal
|
||||
automationsConfig={automationsConfig}
|
||||
availableIntegrations={config.integrations}
|
||||
availablePolicies={availablePoliciesForAutomation}
|
||||
availablePolicies={policiesAvailableToAutomate}
|
||||
isUpdatingAutomations={isUpdatingAutomations}
|
||||
onExit={toggleOtherWorkflowsModal}
|
||||
handleSubmit={handleUpdateOtherWorkflows}
|
||||
|
|
@ -950,7 +840,7 @@ const ManagePolicyPage = ({
|
|||
?.enable_calendar_events ?? false
|
||||
}
|
||||
url={teamConfig?.integrations.google_calendar?.webhook_url || ""}
|
||||
policies={teamPolicies || []}
|
||||
policies={policiesAvailableToAutomate}
|
||||
isUpdating={updatingPolicyEnabledCalendarEvents}
|
||||
/>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -165,8 +165,8 @@
|
|||
}
|
||||
}
|
||||
|
||||
.critical-tooltip {
|
||||
text-align: left;
|
||||
.critical-tooltip,
|
||||
.inherited-tooltip {
|
||||
font-weight: $regular;
|
||||
}
|
||||
|
||||
|
|
@ -184,8 +184,13 @@
|
|||
display: flex; // required for inline icon
|
||||
gap: $pad-xsmall;
|
||||
|
||||
.tooltip-base {
|
||||
display: inline-flex;
|
||||
// Underlines only the name text
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
|
||||
.policy-name-text {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.policy-name-text {
|
||||
|
|
@ -194,6 +199,25 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,42 +1,84 @@
|
|||
import React from "react";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { screen, waitFor } from "@testing-library/react";
|
||||
import { noop } from "lodash";
|
||||
import { createCustomRenderer } from "test/test-utils";
|
||||
|
||||
import createMockUser from "__mocks__/userMock";
|
||||
import createMockPolicy from "__mocks__/policyMock";
|
||||
import PoliciesTable from "./PoliciesTable";
|
||||
|
||||
describe("Policies table", () => {
|
||||
const testCriticalPolicy = createMockPolicy({ critical: true });
|
||||
it("Renders the page-wide empty state when no policies are present", async () => {
|
||||
const render = createCustomRenderer({
|
||||
context: {
|
||||
app: {
|
||||
isGlobalAdmin: true,
|
||||
currentUser: createMockUser(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
it("Renders a tooltip including 'Premium feature' copy for a critical policy in Sandbox mode", () => {
|
||||
render(
|
||||
<PoliciesTable
|
||||
policiesList={[testCriticalPolicy]}
|
||||
policiesList={[]}
|
||||
isLoading={false}
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
onDeletePolicyClick={() => {}}
|
||||
currentTeam={{ id: -1, name: "All teams" }}
|
||||
isPremiumTier
|
||||
isSandboxMode
|
||||
searchQuery=""
|
||||
page={0}
|
||||
onQueryChange={noop}
|
||||
renderPoliciesCount={noop}
|
||||
renderPoliciesCount={() => null}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.getByText("This policy has been marked as critical.", {
|
||||
exact: false,
|
||||
})
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText("This is a premium feature.", { exact: false })
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText("You don't have any policies")).toBeInTheDocument();
|
||||
expect(screen.queryByText("Name")).toBeNull();
|
||||
});
|
||||
|
||||
it("Renders a tooltip excluding 'Premium feature' copy for a critical policy not in Sandbox mode", () => {
|
||||
it("Renders the empty search state when search query exists for server side search with no results", async () => {
|
||||
const render = createCustomRenderer({
|
||||
context: {
|
||||
app: {
|
||||
isGlobalAdmin: true,
|
||||
currentUser: createMockUser(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
render(
|
||||
<PoliciesTable
|
||||
policiesList={[]}
|
||||
isLoading={false}
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
onDeletePolicyClick={() => {}}
|
||||
currentTeam={{ id: -1, name: "All teams" }}
|
||||
isPremiumTier
|
||||
searchQuery="shouldn't match anything"
|
||||
page={0}
|
||||
onQueryChange={noop}
|
||||
renderPoliciesCount={() => null}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText("No matching policies")).toBeInTheDocument();
|
||||
expect(screen.queryByText("Name")).toBeNull();
|
||||
});
|
||||
|
||||
it("Renders a critical badge and tooltip for a critical policy", async () => {
|
||||
const render = createCustomRenderer({
|
||||
context: {
|
||||
app: {
|
||||
isGlobalAdmin: true,
|
||||
currentUser: createMockUser(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const testCriticalPolicy = createMockPolicy({ critical: true });
|
||||
|
||||
const { user } = render(
|
||||
<PoliciesTable
|
||||
policiesList={[testCriticalPolicy]}
|
||||
isLoading={false}
|
||||
|
|
@ -44,21 +86,148 @@ describe("Policies table", () => {
|
|||
onDeletePolicyClick={() => {}}
|
||||
currentTeam={{ id: -1, name: "All teams" }}
|
||||
isPremiumTier
|
||||
isSandboxMode={false}
|
||||
searchQuery=""
|
||||
page={0}
|
||||
onQueryChange={noop}
|
||||
renderPoliciesCount={noop}
|
||||
renderPoliciesCount={() => null}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.getByText("This policy has been marked as critical.", {
|
||||
exact: false,
|
||||
})
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByText("This is a premium feature.", { exact: false })
|
||||
).toBeNull();
|
||||
await waitFor(() => {
|
||||
waitFor(() => {
|
||||
user.hover(screen.getByTestId("policy-icon"));
|
||||
});
|
||||
|
||||
expect(
|
||||
screen.getByText("This policy has been marked as critical.")
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("Renders an inherited badge and tooltip for inherited policy on a team's policies page", async () => {
|
||||
const render = createCustomRenderer({
|
||||
context: {
|
||||
app: {
|
||||
isGlobalAdmin: true,
|
||||
currentUser: createMockUser(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const testInheritedPolicy = createMockPolicy({ team_id: null });
|
||||
|
||||
const { user } = render(
|
||||
<PoliciesTable
|
||||
policiesList={[testInheritedPolicy]}
|
||||
isLoading={false}
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
onDeletePolicyClick={() => {}}
|
||||
currentTeam={{ id: 2, name: "Team 2" }}
|
||||
isPremiumTier
|
||||
searchQuery=""
|
||||
page={0}
|
||||
onQueryChange={noop}
|
||||
renderPoliciesCount={() => null}
|
||||
/>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
waitFor(() => {
|
||||
user.hover(screen.getByText("Inherited"));
|
||||
});
|
||||
|
||||
expect(
|
||||
screen.getByText("This policy runs on all hosts.")
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("Does not render an inherited badge and tooltip for global policy on the All teams's policies page", () => {
|
||||
const render = createCustomRenderer({
|
||||
context: {
|
||||
app: {
|
||||
isGlobalAdmin: true,
|
||||
currentUser: createMockUser(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const testGlobalPolicy = createMockPolicy({ team_id: null });
|
||||
|
||||
render(
|
||||
<PoliciesTable
|
||||
policiesList={[testGlobalPolicy]}
|
||||
isLoading={false}
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
onDeletePolicyClick={() => {}}
|
||||
currentTeam={{ id: -1, name: "All teams" }}
|
||||
isPremiumTier
|
||||
searchQuery=""
|
||||
page={0}
|
||||
onQueryChange={noop}
|
||||
renderPoliciesCount={() => null}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.queryByText("Inherited")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("Renders the correct number of checkboxes for team policies and not inherited policies on a team's policies page and can check select all box", async () => {
|
||||
const render = createCustomRenderer({
|
||||
context: {
|
||||
app: {
|
||||
isGlobalAdmin: true,
|
||||
currentUser: createMockUser(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const testInheritedPolicies = [
|
||||
createMockPolicy({ team_id: null, name: "Inherited policy 1" }),
|
||||
createMockPolicy({ id: 2, team_id: null, name: "Inherited policy 2" }),
|
||||
createMockPolicy({ id: 3, team_id: null, name: "Inherited policy 3" }),
|
||||
];
|
||||
|
||||
const testTeamPolicies = [
|
||||
createMockPolicy({ id: 4, team_id: 2, name: "Team policy 1" }),
|
||||
createMockPolicy({ id: 5, team_id: 2, name: "Team policy 2" }),
|
||||
];
|
||||
|
||||
const { container, user } = render(
|
||||
<PoliciesTable
|
||||
policiesList={[...testInheritedPolicies, ...testTeamPolicies]}
|
||||
isLoading={false}
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
onDeletePolicyClick={() => {}}
|
||||
currentTeam={{ id: 2, name: "Team 2" }}
|
||||
isPremiumTier
|
||||
searchQuery=""
|
||||
page={0}
|
||||
onQueryChange={noop}
|
||||
renderPoliciesCount={() => null}
|
||||
canAddOrDeletePolicy
|
||||
hasPoliciesToDelete
|
||||
/>
|
||||
);
|
||||
|
||||
const numberOfCheckboxes = container.querySelectorAll(
|
||||
"input[type='checkbox']"
|
||||
).length;
|
||||
|
||||
expect(numberOfCheckboxes).toBe(
|
||||
testTeamPolicies.length + 1 // +1 for Select all checkbox
|
||||
);
|
||||
|
||||
const checkbox = container.querySelectorAll(
|
||||
"input[type='checkbox']"
|
||||
)[0] as HTMLInputElement;
|
||||
|
||||
await waitFor(() => {
|
||||
waitFor(() => {
|
||||
user.click(checkbox);
|
||||
});
|
||||
|
||||
expect(checkbox.checked).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
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";
|
||||
|
|
@ -14,12 +13,6 @@ 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";
|
||||
|
||||
|
|
@ -29,13 +22,11 @@ interface IPoliciesTableProps {
|
|||
onAddPolicyClick?: () => void;
|
||||
onDeletePolicyClick: (selectedTableIds: number[]) => void;
|
||||
canAddOrDeletePolicy?: boolean;
|
||||
tableType?: "inheritedPolicies";
|
||||
hasPoliciesToDelete?: boolean;
|
||||
currentTeam: ITeamSummary | undefined;
|
||||
currentAutomatedPolicies?: number[];
|
||||
isPremiumTier?: boolean;
|
||||
isSandboxMode?: boolean;
|
||||
// onClientSidePaginationChange?: (pageIndex: number) => void;
|
||||
renderPoliciesCount: any; // TODO: typing
|
||||
renderPoliciesCount: () => JSX.Element | null;
|
||||
onQueryChange: (newTableQuery: ITableQueryData) => void;
|
||||
searchQuery: string;
|
||||
sortHeader?: "name" | "failing_host_count";
|
||||
|
|
@ -49,13 +40,11 @@ const PoliciesTable = ({
|
|||
onAddPolicyClick,
|
||||
onDeletePolicyClick,
|
||||
canAddOrDeletePolicy,
|
||||
tableType,
|
||||
hasPoliciesToDelete,
|
||||
currentTeam,
|
||||
currentAutomatedPolicies,
|
||||
isPremiumTier,
|
||||
isSandboxMode,
|
||||
onQueryChange,
|
||||
// onClientSidePaginationChange,
|
||||
renderPoliciesCount,
|
||||
searchQuery,
|
||||
sortHeader,
|
||||
|
|
@ -64,23 +53,18 @@ 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 = (
|
||||
|
|
@ -96,9 +80,8 @@ const PoliciesTable = ({
|
|||
if (searchQuery) {
|
||||
delete emptyPolicies.graphicName;
|
||||
delete emptyPolicies.primaryButton;
|
||||
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.";
|
||||
emptyPolicies.header = "No matching policies";
|
||||
emptyPolicies.info = "No policies match the current filters.";
|
||||
}
|
||||
|
||||
return emptyPolicies;
|
||||
|
|
@ -106,23 +89,20 @@ const PoliciesTable = ({
|
|||
|
||||
const searchable = !(policiesList?.length === 0 && searchQuery === "");
|
||||
|
||||
const hasPermissionAndPoliciesToDelete =
|
||||
canAddOrDeletePolicy && hasPoliciesToDelete;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`${baseClass} ${
|
||||
canAddOrDeletePolicy ? "" : "hide-selection-column"
|
||||
}`}
|
||||
>
|
||||
<div className={baseClass}>
|
||||
<TableContainer
|
||||
resultsTitle="policies"
|
||||
columnConfigs={generateTableHeaders(
|
||||
{
|
||||
selectedTeamId: currentTeam?.id,
|
||||
canAddOrDeletePolicy,
|
||||
tableType,
|
||||
hasPermissionAndPoliciesToDelete,
|
||||
},
|
||||
policiesList,
|
||||
isPremiumTier,
|
||||
isSandboxMode
|
||||
isPremiumTier
|
||||
)}
|
||||
data={generateDataSet(
|
||||
policiesList,
|
||||
|
|
@ -152,7 +132,6 @@ const PoliciesTable = ({
|
|||
primaryButton: emptyState().primaryButton,
|
||||
})
|
||||
}
|
||||
disableCount={tableType === "inheritedPolicies"}
|
||||
renderCount={renderPoliciesCount}
|
||||
onQueryChange={onTableQueryChange}
|
||||
inputPlaceHolder="Search by name"
|
||||
|
|
|
|||
|
|
@ -4,23 +4,22 @@
|
|||
import React from "react";
|
||||
import {
|
||||
formatDistanceToNowStrict,
|
||||
isAfter,
|
||||
millisecondsToHours,
|
||||
millisecondsToMinutes,
|
||||
} from "date-fns";
|
||||
import ReactTooltip from "react-tooltip";
|
||||
import { Tooltip as ReactTooltip5 } from "react-tooltip-5";
|
||||
// @ts-ignore
|
||||
import Checkbox from "components/forms/fields/Checkbox";
|
||||
import HeaderCell from "components/TableContainer/DataTable/HeaderCell";
|
||||
import LinkCell from "components/TableContainer/DataTable/LinkCell/LinkCell";
|
||||
import StatusIndicator from "components/StatusIndicator";
|
||||
import Icon from "components/Icon";
|
||||
import { IPolicyStats } from "interfaces/policy";
|
||||
import PATHS from "router/paths";
|
||||
import sortUtils from "utilities/sort";
|
||||
import { PolicyResponse } from "utilities/constants";
|
||||
import { buildQueryStringFromParams } from "utilities/url";
|
||||
import { COLORS } from "styles/var/colors";
|
||||
import InheritedBadge from "components/InheritedBadge";
|
||||
import { getConditionalSelectHeaderCheckboxProps } from "components/TableContainer/utilities/config_utils";
|
||||
import PassingColumnHeader from "../PassingColumnHeader";
|
||||
|
||||
interface IGetToggleAllRowsSelectedProps {
|
||||
|
|
@ -88,11 +87,11 @@ const getPolicyRefreshTime = (ms: number): string => {
|
|||
|
||||
const getTooltip = (osqueryPolicyMs: number): JSX.Element => {
|
||||
return (
|
||||
<span className={`tooltip__tooltip-text`}>
|
||||
<>
|
||||
Fleet is collecting policy results. Try again
|
||||
<br />
|
||||
in about {getPolicyRefreshTime(osqueryPolicyMs)} as the system catches up.
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -101,15 +100,14 @@ const getTooltip = (osqueryPolicyMs: number): JSX.Element => {
|
|||
const generateTableHeaders = (
|
||||
options: {
|
||||
selectedTeamId?: number | null;
|
||||
canAddOrDeletePolicy?: boolean;
|
||||
hasPermissionAndPoliciesToDelete?: boolean;
|
||||
tableType?: string;
|
||||
},
|
||||
policiesList: IPolicyStats[] = [],
|
||||
isPremiumTier?: boolean,
|
||||
isSandboxMode?: boolean
|
||||
isPremiumTier?: boolean
|
||||
): IDataColumn[] => {
|
||||
const { selectedTeamId, tableType, canAddOrDeletePolicy } = options;
|
||||
|
||||
const { selectedTeamId, hasPermissionAndPoliciesToDelete } = options;
|
||||
const viewingTeamPolicies = selectedTeamId !== -1;
|
||||
// Figure the time since the host counts were updated.
|
||||
// First, find first policy item with host_count_updated_at.
|
||||
const updatedAt =
|
||||
|
|
@ -144,11 +142,10 @@ const generateTableHeaders = (
|
|||
<>
|
||||
<div className="policy-name-text">{cellProps.cell.value}</div>
|
||||
{isPremiumTier && cellProps.row.original.critical && (
|
||||
<>
|
||||
<div className="critical-badge">
|
||||
<span
|
||||
className="tooltip-base"
|
||||
data-tip
|
||||
data-for={`critical-tooltip-${cellProps.row.original.id}`}
|
||||
className="critical-badge-icon"
|
||||
data-tooltip-id={`critical-tooltip-${cellProps.row.original.id}`}
|
||||
>
|
||||
<Icon
|
||||
className="critical-policy-icon"
|
||||
|
|
@ -157,23 +154,21 @@ const generateTableHeaders = (
|
|||
color="core-fleet-blue"
|
||||
/>
|
||||
</span>
|
||||
<ReactTooltip
|
||||
<ReactTooltip5
|
||||
className="critical-tooltip"
|
||||
disableStyleInjection
|
||||
place="top"
|
||||
type="dark"
|
||||
effect="solid"
|
||||
opacity={1}
|
||||
id={`critical-tooltip-${cellProps.row.original.id}`}
|
||||
backgroundColor={COLORS["tooltip-bg"]}
|
||||
offset={8}
|
||||
positionStrategy="fixed"
|
||||
>
|
||||
This policy has been marked as critical.
|
||||
{isSandboxMode && (
|
||||
<>
|
||||
<br />
|
||||
This is a premium feature.
|
||||
</>
|
||||
)}
|
||||
</ReactTooltip>
|
||||
</>
|
||||
</ReactTooltip5>
|
||||
</div>
|
||||
)}
|
||||
{viewingTeamPolicies && !cellProps.row.original.team_id && (
|
||||
<InheritedBadge tooltipContent="This policy runs on all hosts." />
|
||||
)}
|
||||
</>
|
||||
}
|
||||
|
|
@ -208,24 +203,24 @@ const generateTableHeaders = (
|
|||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<div className="policy-has-not-run">
|
||||
<span
|
||||
className="text-cell text-muted has-not-run tooltip"
|
||||
data-tip
|
||||
data-for={`passing_${cellProps.row.original.id.toString()}`}
|
||||
data-tooltip-id={`passing_${cellProps.row.original.id.toString()}`}
|
||||
>
|
||||
---
|
||||
</span>
|
||||
<ReactTooltip
|
||||
place="bottom"
|
||||
effect="solid"
|
||||
backgroundColor={COLORS["tooltip-bg"]}
|
||||
<ReactTooltip5
|
||||
className="policy-has-not-run-tooltip"
|
||||
disableStyleInjection
|
||||
place="top"
|
||||
opacity={1}
|
||||
id={`passing_${cellProps.row.original.id.toString()}`}
|
||||
data-html
|
||||
offset={8}
|
||||
positionStrategy="fixed"
|
||||
>
|
||||
{getTooltip(cellProps.row.original.next_update_ms)}
|
||||
</ReactTooltip>
|
||||
</>
|
||||
</ReactTooltip5>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
|
|
@ -259,52 +254,73 @@ const generateTableHeaders = (
|
|||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<div className="policy-has-not-run">
|
||||
<span
|
||||
className="text-cell text-muted has-not-run tooltip"
|
||||
data-tip
|
||||
data-for={`failing_${cellProps.row.original.id.toString()}`}
|
||||
data-tooltip-id={`passing_${cellProps.row.original.id.toString()}`}
|
||||
>
|
||||
---
|
||||
</span>
|
||||
<ReactTooltip
|
||||
place="bottom"
|
||||
effect="solid"
|
||||
backgroundColor={COLORS["tooltip-bg"]}
|
||||
id={`failing_${cellProps.row.original.id.toString()}`}
|
||||
data-html
|
||||
<ReactTooltip5
|
||||
className="policy-has-not-run-tooltip"
|
||||
disableStyleInjection
|
||||
place="top"
|
||||
opacity={1}
|
||||
id={`passing_${cellProps.row.original.id.toString()}`}
|
||||
offset={8}
|
||||
positionStrategy="fixed"
|
||||
>
|
||||
{getTooltip(cellProps.row.original.next_update_ms)}
|
||||
</ReactTooltip>
|
||||
</>
|
||||
</ReactTooltip5>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
sortType: "caseInsensitive",
|
||||
},
|
||||
];
|
||||
|
||||
if (tableType !== "inheritedPolicies") {
|
||||
if (!canAddOrDeletePolicy) {
|
||||
return tableHeaders;
|
||||
}
|
||||
|
||||
if (hasPermissionAndPoliciesToDelete) {
|
||||
tableHeaders.unshift({
|
||||
id: "selection",
|
||||
Header: (cellProps: IHeaderProps) => {
|
||||
const props = cellProps.getToggleAllRowsSelectedProps();
|
||||
const checkboxProps = {
|
||||
value: props.checked,
|
||||
indeterminate: props.indeterminate,
|
||||
onChange: () => cellProps.toggleAllRowsSelected(),
|
||||
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();
|
||||
},
|
||||
};
|
||||
|
||||
const checkboxProps = viewingTeamPolicies
|
||||
? teamCheckboxProps
|
||||
: regularCheckboxProps;
|
||||
return <Checkbox {...checkboxProps} />;
|
||||
},
|
||||
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 <Checkbox {...checkboxProps} />;
|
||||
},
|
||||
disableHidden: true,
|
||||
|
|
|
|||
|
|
@ -26,10 +26,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
.has-not-run {
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
.no-team-policy {
|
||||
border: 1px solid #e2e4ea;
|
||||
box-sizing: border-box;
|
||||
|
|
|
|||
|
|
@ -26,11 +26,7 @@ const PolicyErrorsTable = ({
|
|||
canAddOrDeletePolicy,
|
||||
}: IPolicyErrorsTableProps): JSX.Element => {
|
||||
return (
|
||||
<div
|
||||
className={`${baseClass} ${
|
||||
canAddOrDeletePolicy ? "" : "hide-selection-column"
|
||||
}`}
|
||||
>
|
||||
<div className={baseClass}>
|
||||
<TableContainer
|
||||
resultsTitle={resultsTitle || "policies"}
|
||||
columnConfigs={generateTableHeaders()}
|
||||
|
|
|
|||
|
|
@ -26,11 +26,7 @@ const PolicyResultsTable = ({
|
|||
canAddOrDeletePolicy,
|
||||
}: IPolicyResultsTableProps): JSX.Element => {
|
||||
return (
|
||||
<div
|
||||
className={`${baseClass} ${
|
||||
canAddOrDeletePolicy ? "" : "hide-selection-column"
|
||||
}`}
|
||||
>
|
||||
<div className={baseClass}>
|
||||
<TableContainer
|
||||
resultsTitle={resultsTitle || "policies"}
|
||||
columnConfigs={generateTableHeaders()}
|
||||
|
|
|
|||
|
|
@ -15,13 +15,13 @@ import { TableContext } from "context/table";
|
|||
import { NotificationContext } from "context/notification";
|
||||
import { getPerformanceImpactDescription } from "utilities/helpers";
|
||||
import { SupportedPlatform } from "interfaces/platform";
|
||||
import { API_ALL_TEAMS_ID } from "interfaces/team";
|
||||
import {
|
||||
IEnhancedQuery,
|
||||
IQueryKeyQueriesLoadAll,
|
||||
ISchedulableQuery,
|
||||
} from "interfaces/schedulable_query";
|
||||
import { DEFAULT_TARGETS_BY_TYPE } from "interfaces/target";
|
||||
import { API_ALL_TEAMS_ID } from "interfaces/team";
|
||||
import queriesAPI from "services/entities/queries";
|
||||
import PATHS from "router/paths";
|
||||
import { DEFAULT_QUERY } from "utilities/constants";
|
||||
|
|
@ -32,7 +32,6 @@ import TableDataError from "components/DataError";
|
|||
import MainContent from "components/MainContent";
|
||||
import TeamsDropdown from "components/TeamsDropdown";
|
||||
import useTeamIdParam from "hooks/useTeamIdParam";
|
||||
import RevealButton from "components/buttons/RevealButton";
|
||||
import QueriesTable from "./components/QueriesTable";
|
||||
import DeleteQueryModal from "./components/DeleteQueryModal";
|
||||
import ManageQueryAutomationsModal from "./components/ManageQueryAutomationsModal/ManageQueryAutomationsModal";
|
||||
|
|
@ -50,9 +49,6 @@ interface IManageQueriesPageProps {
|
|||
order_key?: string;
|
||||
order_direction?: "asc" | "desc";
|
||||
team_id?: string;
|
||||
inherited_order_key?: string;
|
||||
inherited_order_direction?: "asc" | "desc";
|
||||
inherited_page?: string;
|
||||
};
|
||||
search: string;
|
||||
};
|
||||
|
|
@ -64,7 +60,7 @@ const getPlatforms = (queryString: string): SupportedPlatform[] => {
|
|||
return platforms ?? [];
|
||||
};
|
||||
|
||||
const enhanceQuery = (q: ISchedulableQuery): IEnhancedQuery => {
|
||||
export const enhanceQuery = (q: ISchedulableQuery): IEnhancedQuery => {
|
||||
return {
|
||||
...q,
|
||||
performance: getPerformanceImpactDescription(
|
||||
|
|
@ -120,14 +116,16 @@ 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: curTeamEnhancedQueries,
|
||||
error: curTeamQueriesError,
|
||||
isFetching: isFetchingCurTeamQueries,
|
||||
refetch: refetchCurTeamQueries,
|
||||
data: enhancedQueries,
|
||||
error: queriesError,
|
||||
isFetching: isFetchingQueries,
|
||||
refetch: refetchQueries,
|
||||
} = useQuery<
|
||||
IEnhancedQuery[],
|
||||
Error,
|
||||
|
|
@ -137,46 +135,41 @@ const ManageQueriesPage = ({
|
|||
[{ scope: "queries", teamId: teamIdForApi }],
|
||||
({ queryKey: [{ teamId }] }) =>
|
||||
queriesAPI
|
||||
.loadAll(teamId)
|
||||
.loadAll(teamId, teamId !== API_ALL_TEAMS_ID)
|
||||
.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);
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
// 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,
|
||||
const onlyInheritedQueries = useMemo(() => {
|
||||
if (teamIdForApi === API_ALL_TEAMS_ID) {
|
||||
// global scope
|
||||
return false;
|
||||
}
|
||||
);
|
||||
return !enhancedQueries?.some((query) => query.team_id === teamIdForApi);
|
||||
}, [teamIdForApi, enhancedQueries]);
|
||||
|
||||
const automatedQueryIds = useMemo(() => {
|
||||
return curTeamEnhancedQueries
|
||||
? curTeamEnhancedQueries
|
||||
.filter((query) => query.automations_enabled)
|
||||
.map((query) => query.id)
|
||||
: [];
|
||||
}, [curTeamEnhancedQueries]);
|
||||
return queriesAvailableToAutomate
|
||||
.filter((query) => query.automations_enabled)
|
||||
.map((query) => query.id);
|
||||
}, [queriesAvailableToAutomate]);
|
||||
|
||||
useEffect(() => {
|
||||
const path = location.pathname + location.search;
|
||||
|
|
@ -204,11 +197,6 @@ const ManageQueriesPage = ({
|
|||
setSelectedQueryIds(selectedTableQueryIds);
|
||||
};
|
||||
|
||||
const refetchAllQueries = useCallback(() => {
|
||||
refetchCurTeamQueries();
|
||||
refetchGlobalQueries();
|
||||
}, [refetchCurTeamQueries, refetchGlobalQueries]);
|
||||
|
||||
const toggleManageAutomationsModal = useCallback(() => {
|
||||
setShowManageAutomationsModal(!showManageAutomationsModal);
|
||||
}, [showManageAutomationsModal, setShowManageAutomationsModal]);
|
||||
|
|
@ -237,7 +225,7 @@ const ManageQueriesPage = ({
|
|||
`Successfully deleted ${bulk ? "queries" : "query"}.`
|
||||
);
|
||||
setResetSelectedRows(true);
|
||||
refetchAllQueries();
|
||||
refetchQueries();
|
||||
} catch (errorResponse) {
|
||||
renderFlash(
|
||||
"error",
|
||||
|
|
@ -249,7 +237,7 @@ const ManageQueriesPage = ({
|
|||
toggleDeleteQueryModal();
|
||||
setIsUpdatingQueries(false);
|
||||
}
|
||||
}, [refetchAllQueries, selectedQueryIds, toggleDeleteQueryModal]);
|
||||
}, [refetchQueries, selectedQueryIds, toggleDeleteQueryModal]);
|
||||
|
||||
const renderHeader = () => {
|
||||
if (isPremiumTier) {
|
||||
|
|
@ -271,17 +259,18 @@ const ManageQueriesPage = ({
|
|||
return <h1>Queries</h1>;
|
||||
};
|
||||
|
||||
const renderCurrentScopeQueriesTable = () => {
|
||||
if (isFetchingCurTeamQueries) {
|
||||
const renderQueriesTable = () => {
|
||||
if (isFetchingQueries) {
|
||||
return <Spinner />;
|
||||
}
|
||||
if (curTeamQueriesError) {
|
||||
if (queriesError) {
|
||||
return <TableDataError />;
|
||||
}
|
||||
return (
|
||||
<QueriesTable
|
||||
queriesList={curTeamEnhancedQueries || []}
|
||||
isLoading={isFetchingCurTeamQueries}
|
||||
queriesList={enhancedQueries || []}
|
||||
onlyInheritedQueries={onlyInheritedQueries}
|
||||
isLoading={isFetchingQueries}
|
||||
onCreateQueryClick={onCreateQueryClick}
|
||||
onDeleteQueryClick={onDeleteQueryClick}
|
||||
isOnlyObserver={isOnlyObserver}
|
||||
|
|
@ -289,70 +278,11 @@ const ManageQueriesPage = ({
|
|||
isAnyTeamObserverPlus={isAnyTeamObserverPlus || false}
|
||||
router={router}
|
||||
queryParams={queryParams}
|
||||
currentTeamId={teamIdForApi}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const renderShowInheritedQueriesTableButton = () => {
|
||||
const inheritedQueryCount = globalEnhancedQueries?.length;
|
||||
return (
|
||||
<RevealButton
|
||||
isShowing={showInheritedQueries}
|
||||
className={baseClass}
|
||||
hideText={`Hide ${inheritedQueryCount} inherited quer${
|
||||
inheritedQueryCount === 1 ? "y" : "ies"
|
||||
}`}
|
||||
showText={`Show ${inheritedQueryCount} inherited quer${
|
||||
inheritedQueryCount === 1 ? "y" : "ies"
|
||||
}`}
|
||||
caretPosition="before"
|
||||
tooltipContent={
|
||||
<>
|
||||
Queries from the "All teams"
|
||||
<br />
|
||||
schedule run on this team's hosts.
|
||||
</>
|
||||
}
|
||||
onClick={() => {
|
||||
setShowInheritedQueries(!showInheritedQueries);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const renderInheritedQueriesTable = () => {
|
||||
if (isFetchingGlobalQueries) {
|
||||
return <Spinner />;
|
||||
}
|
||||
if (globalQueriesError) {
|
||||
return <TableDataError />;
|
||||
}
|
||||
return (
|
||||
<QueriesTable
|
||||
queriesList={globalEnhancedQueries || []}
|
||||
isLoading={isFetchingGlobalQueries}
|
||||
onCreateQueryClick={onCreateQueryClick}
|
||||
onDeleteQueryClick={onDeleteQueryClick}
|
||||
isOnlyObserver={isOnlyObserver}
|
||||
isObserverPlus={isObserverPlus}
|
||||
isAnyTeamObserverPlus={isAnyTeamObserverPlus || false}
|
||||
router={router}
|
||||
queryParams={queryParams}
|
||||
isInherited
|
||||
currentTeamId={currentTeamId}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const renderInheritedQueriesSection = () => {
|
||||
return (
|
||||
<>
|
||||
{renderShowInheritedQueriesTableButton()}
|
||||
{showInheritedQueries && renderInheritedQueriesTable()}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const onSaveQueryAutomations = useCallback(
|
||||
async (newAutomatedQueryIds: any) => {
|
||||
setIsUpdatingAutomations(true);
|
||||
|
|
@ -382,7 +312,7 @@ const ManageQueriesPage = ({
|
|||
try {
|
||||
await Promise.all(updateAutomatedQueries).then(() => {
|
||||
renderFlash("success", `Successfully updated query automations.`);
|
||||
refetchAllQueries();
|
||||
refetchQueries();
|
||||
});
|
||||
} catch (errorResponse) {
|
||||
renderFlash(
|
||||
|
|
@ -394,7 +324,7 @@ const ManageQueriesPage = ({
|
|||
setIsUpdatingAutomations(false);
|
||||
}
|
||||
},
|
||||
[refetchAllQueries, automatedQueryIds, toggleManageAutomationsModal]
|
||||
[refetchQueries, automatedQueryIds, toggleManageAutomationsModal]
|
||||
);
|
||||
|
||||
const renderModals = () => {
|
||||
|
|
@ -414,7 +344,7 @@ const ManageQueriesPage = ({
|
|||
onCancel={toggleManageAutomationsModal}
|
||||
isShowingPreviewDataModal={showPreviewDataModal}
|
||||
togglePreviewDataModal={togglePreviewDataModal}
|
||||
availableQueries={curTeamEnhancedQueries}
|
||||
availableQueries={queriesAvailableToAutomate}
|
||||
automatedQueryIds={automatedQueryIds}
|
||||
logDestination={config?.logging.result.plugin || ""}
|
||||
/>
|
||||
|
|
@ -435,29 +365,28 @@ const ManageQueriesPage = ({
|
|||
<div className={`${baseClass}__title`}>{renderHeader()}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`${baseClass}__action-button-container`}>
|
||||
{(isGlobalAdmin || isTeamAdmin) && (
|
||||
<Button
|
||||
onClick={onManageAutomationsClick}
|
||||
className={`${baseClass}__manage-automations button`}
|
||||
variant="inverse"
|
||||
>
|
||||
Manage automations
|
||||
</Button>
|
||||
)}
|
||||
{(!isOnlyObserver || isObserverPlus || isAnyTeamObserverPlus) &&
|
||||
!!curTeamEnhancedQueries?.length && (
|
||||
<>
|
||||
<Button
|
||||
variant="brand"
|
||||
className={`${baseClass}__create-button`}
|
||||
onClick={onCreateQueryClick}
|
||||
>
|
||||
{isObserverPlus ? "Live query" : "Add query"}
|
||||
</Button>
|
||||
</>
|
||||
{!!enhancedQueries?.length && (
|
||||
<div className={`${baseClass}__action-button-container`}>
|
||||
{(isGlobalAdmin || isTeamAdmin) && !onlyInheritedQueries && (
|
||||
<Button
|
||||
onClick={onManageAutomationsClick}
|
||||
className={`${baseClass}__manage-automations button`}
|
||||
variant="inverse"
|
||||
>
|
||||
Manage automations
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
{(!isOnlyObserver || isObserverPlus || isAnyTeamObserverPlus) && (
|
||||
<Button
|
||||
variant="brand"
|
||||
className={`${baseClass}__create-button`}
|
||||
onClick={onCreateQueryClick}
|
||||
>
|
||||
{isObserverPlus ? "Live query" : "Add query"}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className={`${baseClass}__description`}>
|
||||
<p>
|
||||
|
|
@ -466,11 +395,7 @@ const ManageQueriesPage = ({
|
|||
: "Gather data about all hosts."}
|
||||
</p>
|
||||
</div>
|
||||
{renderCurrentScopeQueriesTable()}
|
||||
{isAnyTeamSelected &&
|
||||
globalEnhancedQueries &&
|
||||
globalEnhancedQueries?.length > 0 &&
|
||||
renderInheritedQueriesSection()}
|
||||
{renderQueriesTable()}
|
||||
{renderModals()}
|
||||
</div>
|
||||
</MainContent>
|
||||
|
|
|
|||
|
|
@ -96,6 +96,8 @@
|
|||
}
|
||||
&__table {
|
||||
thead {
|
||||
// maintain height when select header is removed
|
||||
height: 52.3833px;
|
||||
.name__header {
|
||||
width: auto;
|
||||
}
|
||||
|
|
@ -131,15 +133,33 @@
|
|||
.query-name-cell {
|
||||
display: flex; // required for inline icon
|
||||
gap: $pad-xsmall;
|
||||
|
||||
.children-wrapper {
|
||||
text-decoration: none;
|
||||
align-items: center;
|
||||
&:hover {
|
||||
.query-name-text {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
.query-icon {
|
||||
.observer-can-run-query-icon {
|
||||
display: block;
|
||||
}
|
||||
|
||||
|
|
@ -147,9 +167,6 @@
|
|||
display: flex;
|
||||
gap: $pad-xsmall;
|
||||
}
|
||||
.observer-can-run-tooltip {
|
||||
font-weight: $regular;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: $break-md) {
|
||||
|
|
@ -184,6 +201,13 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
.empty-table {
|
||||
&__additional-info {
|
||||
* {
|
||||
font-size: $xx-small;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.reveal-button {
|
||||
.component__tooltip-wrapper__underline {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,367 @@
|
|||
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(<QueriesTable {...tableProps} />);
|
||||
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(
|
||||
<QueriesTable {...tableProps} />
|
||||
);
|
||||
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(
|
||||
<QueriesTable
|
||||
queriesList={testQueries}
|
||||
onlyInheritedQueries={false}
|
||||
isLoading={false}
|
||||
onDeleteQueryClick={jest.fn()}
|
||||
onCreateQueryClick={jest.fn()}
|
||||
isOnlyObserver={false}
|
||||
isObserverPlus={false}
|
||||
isAnyTeamObserverPlus={false}
|
||||
currentTeamId={1}
|
||||
/>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
waitFor(() => {
|
||||
user.hover(screen.getByTestId("query-icon"));
|
||||
});
|
||||
|
||||
expect(
|
||||
screen.getByText("Observers can run this query.")
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("Renders an inherited badge and tooltip for inherited query on a team's queries page", async () => {
|
||||
const render = createCustomRenderer({
|
||||
context: {
|
||||
app: {
|
||||
isGlobalAdmin: true,
|
||||
currentUser: createMockUser(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const testInheritedQuery = [createMockQuery()];
|
||||
|
||||
const testQueries = testInheritedQuery.map(enhanceQuery);
|
||||
|
||||
const { user } = render(
|
||||
<QueriesTable
|
||||
queriesList={testQueries}
|
||||
onlyInheritedQueries={false}
|
||||
isLoading={false}
|
||||
onDeleteQueryClick={jest.fn()}
|
||||
onCreateQueryClick={jest.fn()}
|
||||
isOnlyObserver={false}
|
||||
isObserverPlus={false}
|
||||
isAnyTeamObserverPlus={false}
|
||||
currentTeamId={1}
|
||||
/>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
waitFor(() => {
|
||||
user.hover(screen.getByText("Inherited"));
|
||||
});
|
||||
|
||||
expect(
|
||||
screen.getByText("This query runs on all hosts.")
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("Does not render an inherited badge and tooltip for global query on the All team's queries page", () => {
|
||||
const render = createCustomRenderer({
|
||||
context: {
|
||||
app: {
|
||||
isGlobalAdmin: true,
|
||||
currentUser: createMockUser(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const testGlobalQuery = [createMockQuery()];
|
||||
const testQueries = testGlobalQuery.map(enhanceQuery);
|
||||
|
||||
render(
|
||||
<QueriesTable
|
||||
queriesList={testQueries}
|
||||
onlyInheritedQueries={false}
|
||||
isLoading={false}
|
||||
onDeleteQueryClick={jest.fn()}
|
||||
onCreateQueryClick={jest.fn()}
|
||||
isOnlyObserver={false}
|
||||
isObserverPlus={false}
|
||||
isAnyTeamObserverPlus={false}
|
||||
currentTeamId={undefined}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.queryByText("Inherited")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("Renders the correct number of checkboxes for team queries and not inherited queries on a team's queries page and can check select all box", async () => {
|
||||
const render = createCustomRenderer({
|
||||
context: {
|
||||
app: {
|
||||
isGlobalAdmin: true,
|
||||
currentUser: createMockUser(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { container, user } = render(
|
||||
<QueriesTable
|
||||
queriesList={[...testTeamQueries, ...testGlobalQueries]}
|
||||
onlyInheritedQueries={false}
|
||||
isLoading={false}
|
||||
onDeleteQueryClick={jest.fn()}
|
||||
onCreateQueryClick={jest.fn()}
|
||||
isOnlyObserver={false}
|
||||
isObserverPlus={false}
|
||||
isAnyTeamObserverPlus={false}
|
||||
currentTeamId={1}
|
||||
/>
|
||||
);
|
||||
|
||||
const numberOfCheckboxes = container.querySelectorAll(
|
||||
"input[type='checkbox']"
|
||||
).length;
|
||||
|
||||
expect(numberOfCheckboxes).toBe(
|
||||
testTeamQueries.length + 1 // +1 for Select all checkbox
|
||||
);
|
||||
|
||||
const checkbox = container.querySelectorAll(
|
||||
"input[type='checkbox']"
|
||||
)[0] as HTMLInputElement;
|
||||
|
||||
await waitFor(() => {
|
||||
waitFor(() => {
|
||||
user.click(checkbox);
|
||||
});
|
||||
|
||||
expect(checkbox.checked).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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,13 +17,9 @@ import Dropdown from "components/forms/fields/Dropdown";
|
|||
import generateColumnConfigs from "./QueriesTableConfig";
|
||||
|
||||
const baseClass = "queries-table";
|
||||
|
||||
interface IQueryTableData extends IQuery {
|
||||
performance: string;
|
||||
platforms: string[];
|
||||
}
|
||||
interface IQueriesTableProps {
|
||||
queriesList: IQueryTableData[] | null;
|
||||
export interface IQueriesTableProps {
|
||||
queriesList: IEnhancedQuery[] | null;
|
||||
onlyInheritedQueries: boolean;
|
||||
isLoading: boolean;
|
||||
onDeleteQueryClick: (selectedTableQueryIds: number[]) => void;
|
||||
onCreateQueryClick: () => void;
|
||||
|
|
@ -38,11 +34,7 @@ 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;
|
||||
}
|
||||
|
||||
|
|
@ -86,6 +78,7 @@ const PLATFORM_FILTER_OPTIONS = [
|
|||
|
||||
const QueriesTable = ({
|
||||
queriesList,
|
||||
onlyInheritedQueries,
|
||||
isLoading,
|
||||
onDeleteQueryClick,
|
||||
onCreateQueryClick,
|
||||
|
|
@ -94,7 +87,6 @@ const QueriesTable = ({
|
|||
isAnyTeamObserverPlus,
|
||||
router,
|
||||
queryParams,
|
||||
isInherited = false,
|
||||
currentTeamId,
|
||||
}: IQueriesTableProps): JSX.Element | null => {
|
||||
const { currentUser } = useContext(AppContext);
|
||||
|
|
@ -112,28 +104,14 @@ 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 = isInherited ? initialInheritedPage : initialPage;
|
||||
const sortDirection = isInherited
|
||||
? initialInheritedSortDirection
|
||||
: initialSortDirection;
|
||||
const sortHeader = isInherited
|
||||
? initialInheritedSortHeader
|
||||
: initialSortHeader;
|
||||
const page = initialPage;
|
||||
const sortDirection = initialSortDirection;
|
||||
const sortHeader = initialSortHeader;
|
||||
|
||||
// TODO: Look into useDebounceCallback with dependencies
|
||||
const onQueryChange = useCallback(
|
||||
|
|
@ -148,37 +126,19 @@ const QueriesTable = ({
|
|||
// Rebuild queryParams to dispatch new browser location to react-router
|
||||
const newQueryParams: { [key: string]: string | number | undefined } = {};
|
||||
|
||||
// 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";
|
||||
}
|
||||
// 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";
|
||||
}
|
||||
|
||||
newQueryParams.team_id = queryParams?.team_id;
|
||||
|
|
@ -194,17 +154,11 @@ const QueriesTable = ({
|
|||
|
||||
const onClientSidePaginationChange = useCallback(
|
||||
(pageIndex: number) => {
|
||||
const newQueryParams = isInherited
|
||||
? {
|
||||
...queryParams,
|
||||
inherited_page: pageIndex, // update inherited page index
|
||||
query: searchQuery,
|
||||
}
|
||||
: {
|
||||
...queryParams,
|
||||
page: pageIndex, // update main table index
|
||||
query: searchQuery,
|
||||
};
|
||||
const newQueryParams = {
|
||||
...queryParams,
|
||||
page: pageIndex, // update main table index
|
||||
query: searchQuery,
|
||||
};
|
||||
|
||||
const locationPath = getNextLocationPath({
|
||||
pathPrefix: PATHS.MANAGE_QUERIES,
|
||||
|
|
@ -219,20 +173,18 @@ 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 queries match the current search criteria";
|
||||
emptyQueries.info =
|
||||
"Expecting to see queries? Try again in a few seconds as the system catches up.";
|
||||
emptyQueries.header = "No matching queries";
|
||||
emptyQueries.info = "No queries match the current filters.";
|
||||
} else if (!isOnlyObserver || isObserverPlus || isAnyTeamObserverPlus) {
|
||||
emptyQueries.additionalInfo = (
|
||||
<>
|
||||
Create a new query, or{" "}
|
||||
<CustomLink
|
||||
url="https://fleetdm.com/docs/using-fleet/standard-query-library"
|
||||
text="import Fleet’s standard query library"
|
||||
text="import Fleet's standard query library"
|
||||
newTab
|
||||
/>
|
||||
</>
|
||||
|
|
@ -243,7 +195,7 @@ const QueriesTable = ({
|
|||
className={`${baseClass}__create-button`}
|
||||
onClick={onCreateQueryClick}
|
||||
>
|
||||
Create new query
|
||||
Add query
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
|
@ -280,26 +232,28 @@ const QueriesTable = ({
|
|||
const columnConfigs = useMemo(
|
||||
() =>
|
||||
currentUser &&
|
||||
generateColumnConfigs({ currentUser, isInherited, currentTeamId }),
|
||||
[currentUser, isInherited, currentTeamId]
|
||||
generateColumnConfigs({
|
||||
currentUser,
|
||||
currentTeamId,
|
||||
omitSelectionColumn: onlyInheritedQueries,
|
||||
}),
|
||||
[currentUser, currentTeamId, onlyInheritedQueries]
|
||||
);
|
||||
|
||||
const searchable =
|
||||
!(queriesList?.length === 0 && searchQuery === "") && !isInherited;
|
||||
const searchable = !(queriesList?.length === 0 && searchQuery === "");
|
||||
|
||||
const trimmedSearchQuery = searchQuery.trim();
|
||||
return columnConfigs && !isLoading ? (
|
||||
<div className={`${baseClass}`}>
|
||||
<TableContainer
|
||||
disableCount={isInherited}
|
||||
resultsTitle="queries"
|
||||
columnConfigs={columnConfigs}
|
||||
data={queriesList}
|
||||
filters={{ name: isInherited ? "" : trimmedSearchQuery }}
|
||||
filters={{ name: trimmedSearchQuery }}
|
||||
isLoading={isLoading}
|
||||
defaultSortHeader={sortHeader || DEFAULT_SORT_HEADER}
|
||||
defaultSortDirection={sortDirection || DEFAULT_SORT_DIRECTION}
|
||||
defaultSearchQuery={isInherited ? "" : trimmedSearchQuery}
|
||||
defaultSearchQuery={trimmedSearchQuery}
|
||||
defaultPageIndex={page}
|
||||
pageSize={DEFAULT_PAGE_SIZE}
|
||||
inputPlaceHolder="Search by name"
|
||||
|
|
@ -317,9 +271,7 @@ const QueriesTable = ({
|
|||
isAllPagesSelected={false}
|
||||
searchable={searchable}
|
||||
searchQueryColumn="name"
|
||||
customControl={
|
||||
searchable && !isInherited ? renderPlatformDropdown : undefined
|
||||
}
|
||||
customControl={searchable ? renderPlatformDropdown : undefined}
|
||||
isClientSidePagination
|
||||
onClientSidePaginationChange={onClientSidePaginationChange}
|
||||
isClientSideFilter
|
||||
|
|
@ -330,7 +282,8 @@ const QueriesTable = ({
|
|||
variant: "text-icon",
|
||||
onActionButtonClick: onDeleteQueryClick,
|
||||
}}
|
||||
selectedDropdownFilter={!isInherited ? platform : undefined}
|
||||
selectedDropdownFilter={platform}
|
||||
show0Count
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -14,16 +14,19 @@ 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 { COLORS } from "styles/var/colors";
|
||||
import InheritedBadge from "components/InheritedBadge";
|
||||
import { Tooltip as ReactTooltip5 } from "react-tooltip-5";
|
||||
import QueryAutomationsStatusIndicator from "../QueryAutomationsStatusIndicator";
|
||||
|
||||
interface IQueryRow {
|
||||
|
|
@ -101,18 +104,19 @@ 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[] = [
|
||||
{
|
||||
|
|
@ -132,26 +136,56 @@ const generateTableHeaders = ({
|
|||
<>
|
||||
<div className="query-name-text">{cellProps.cell.value}</div>
|
||||
{!isOnlyObserver && cellProps.row.original.observer_can_run && (
|
||||
<>
|
||||
<div className="observer-can-run-badge">
|
||||
<span
|
||||
className="tooltip-base"
|
||||
data-tip
|
||||
data-for={`observer-can-run-tooltip-${cellProps.row.original.id}`}
|
||||
className="observer-can-run-icon"
|
||||
data-tooltip-id={`observer-can-run-tooltip-${cellProps.row.original.id}`}
|
||||
>
|
||||
<Icon className="query-icon" name="query" size="small" />
|
||||
<Icon
|
||||
className="observer-can-run-query-icon"
|
||||
name="query"
|
||||
size="small"
|
||||
color="core-fleet-blue"
|
||||
/>
|
||||
</span>
|
||||
<ReactTooltip
|
||||
<ReactTooltip5
|
||||
className="observer-can-run-tooltip"
|
||||
disableStyleInjection
|
||||
place="top"
|
||||
type="dark"
|
||||
effect="solid"
|
||||
opacity={1}
|
||||
id={`observer-can-run-tooltip-${cellProps.row.original.id}`}
|
||||
backgroundColor={COLORS["tooltip-bg"]}
|
||||
offset={8}
|
||||
positionStrategy="fixed"
|
||||
>
|
||||
Observers can run this query.
|
||||
</ReactTooltip>
|
||||
</>
|
||||
</ReactTooltip5>
|
||||
</div>
|
||||
|
||||
// <>
|
||||
// <span
|
||||
// className="tooltip-base"
|
||||
// data-tip
|
||||
// data-for={`observer-can-run-tooltip-${cellProps.row.original.id}`}
|
||||
// >
|
||||
// <Icon className="query-icon" name="query" size="small" />
|
||||
// </span>
|
||||
// <ReactTooltip
|
||||
// className="observer-can-run-tooltip"
|
||||
// place="top"
|
||||
// type="dark"
|
||||
// effect="solid"
|
||||
// id={`observer-can-run-tooltip-${cellProps.row.original.id}`}
|
||||
// backgroundColor={COLORS["tooltip-bg"]}
|
||||
// >
|
||||
// Observers can run this query.
|
||||
// </ReactTooltip>
|
||||
// </>
|
||||
)}
|
||||
{viewingTeamScope &&
|
||||
// inherited
|
||||
cellProps.row.original.team_id !== currentTeamId && (
|
||||
<InheritedBadge tooltipContent="This query runs on all hosts." />
|
||||
)}
|
||||
</>
|
||||
}
|
||||
path={PATHS.QUERY_DETAILS(
|
||||
|
|
@ -246,26 +280,27 @@ const generateTableHeaders = ({
|
|||
),
|
||||
},
|
||||
];
|
||||
if (!isOnlyObserver && !isInherited) {
|
||||
tableHeaders.splice(0, 0, {
|
||||
if (!isOnlyObserver && !omitSelectionColumn) {
|
||||
tableHeaders.unshift({
|
||||
id: "selection",
|
||||
Header: (cellProps: IHeaderProps): JSX.Element => {
|
||||
const {
|
||||
getToggleAllRowsSelectedProps,
|
||||
toggleAllRowsSelected,
|
||||
} = cellProps;
|
||||
const { checked, indeterminate } = getToggleAllRowsSelectedProps();
|
||||
// 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,
|
||||
});
|
||||
|
||||
const checkboxProps = {
|
||||
value: checked,
|
||||
indeterminate,
|
||||
onChange: () => {
|
||||
toggleAllRowsSelected();
|
||||
},
|
||||
};
|
||||
return <Checkbox {...checkboxProps} />;
|
||||
},
|
||||
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 = {
|
||||
|
|
|
|||
|
|
@ -35,9 +35,12 @@ export default {
|
|||
|
||||
return sendRequest("GET", path);
|
||||
},
|
||||
loadAll: (teamId?: number) => {
|
||||
loadAll: (teamId?: number, mergeInherited = false) => {
|
||||
const { QUERIES } = endpoints;
|
||||
const queryString = buildQueryStringFromParams({ team_id: teamId });
|
||||
const queryString = buildQueryStringFromParams({
|
||||
team_id: teamId,
|
||||
merge_inherited: mergeInherited || null,
|
||||
});
|
||||
const path = `${QUERIES}`;
|
||||
|
||||
return sendRequest(
|
||||
|
|
|
|||
|
|
@ -17,14 +17,11 @@ 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 {
|
||||
|
|
@ -32,13 +29,14 @@ export interface ITeamPoliciesQueryKey extends IPoliciesApiParams {
|
|||
}
|
||||
|
||||
export interface ITeamPoliciesCountQueryKey
|
||||
extends Pick<IPoliciesApiParams, "query" | "teamId"> {
|
||||
scope: "teamPoliciesCount";
|
||||
extends Pick<IPoliciesApiParams, "query" | "teamId" | "mergeInherited"> {
|
||||
scope: "teamPoliciesCountMergeInherited" | "teamPoliciesCount";
|
||||
}
|
||||
|
||||
interface IPoliciesCountApiParams {
|
||||
teamId: number;
|
||||
query?: string;
|
||||
mergeInherited?: boolean;
|
||||
}
|
||||
|
||||
const ORDER_KEY = "name";
|
||||
|
|
@ -118,7 +116,6 @@ 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<ILoadTeamPoliciesResponse> => {
|
||||
|
|
@ -137,10 +134,7 @@ export default {
|
|||
orderKey = ORDER_KEY,
|
||||
orderDirection: orderDir = ORDER_DIRECTION,
|
||||
query,
|
||||
inheritedPage,
|
||||
inheritedPerPage,
|
||||
inheritedOrderKey = ORDER_KEY,
|
||||
inheritedOrderDirection: inheritedOrderDir = ORDER_DIRECTION,
|
||||
mergeInherited,
|
||||
}: IPoliciesApiParams): Promise<ILoadTeamPoliciesResponse> => {
|
||||
const { TEAMS } = endpoints;
|
||||
|
||||
|
|
@ -150,10 +144,7 @@ export default {
|
|||
orderKey,
|
||||
orderDirection: orderDir,
|
||||
query,
|
||||
inheritedPage,
|
||||
inheritedPerPage,
|
||||
inheritedOrderKey,
|
||||
inheritedOrderDirection: inheritedOrderDir,
|
||||
mergeInherited,
|
||||
};
|
||||
|
||||
const snakeCaseParams = convertParamsToSnakeCase(queryParams);
|
||||
|
|
@ -168,14 +159,16 @@ export default {
|
|||
getCount: async ({
|
||||
query,
|
||||
teamId,
|
||||
mergeInherited = false,
|
||||
}: Pick<
|
||||
IPoliciesCountApiParams,
|
||||
"query" | "teamId"
|
||||
"query" | "teamId" | "mergeInherited"
|
||||
>): Promise<IPoliciesCountResponse> => {
|
||||
const { TEAM_POLICIES } = endpoints;
|
||||
const path = `${TEAM_POLICIES(teamId)}/count`;
|
||||
const queryParams = {
|
||||
query,
|
||||
mergeInherited,
|
||||
};
|
||||
const snakeCaseParams = convertParamsToSnakeCase(queryParams);
|
||||
const queryString = buildQueryStringFromParams(snakeCaseParams);
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@
|
|||
* 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,
|
||||
|
|
@ -10590,6 +10592,7 @@ 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:
|
||||
|
|
@ -10614,4 +10617,5 @@ export default {
|
|||
teamQuery1,
|
||||
teamQuery2,
|
||||
aiAutofillPolicy,
|
||||
teamPolicy1,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -341,3 +341,23 @@ $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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ import {
|
|||
PLATFORM_LABEL_DISPLAY_TYPES,
|
||||
isPlatformLabelNameFromAPI,
|
||||
} from "utilities/constants";
|
||||
import { IScheduledQueryStats } from "interfaces/scheduled_query_stats";
|
||||
import { ISchedulableQueryStats } from "interfaces/schedulable_query";
|
||||
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: IScheduledQueryStats
|
||||
scheduledQueryStats: ISchedulableQueryStats
|
||||
) => {
|
||||
if (
|
||||
!scheduledQueryStats.total_executions ||
|
||||
|
|
|
|||
|
|
@ -451,6 +451,25 @@ 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, '<deleted>') AS author_name,
|
||||
|
|
@ -608,6 +627,40 @@ 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, '<deleted>') 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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ 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},
|
||||
|
|
@ -709,6 +710,75 @@ 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)
|
||||
|
|
@ -2899,6 +2969,10 @@ 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)})
|
||||
|
|
@ -2913,6 +2987,10 @@ 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)})
|
||||
|
|
@ -2926,6 +3004,10 @@ 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) {
|
||||
|
|
|
|||
|
|
@ -4,11 +4,12 @@ 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 (
|
||||
|
|
@ -414,7 +415,6 @@ 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,10 +504,14 @@ func (ds *Datastore) ListQueries(ctx context.Context, opt fleet.ListQueryOptions
|
|||
args := []interface{}{false, fleet.AggregatedStatsTypeScheduledQuery}
|
||||
whereClauses := "WHERE saved = true"
|
||||
|
||||
if opt.TeamID != nil {
|
||||
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:
|
||||
args = append(args, *opt.TeamID)
|
||||
whereClauses += " AND team_id = ?"
|
||||
} else {
|
||||
default:
|
||||
whereClauses += " AND team_id IS NULL"
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -219,7 +219,6 @@ 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) {
|
||||
|
|
@ -765,6 +764,27 @@ 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) {
|
||||
|
|
|
|||
|
|
@ -1016,6 +1016,9 @@ 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 {
|
||||
|
|
|
|||
|
|
@ -612,6 +612,7 @@ 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)
|
||||
|
|
@ -661,6 +662,8 @@ 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)
|
||||
|
||||
|
|
|
|||
|
|
@ -270,7 +270,9 @@ 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`.
|
||||
ListQueries(ctx context.Context, opt ListOptions, teamID *uint, scheduled *bool) ([]*Query, error)
|
||||
// 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)
|
||||
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)
|
||||
|
|
@ -640,11 +642,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) (teamPolicies, inheritedPolicies []*Policy, err error)
|
||||
ListTeamPolicies(ctx context.Context, teamID uint, opts ListOptions, iopts ListOptions, mergeInherited bool) (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) (int, error)
|
||||
CountTeamPolicies(ctx context.Context, teamID uint, matchQuery string, mergeInherited bool) (int, error)
|
||||
|
||||
// /////////////////////////////////////////////////////////////////////////////
|
||||
// Geolocation
|
||||
|
|
|
|||
|
|
@ -447,6 +447,8 @@ 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)
|
||||
|
|
@ -495,6 +497,8 @@ 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)
|
||||
|
|
@ -1564,6 +1568,9 @@ type DataStore struct {
|
|||
CountPoliciesFunc CountPoliciesFunc
|
||||
CountPoliciesFuncInvoked bool
|
||||
|
||||
CountMergedTeamPoliciesFunc CountMergedTeamPoliciesFunc
|
||||
CountMergedTeamPoliciesFuncInvoked bool
|
||||
|
||||
UpdateHostPolicyCountsFunc UpdateHostPolicyCountsFunc
|
||||
UpdateHostPolicyCountsFuncInvoked bool
|
||||
|
||||
|
|
@ -1636,6 +1643,9 @@ type DataStore struct {
|
|||
ListTeamPoliciesFunc ListTeamPoliciesFunc
|
||||
ListTeamPoliciesFuncInvoked bool
|
||||
|
||||
ListMergedTeamPoliciesFunc ListMergedTeamPoliciesFunc
|
||||
ListMergedTeamPoliciesFuncInvoked bool
|
||||
|
||||
DeleteTeamPoliciesFunc DeleteTeamPoliciesFunc
|
||||
DeleteTeamPoliciesFuncInvoked bool
|
||||
|
||||
|
|
@ -3776,6 +3786,13 @@ 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
|
||||
|
|
@ -3944,6 +3961,13 @@ 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
|
||||
|
|
|
|||
|
|
@ -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)) // teamID == nil means global
|
||||
queries, err := svc.ListQueries(ctx, opts, nil, ptr.Bool(true), false) // teamID == nil means global
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -263,7 +263,7 @@ func (s *integrationTestSuite) TestQueryCreationLogsActivity() {
|
|||
}
|
||||
var createQueryResp createQueryResponse
|
||||
s.DoJSON("POST", "/api/latest/fleet/queries", ¶ms, http.StatusOK, &createQueryResp)
|
||||
defer cleanupQuery(s, createQueryResp.Query.ID)
|
||||
defer s.cleanupQuery(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 cleanupQuery(s, q.ID)
|
||||
defer s.cleanupQuery(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 cleanupQuery(s, existingQueryID)
|
||||
defer s.cleanupQuery(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 cleanupQuery(s *integrationTestSuite, queryID uint) {
|
||||
func (s *integrationTestSuite) cleanupQuery(queryID uint) {
|
||||
var delResp deleteQueryByIDResponse
|
||||
s.DoJSON("DELETE", fmt.Sprintf("/api/latest/fleet/queries/id/%d", queryID), nil, http.StatusOK, &delResp)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -844,6 +844,35 @@ 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)
|
||||
|
|
@ -853,6 +882,53 @@ 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()
|
||||
|
||||
|
|
@ -2840,7 +2916,8 @@ 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{}
|
||||
|
|
@ -2864,7 +2941,8 @@ 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{}
|
||||
|
|
@ -2875,7 +2953,8 @@ 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{}
|
||||
|
|
@ -8653,3 +8732,8 @@ 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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -58,7 +58,8 @@ 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"`
|
||||
TeamID uint `query:"team_id,optional"`
|
||||
MergeInherited bool `query:"merge_inherited,optional"`
|
||||
}
|
||||
|
||||
type listQueriesResponse struct {
|
||||
|
|
@ -76,7 +77,7 @@ func listQueriesEndpoint(ctx context.Context, request interface{}, svc fleet.Ser
|
|||
teamID = &req.TeamID
|
||||
}
|
||||
|
||||
queries, err := svc.ListQueries(ctx, req.ListOptions, teamID, nil)
|
||||
queries, err := svc.ListQueries(ctx, req.ListOptions, teamID, nil, req.MergeInherited)
|
||||
if err != nil {
|
||||
return listQueriesResponse{Err: err}, nil
|
||||
}
|
||||
|
|
@ -90,7 +91,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) ([]*fleet.Query, error) {
|
||||
func (svc *Service) ListQueries(ctx context.Context, opt fleet.ListOptions, teamID *uint, scheduled *bool, mergeInherited 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,
|
||||
|
|
@ -99,9 +100,10 @@ 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,
|
||||
ListOptions: opt,
|
||||
TeamID: teamID,
|
||||
IsScheduled: scheduled,
|
||||
MergeInherited: mergeInherited,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -733,7 +735,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)
|
||||
queries, err := svc.ListQueries(ctx, fleet.ListOptions{}, teamID, nil, false)
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "getting queries")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
_, err = svc.ListQueries(ctx, fleet.ListOptions{}, query.TeamID, nil, false)
|
||||
checkAuthErr(t, tt.shouldFailRead, err)
|
||||
|
||||
teamName := ""
|
||||
|
|
|
|||
|
|
@ -106,6 +106,7 @@ 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 {
|
||||
|
|
@ -126,14 +127,14 @@ func listTeamPoliciesEndpoint(ctx context.Context, request interface{}, svc flee
|
|||
OrderKey: req.InheritedOrderKey,
|
||||
}
|
||||
|
||||
tmPols, inheritedPols, err := svc.ListTeamPolicies(ctx, req.TeamID, req.Opts, inheritedListOptions)
|
||||
tmPols, inheritedPols, err := svc.ListTeamPolicies(ctx, req.TeamID, req.Opts, inheritedListOptions, req.MergeInherited)
|
||||
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) (teamPolicies, inheritedPolicies []*fleet.Policy, err error) {
|
||||
func (svc *Service) ListTeamPolicies(ctx context.Context, teamID uint, opts fleet.ListOptions, iopts fleet.ListOptions, mergeInherited bool) (teamPolicies, inheritedPolicies []*fleet.Policy, err error) {
|
||||
if err := svc.authz.Authorize(ctx, &fleet.Policy{
|
||||
PolicyData: fleet.PolicyData{
|
||||
TeamID: ptr.Uint(teamID),
|
||||
|
|
@ -146,6 +147,11 @@ 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)
|
||||
}
|
||||
|
||||
|
|
@ -154,8 +160,9 @@ 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"`
|
||||
ListOptions fleet.ListOptions `url:"list_options"`
|
||||
TeamID uint `url:"team_id"`
|
||||
MergeInherited bool `query:"merge_inherited,optional"`
|
||||
}
|
||||
|
||||
type countTeamPoliciesResponse struct {
|
||||
|
|
@ -167,14 +174,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)
|
||||
resp, err := svc.CountTeamPolicies(ctx, req.TeamID, req.ListOptions.MatchQuery, req.MergeInherited)
|
||||
if err != nil {
|
||||
return countTeamPoliciesResponse{Err: err}, nil
|
||||
}
|
||||
return countTeamPoliciesResponse{Count: resp}, nil
|
||||
}
|
||||
|
||||
func (svc *Service) CountTeamPolicies(ctx context.Context, teamID uint, matchQuery string) (int, error) {
|
||||
func (svc *Service) CountTeamPolicies(ctx context.Context, teamID uint, matchQuery string, mergeInherited bool) (int, error) {
|
||||
if err := svc.authz.Authorize(ctx, &fleet.Policy{
|
||||
PolicyData: fleet.PolicyData{
|
||||
TeamID: ptr.Uint(teamID),
|
||||
|
|
@ -187,6 +194,10 @@ 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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -149,7 +149,7 @@ func TestTeamPoliciesAuth(t *testing.T) {
|
|||
})
|
||||
checkAuthErr(t, tt.shouldFailWrite, err)
|
||||
|
||||
_, _, err = svc.ListTeamPolicies(ctx, 1, fleet.ListOptions{}, fleet.ListOptions{})
|
||||
_, _, err = svc.ListTeamPolicies(ctx, 1, fleet.ListOptions{}, fleet.ListOptions{}, false)
|
||||
checkAuthErr(t, tt.shouldFailRead, err)
|
||||
|
||||
_, err = svc.GetTeamPolicyByIDQueries(ctx, 1, 1)
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
queries, err := svc.ListQueries(ctx, opts, teamID_, ptr.Bool(true), false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue