diff --git a/changes/21855-paginate-queries b/changes/21855-paginate-queries new file mode 100644 index 000000000000..a54dfb43c8c7 --- /dev/null +++ b/changes/21855-paginate-queries @@ -0,0 +1,5 @@ +- Fleshed out server response from `queries` endpoint to include `count` and `meta` pagination information. +- Updated UI queries page to filter, sort, paginate, etc. via query params in call to server. +- Updated platform filtering on queries page to refer to targeted platforms instead of compatible + platforms +- Updated queries API to support above targeted platform filtering diff --git a/cmd/fleetctl/get_test.go b/cmd/fleetctl/get_test.go index 41b89ea6ba71..bba40091748b 100644 --- a/cmd/fleetctl/get_test.go +++ b/cmd/fleetctl/get_test.go @@ -1250,7 +1250,7 @@ func TestGetQueries(t *testing.T) { } return nil, ¬FoundError{} } - ds.ListQueriesFunc = func(ctx context.Context, opt fleet.ListQueryOptions) ([]*fleet.Query, error) { + ds.ListQueriesFunc = func(ctx context.Context, opt fleet.ListQueryOptions) ([]*fleet.Query, int, *fleet.PaginationMetadata, error) { if opt.TeamID == nil { //nolint:gocritic // ignore ifElseChain return []*fleet.Query{ { @@ -1282,7 +1282,7 @@ func TestGetQueries(t *testing.T) { Saved: true, // ListQueries always returns the saved ones. ObserverCanRun: true, }, - }, nil + }, 3, nil, nil } else if *opt.TeamID == 1 { return []*fleet.Query{ { @@ -1299,11 +1299,11 @@ func TestGetQueries(t *testing.T) { TeamID: ptr.Uint(1), ObserverCanRun: true, }, - }, nil + }, 1, nil, nil } else if *opt.TeamID == 2 { - return []*fleet.Query{}, nil + return []*fleet.Query{}, 0, nil, nil } - return nil, errors.New("invalid team ID") + return nil, 0, nil, errors.New("invalid team ID") } expectedGlobal := `+--------+-------------+-----------+-----------+--------------------------------+ @@ -1563,7 +1563,7 @@ func TestGetQueriesAsObserver(t *testing.T) { } } - ds.ListQueriesFunc = func(ctx context.Context, opt fleet.ListQueryOptions) ([]*fleet.Query, error) { + ds.ListQueriesFunc = func(ctx context.Context, opt fleet.ListQueryOptions) ([]*fleet.Query, int, *fleet.PaginationMetadata, error) { return []*fleet.Query{ { ID: 42, @@ -1586,7 +1586,7 @@ func TestGetQueriesAsObserver(t *testing.T) { Query: "select 3;", ObserverCanRun: false, }, - }, nil + }, 3, nil, nil } for _, tc := range []struct { @@ -1794,7 +1794,7 @@ spec: GlobalRole: nil, Teams: []fleet.UserTeam{{Role: fleet.RoleObserver}}, }) - ds.ListQueriesFunc = func(ctx context.Context, opt fleet.ListQueryOptions) ([]*fleet.Query, error) { + ds.ListQueriesFunc = func(ctx context.Context, opt fleet.ListQueryOptions) ([]*fleet.Query, int, *fleet.PaginationMetadata, error) { return []*fleet.Query{ { ID: 42, @@ -1810,12 +1810,12 @@ spec: Query: "select 2;", ObserverCanRun: false, }, - }, nil + }, 2, nil, nil } assert.Equal(t, "", runAppForTest(t, []string{"get", "queries"})) // No filtering is performed if all are observer_can_run. - ds.ListQueriesFunc = func(ctx context.Context, opt fleet.ListQueryOptions) ([]*fleet.Query, error) { + ds.ListQueriesFunc = func(ctx context.Context, opt fleet.ListQueryOptions) ([]*fleet.Query, int, *fleet.PaginationMetadata, error) { return []*fleet.Query{ { ID: 42, @@ -1831,7 +1831,7 @@ spec: Query: "select 2;", ObserverCanRun: true, }, - }, nil + }, 2, nil, nil } expected = `+--------+-------------+-----------+-----------+----------------------------+ | NAME | DESCRIPTION | QUERY | TEAM | SCHEDULE | diff --git a/cmd/fleetctl/gitops_test.go b/cmd/fleetctl/gitops_test.go index ad11066b2aa5..cfb97a6de5e3 100644 --- a/cmd/fleetctl/gitops_test.go +++ b/cmd/fleetctl/gitops_test.go @@ -69,7 +69,9 @@ func TestGitOpsBasicGlobalFree(t *testing.T) { return nil } ds.ListGlobalPoliciesFunc = func(ctx context.Context, opts fleet.ListOptions) ([]*fleet.Policy, error) { return nil, nil } - ds.ListQueriesFunc = func(ctx context.Context, opts fleet.ListQueryOptions) ([]*fleet.Query, error) { return nil, nil } + ds.ListQueriesFunc = func(ctx context.Context, opts fleet.ListQueryOptions) ([]*fleet.Query, int, *fleet.PaginationMetadata, error) { + return nil, 0, nil, nil + } // Mock appConfig savedAppConfig := &fleet.AppConfig{} @@ -224,7 +226,9 @@ func TestGitOpsBasicGlobalPremium(t *testing.T) { return nil } ds.ListGlobalPoliciesFunc = func(ctx context.Context, opts fleet.ListOptions) ([]*fleet.Policy, error) { return nil, nil } - ds.ListQueriesFunc = func(ctx context.Context, opts fleet.ListQueryOptions) ([]*fleet.Query, error) { return nil, nil } + ds.ListQueriesFunc = func(ctx context.Context, opts fleet.ListQueryOptions) ([]*fleet.Query, int, *fleet.PaginationMetadata, error) { + return nil, 0, nil, nil + } // Mock appConfig savedAppConfig := &fleet.AppConfig{} @@ -364,7 +368,9 @@ func TestGitOpsBasicTeam(t *testing.T) { ) (teamPolicies []*fleet.Policy, inheritedPolicies []*fleet.Policy, err error) { return nil, nil, nil } - ds.ListQueriesFunc = func(ctx context.Context, opts fleet.ListQueryOptions) ([]*fleet.Query, error) { return nil, nil } + ds.ListQueriesFunc = func(ctx context.Context, opts fleet.ListQueryOptions) ([]*fleet.Query, int, *fleet.PaginationMetadata, error) { + return nil, 0, nil, nil + } team := &fleet.Team{ ID: 1, CreatedAt: time.Now(), @@ -600,8 +606,8 @@ func TestGitOpsFullGlobal(t *testing.T) { query.ID = 1 query.Name = "Query to delete" queryDeleted := false - ds.ListQueriesFunc = func(ctx context.Context, opts fleet.ListQueryOptions) ([]*fleet.Query, error) { - return []*fleet.Query{&query}, nil + ds.ListQueriesFunc = func(ctx context.Context, opts fleet.ListQueryOptions) ([]*fleet.Query, int, *fleet.PaginationMetadata, error) { + return []*fleet.Query{&query}, 1, nil, nil } ds.DeleteQueriesFunc = func(ctx context.Context, ids []uint) (uint, error) { queryDeleted = true @@ -861,8 +867,8 @@ func TestGitOpsFullTeam(t *testing.T) { query.TeamID = ptr.Uint(teamID) query.Name = "Query to delete" queryDeleted := false - ds.ListQueriesFunc = func(ctx context.Context, opts fleet.ListQueryOptions) ([]*fleet.Query, error) { - return []*fleet.Query{&query}, nil + ds.ListQueriesFunc = func(ctx context.Context, opts fleet.ListQueryOptions) ([]*fleet.Query, int, *fleet.PaginationMetadata, error) { + return []*fleet.Query{&query}, 1, nil, nil } ds.DeleteQueriesFunc = func(ctx context.Context, ids []uint) (uint, error) { queryDeleted = true @@ -1154,7 +1160,9 @@ func TestGitOpsBasicGlobalAndTeam(t *testing.T) { } return nil, nil } - ds.ListQueriesFunc = func(ctx context.Context, opts fleet.ListQueryOptions) ([]*fleet.Query, error) { return nil, nil } + ds.ListQueriesFunc = func(ctx context.Context, opts fleet.ListQueryOptions) ([]*fleet.Query, int, *fleet.PaginationMetadata, error) { + return nil, 0, nil, nil + } ds.NewActivityFunc = func( ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time, ) error { @@ -1485,7 +1493,9 @@ func TestGitOpsBasicGlobalAndNoTeam(t *testing.T) { ds.ListTeamsFunc = func(ctx context.Context, filter fleet.TeamFilter, opt fleet.ListOptions) ([]*fleet.Team, error) { return nil, nil } - ds.ListQueriesFunc = func(ctx context.Context, opts fleet.ListQueryOptions) ([]*fleet.Query, error) { return nil, nil } + ds.ListQueriesFunc = func(ctx context.Context, opts fleet.ListQueryOptions) ([]*fleet.Query, int, *fleet.PaginationMetadata, error) { + return nil, 0, nil, nil + } ds.NewActivityFunc = func( ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time, ) error { @@ -2441,7 +2451,9 @@ func setupFullGitOpsPremiumServer(t *testing.T) (*mock.Store, **fleet.AppConfig, } return nil, nil } - ds.ListQueriesFunc = func(ctx context.Context, opts fleet.ListQueryOptions) ([]*fleet.Query, error) { return nil, nil } + ds.ListQueriesFunc = func(ctx context.Context, opts fleet.ListQueryOptions) ([]*fleet.Query, int, *fleet.PaginationMetadata, error) { + return nil, 0, nil, nil + } ds.NewActivityFunc = func( ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time, ) error { diff --git a/cmd/fleetctl/query_test.go b/cmd/fleetctl/query_test.go index 8e5441b58ad5..6553b41aafe8 100644 --- a/cmd/fleetctl/query_test.go +++ b/cmd/fleetctl/query_test.go @@ -60,11 +60,11 @@ func TestSavedLiveQuery(t *testing.T) { ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) { return &fleet.AppConfig{}, nil } - ds.ListQueriesFunc = func(ctx context.Context, opt fleet.ListQueryOptions) ([]*fleet.Query, error) { + ds.ListQueriesFunc = func(ctx context.Context, opt fleet.ListQueryOptions) ([]*fleet.Query, int, *fleet.PaginationMetadata, error) { if opt.MatchQuery == queryName { - return []*fleet.Query{&query}, nil + return []*fleet.Query{&query}, 1, nil, nil } - return []*fleet.Query{}, nil + return []*fleet.Query{}, 0, nil, nil } ds.NewDistributedQueryCampaignFunc = func(ctx context.Context, camp *fleet.DistributedQueryCampaign) (*fleet.DistributedQueryCampaign, error) { camp.ID = 321 diff --git a/cmd/fleetctl/session_test.go b/cmd/fleetctl/session_test.go index 62796bcc25b2..8d06769c0fdf 100644 --- a/cmd/fleetctl/session_test.go +++ b/cmd/fleetctl/session_test.go @@ -14,8 +14,8 @@ import ( func TestEarlySessionCheck(t *testing.T) { _, ds := runServerWithMockedDS(t) - ds.ListQueriesFunc = func(ctx context.Context, opt fleet.ListQueryOptions) ([]*fleet.Query, error) { - return nil, nil + ds.ListQueriesFunc = func(ctx context.Context, opt fleet.ListQueryOptions) ([]*fleet.Query, int, *fleet.PaginationMetadata, error) { + return nil, 0, nil, nil } ds.SessionByKeyFunc = func(ctx context.Context, key string) (*fleet.Session, error) { return nil, errors.New("invalid session") diff --git a/cmd/fleetctl/upgrade_packs_test.go b/cmd/fleetctl/upgrade_packs_test.go index c30ae6e2c3b6..79c96c989178 100644 --- a/cmd/fleetctl/upgrade_packs_test.go +++ b/cmd/fleetctl/upgrade_packs_test.go @@ -247,8 +247,8 @@ func TestFleetctlUpgradePacks_EmptyPacks(t *testing.T) { return fleet.TargetMetrics{}, nil } - ds.ListQueriesFunc = func(ctx context.Context, opt fleet.ListQueryOptions) ([]*fleet.Query, error) { - return nil, nil + ds.ListQueriesFunc = func(ctx context.Context, opt fleet.ListQueryOptions) ([]*fleet.Query, int, *fleet.PaginationMetadata, error) { + return nil, 0, nil, nil } tempDir := t.TempDir() @@ -314,12 +314,12 @@ func TestFleetctlUpgradePacks_NonEmpty(t *testing.T) { return fleet.TargetMetrics{}, nil } - ds.ListQueriesFunc = func(ctx context.Context, opt fleet.ListQueryOptions) ([]*fleet.Query, error) { + ds.ListQueriesFunc = func(ctx context.Context, opt fleet.ListQueryOptions) ([]*fleet.Query, int, *fleet.PaginationMetadata, error) { return []*fleet.Query{ {Name: "q1", Query: "select 1"}, {Name: "q2", Query: "select 2"}, {Name: "q3", Query: "select 3"}, - }, nil + }, 3, nil, nil } const expected = ` diff --git a/frontend/components/PlatformCompatibility/PlatformCompatibility.tsx b/frontend/components/PlatformCompatibility/PlatformCompatibility.tsx index 700726d37ecb..59d93e71c81d 100644 --- a/frontend/components/PlatformCompatibility/PlatformCompatibility.tsx +++ b/frontend/components/PlatformCompatibility/PlatformCompatibility.tsx @@ -88,8 +88,13 @@ const PlatformCompatibility = ({ tipContent={ <> Estimated compatibility based on the
- tables used in the query. Querying
- iPhones & iPads is not supported. + tables used in the query. +
+
+ Only live queries are supported on ChromeOS. +
+
+ Querying iPhones & iPads is not supported. } > diff --git a/frontend/hooks/usePlatformCompatibility.tsx b/frontend/hooks/usePlatformCompatibility.tsx index ebb4ec1ee4e1..88a8d1b2fa07 100644 --- a/frontend/hooks/usePlatformCompatibility.tsx +++ b/frontend/hooks/usePlatformCompatibility.tsx @@ -1,7 +1,7 @@ import React, { useCallback, useState } from "react"; import { useDebouncedCallback } from "use-debounce"; -import { QueryablePlatform, SUPPORTED_PLATFORMS } from "interfaces/platform"; +import { QueryablePlatform, QUERYABLE_PLATFORMS } from "interfaces/platform"; import { checkPlatformCompatibility } from "utilities/sql_tools"; import PlatformCompatibility from "components/PlatformCompatibility"; @@ -37,7 +37,7 @@ const usePlatformCompatibility = (): IPlatformCompatibility => { ); const getCompatiblePlatforms = useCallback( - () => SUPPORTED_PLATFORMS.filter((p) => compatiblePlatforms?.includes(p)), + () => QUERYABLE_PLATFORMS.filter((p) => compatiblePlatforms?.includes(p)), [compatiblePlatforms] ); diff --git a/frontend/hooks/usePlatformSelector.tsx b/frontend/hooks/usePlatformSelector.tsx index b1fb384dd98f..4d7ad70f3a73 100644 --- a/frontend/hooks/usePlatformSelector.tsx +++ b/frontend/hooks/usePlatformSelector.tsx @@ -3,7 +3,7 @@ import { forEach } from "lodash"; import { SelectedPlatformString, - SUPPORTED_PLATFORMS, + QUERYABLE_PLATFORMS, QueryablePlatform, } from "interfaces/platform"; @@ -48,7 +48,7 @@ const usePlatformSelector = ( }; const getSelectedPlatforms = useCallback(() => { - return SUPPORTED_PLATFORMS.filter((p) => checksByPlatform[p]); + return QUERYABLE_PLATFORMS.filter((p) => checksByPlatform[p]); }, [checksByPlatform]); const isAnyPlatformSelected = Object.values(checksByPlatform).includes(true); diff --git a/frontend/interfaces/platform.ts b/frontend/interfaces/platform.ts index 1f33a1639fdf..d96fbbef3934 100644 --- a/frontend/interfaces/platform.ts +++ b/frontend/interfaces/platform.ts @@ -22,13 +22,18 @@ export type QueryableDisplayPlatform = Exclude< >; export type QueryablePlatform = Exclude; -export const SUPPORTED_PLATFORMS: QueryablePlatform[] = [ +export const QUERYABLE_PLATFORMS: QueryablePlatform[] = [ "darwin", "windows", "linux", "chrome", ]; +export const isQueryablePlatform = ( + platform: string | undefined +): platform is QueryablePlatform => + QUERYABLE_PLATFORMS.includes(platform as QueryablePlatform); + // TODO - add "iOS" and "iPadOS" once we support them export const VULN_SUPPORTED_PLATFORMS: Platform[] = ["darwin", "windows"]; diff --git a/frontend/interfaces/query.ts b/frontend/interfaces/query.ts index 9485f2da9a5c..e3d0a41d43af 100644 --- a/frontend/interfaces/query.ts +++ b/frontend/interfaces/query.ts @@ -14,10 +14,6 @@ export interface IStoredQueryResponse { query: ISchedulableQuery; } -export interface IFleetQueriesResponse { - queries: ISchedulableQuery[]; -} - export interface IQuery { created_at: string; updated_at: string; diff --git a/frontend/interfaces/schedulable_query.ts b/frontend/interfaces/schedulable_query.ts index fb0fcac74bb5..ff3698ca57e9 100644 --- a/frontend/interfaces/schedulable_query.ts +++ b/frontend/interfaces/schedulable_query.ts @@ -3,7 +3,11 @@ import PropTypes from "prop-types"; import { IFormField } from "./form_field"; import { IPack } from "./pack"; -import { SelectedPlatformString, QueryablePlatform } from "./platform"; +import { + SelectedPlatformString, + QueryablePlatform, + SelectedPlatform, +} from "./platform"; // Query itself export interface ISchedulableQuery { @@ -15,7 +19,7 @@ export interface ISchedulableQuery { query: string; team_id: number | null; interval: number; - platform: SelectedPlatformString; // Might more accurately be called `platforms_to_query` – comma-sepparated string of platforms to query, default all platforms if ommitted + platform: SelectedPlatformString; // Might more accurately be called `platforms_to_query` or `targeted_platforms` – comma-sepparated string of platforms to query, default all platforms if ommitted min_osquery_version: string; automations_enabled: boolean; logging: QueryLoggingOption; @@ -32,7 +36,7 @@ export interface ISchedulableQuery { export interface IEnhancedQuery extends ISchedulableQuery { performance: string; - platforms: QueryablePlatform[]; + targetedPlatforms: QueryablePlatform[]; } export interface ISchedulableQueryStats { user_time_p50?: number | null; @@ -67,7 +71,14 @@ export interface IListQueriesResponse { export interface IQueryKeyQueriesLoadAll { scope: "queries"; - teamId: number | undefined; + teamId?: number; + page?: number; + perPage?: number; + query?: string; + orderDirection?: "asc" | "desc"; + orderKey?: string; + mergeInherited?: boolean; + targetedPlatform?: SelectedPlatform; } // Create a new query /** POST /api/v1/fleet/queries */ diff --git a/frontend/pages/DashboardPage/DashboardPage.tsx b/frontend/pages/DashboardPage/DashboardPage.tsx index 5c72c33fc990..056acc965c17 100644 --- a/frontend/pages/DashboardPage/DashboardPage.tsx +++ b/frontend/pages/DashboardPage/DashboardPage.tsx @@ -460,7 +460,7 @@ const DashboardPage = ({ router, location }: IDashboardProps): JSX.Element => { setShowAddHostsModal(!showAddHostsModal); }; - // NOTE: this is called once on the initial rendering. The initial render of + // This is called once on the initial rendering. The initial render of // the TableContainer child component will call this handler. const onSoftwareQueryChange = async ({ pageIndex: newPageIndex, diff --git a/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx b/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx index 8e46f71971b7..5aa5100dcb6b 100644 --- a/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx +++ b/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx @@ -214,19 +214,24 @@ const HostDetailsPage = ({ >("past"); const [activityPage, setActivityPage] = useState(0); + // Optimization TODO: move this call into the SelectQuery modal, since queries are only used if that modal is opened const { data: fleetQueries, error: fleetQueriesError } = useQuery< IListQueriesResponse, Error, ISchedulableQuery[], IQueryKeyQueriesLoadAll[] - >([{ scope: "queries", teamId: undefined }], () => queryAPI.loadAll(), { - enabled: !!hostIdFromURL, - refetchOnMount: false, - refetchOnReconnect: false, - refetchOnWindowFocus: false, - retry: false, - select: (data: IListQueriesResponse) => data.queries, - }); + >( + [{ scope: "queries", teamId: undefined }], + ({ queryKey }) => queryAPI.loadAll(queryKey[0]), + { + enabled: !!hostIdFromURL, + refetchOnMount: false, + refetchOnReconnect: false, + refetchOnWindowFocus: false, + retry: false, + select: (data: IListQueriesResponse) => data.queries, + } + ); const { data: teams } = useQuery( "teams", diff --git a/frontend/pages/hosts/details/cards/Software/HostSoftware.tsx b/frontend/pages/hosts/details/cards/Software/HostSoftware.tsx index 8eaac7e65278..7c5290779606 100644 --- a/frontend/pages/hosts/details/cards/Software/HostSoftware.tsx +++ b/frontend/pages/hosts/details/cards/Software/HostSoftware.tsx @@ -250,14 +250,14 @@ const HostSoftware = ({ return isMyDevicePage ? generateDeviceSoftwareTableConfig() : generateHostSoftwareTableConfig({ - router, - softwareIdActionPending, userHasSWWritePermission, hostScriptsEnabled, - onSelectAction, - teamId: hostTeamId, hostCanWriteSoftware, hostMDMEnrolled, + softwareIdActionPending, + router, + teamId: hostTeamId, + onSelectAction, }); }, [ isMyDevicePage, diff --git a/frontend/pages/packs/EditPackPage/EditPackPage.tsx b/frontend/pages/packs/EditPackPage/EditPackPage.tsx index 3359e74af3f8..47718c9009b8 100644 --- a/frontend/pages/packs/EditPackPage/EditPackPage.tsx +++ b/frontend/pages/packs/EditPackPage/EditPackPage.tsx @@ -2,20 +2,25 @@ import React, { useState, useCallback, useContext } from "react"; import { useQuery } from "react-query"; import { InjectedRouter, Params } from "react-router/lib/Router"; +import { AppContext } from "context/app"; +import { NotificationContext } from "context/notification"; + import { IPack, IStoredPackResponse } from "interfaces/pack"; -import { IQuery, IFleetQueriesResponse } from "interfaces/query"; +import { IQuery } from "interfaces/query"; import { IPackQueryFormData, IScheduledQuery, IStoredScheduledQueriesResponse, } from "interfaces/scheduled_query"; import { ITarget, ITargetsAPIResponse } from "interfaces/target"; -import { AppContext } from "context/app"; -import { NotificationContext } from "context/notification"; - +import { + IQueryKeyQueriesLoadAll, + ISchedulableQuery, +} from "interfaces/schedulable_query"; import { getErrorReason } from "interfaces/errors"; + import packsAPI from "services/entities/packs"; -import queriesAPI from "services/entities/queries"; +import queriesAPI, { IQueriesResponse } from "services/entities/queries"; import scheduledQueriesAPI from "services/entities/scheduled_queries"; import PATHS from "router/paths"; @@ -50,13 +55,18 @@ const EditPacksPage = ({ const packId: number = parseInt(paramsPackId, 10); - const { data: fleetQueries } = useQuery< - IFleetQueriesResponse, + const { data: queries } = useQuery< + IQueriesResponse, Error, - IQuery[] - >(["fleet queries"], () => queriesAPI.loadAll(), { - select: (data: IFleetQueriesResponse) => data.queries, - }); + ISchedulableQuery[], + IQueryKeyQueriesLoadAll[] + >( + [{ scope: "queries", teamId: undefined }], + ({ queryKey }) => queriesAPI.loadAll(queryKey[0]), + { + select: (data) => data.queries, + } + ); const { data: storedPack } = useQuery( ["stored pack"], @@ -244,17 +254,17 @@ const EditPacksPage = ({ isUpdatingPack={isUpdatingPack} /> )} - {showPackQueryEditorModal && fleetQueries && ( + {showPackQueryEditorModal && queries && ( )} - {showRemovePackQueryModal && fleetQueries && ( + {showRemovePackQueryModal && queries && ( (); + const [ + tableQueryDataForApi, + setTableQueryDataForApi, + ] = useState(); const [sortHeader, setSortHeader] = useState(initialSortHeader); const [sortDirection, setSortDirection] = useState< "asc" | "desc" | undefined @@ -225,7 +228,7 @@ const ManagePolicyPage = ({ [ { scope: "globalPolicies", - page: tableQueryData?.pageIndex, + page: tableQueryDataForApi?.pageIndex, perPage: DEFAULT_PAGE_SIZE, query: searchQuery, orderDirection: sortDirection, @@ -281,7 +284,7 @@ const ManagePolicyPage = ({ [ { scope: "teamPolicies", - page: tableQueryData?.pageIndex, + page: tableQueryDataForApi?.pageIndex, perPage: DEFAULT_PAGE_SIZE, query: searchQuery, orderDirection: sortDirection, @@ -390,7 +393,7 @@ const ManagePolicyPage = ({ // NOTE: used to reset page number to 0 when modifying filters const handleResetPageIndex = () => { - setTableQueryData( + setTableQueryDataForApi( (prevState) => ({ ...prevState, @@ -412,11 +415,11 @@ const ManagePolicyPage = ({ // TODO: Look into useDebounceCallback with dependencies const onQueryChange = useCallback( async (newTableQuery: ITableQueryData) => { - if (!isRouteOk || isEqual(newTableQuery, tableQueryData)) { + if (!isRouteOk || isEqual(newTableQuery, tableQueryDataForApi)) { return; } - setTableQueryData({ ...newTableQuery }); + setTableQueryDataForApi({ ...newTableQuery }); const { pageIndex: newPageIndex, @@ -451,11 +454,11 @@ const ManagePolicyPage = ({ queryParams: { ...queryParams, ...newQueryParams }, }); - router?.replace(locationPath); + router?.push(locationPath); }, [ isRouteOk, - tableQueryData, + tableQueryDataForApi, sortDirection, sortHeader, searchQuery, diff --git a/frontend/pages/policies/ManagePoliciesPage/components/PoliciesTable/PoliciesTable.tsx b/frontend/pages/policies/ManagePoliciesPage/components/PoliciesTable/PoliciesTable.tsx index 808aadf80c6b..6db439a07392 100644 --- a/frontend/pages/policies/ManagePoliciesPage/components/PoliciesTable/PoliciesTable.tsx +++ b/frontend/pages/policies/ManagePoliciesPage/components/PoliciesTable/PoliciesTable.tsx @@ -55,12 +55,6 @@ const PoliciesTable = ({ }: IPoliciesTableProps): JSX.Element => { const { config } = useContext(AppContext); - const onTableQueryChange = (newTableQuery: ITableQueryData) => { - onQueryChange({ - ...newTableQuery, - }); - }; - const emptyState = () => { const emptyPolicies: IEmptyTableProps = { graphicName: "empty-policies", @@ -135,7 +129,7 @@ const PoliciesTable = ({ }) } renderCount={renderPoliciesCount} - onQueryChange={onTableQueryChange} + onQueryChange={onQueryChange} inputPlaceHolder="Search by name" searchable={searchable} resetPageIndex={resetPageIndex} diff --git a/frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx b/frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx index 34550c98728e..2c988659814f 100644 --- a/frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx +++ b/frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx @@ -14,7 +14,11 @@ import { QueryContext } from "context/query"; import { TableContext } from "context/table"; import { NotificationContext } from "context/notification"; import { getPerformanceImpactDescription } from "utilities/helpers"; -import { QueryablePlatform, SelectedPlatform } from "interfaces/platform"; +import { + isQueryablePlatform, + QueryablePlatform, + SelectedPlatform, +} from "interfaces/platform"; import { IEnhancedQuery, IQueryKeyQueriesLoadAll, @@ -22,10 +26,9 @@ import { } 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 queriesAPI, { IQueriesResponse } from "services/entities/queries"; import PATHS from "router/paths"; import { DEFAULT_QUERY } from "utilities/constants"; -import { checkPlatformCompatibility } from "utilities/sql_tools"; import Button from "components/buttons/Button"; import Spinner from "components/Spinner"; import TableDataError from "components/DataError"; @@ -37,13 +40,16 @@ import DeleteQueryModal from "./components/DeleteQueryModal"; import ManageQueryAutomationsModal from "./components/ManageQueryAutomationsModal/ManageQueryAutomationsModal"; import PreviewDataModal from "./components/PreviewDataModal/PreviewDataModal"; +const DEFAULT_PAGE_SIZE = 20; + const baseClass = "manage-queries-page"; interface IManageQueriesPageProps { router: InjectedRouter; // v3 location: { pathname: string; query: { - platform?: SelectedPlatform; + // note that the URL value "darwin" will correspond to the request query param "macos" + platform?: SelectedPlatform; // which targeted platform to filter queries by page?: string; query?: string; order_key?: string; @@ -54,10 +60,9 @@ interface IManageQueriesPageProps { }; } -const getPlatforms = (queryString: string): QueryablePlatform[] => { - const { platforms } = checkPlatformCompatibility(queryString); - - return platforms ?? []; +const getTargetedPlatforms = (platformString: string): QueryablePlatform[] => { + const platforms = platformString.split(","); + return platforms.filter(isQueryablePlatform); }; export const enhanceQuery = (q: ISchedulableQuery): IEnhancedQuery => { @@ -66,7 +71,7 @@ export const enhanceQuery = (q: ISchedulableQuery): IEnhancedQuery => { performance: getPerformanceImpactDescription( pick(q.stats, ["user_time_p50", "system_time_p50", "total_executions"]) ), - platforms: getPlatforms(q.query), + targetedPlatforms: getTargetedPlatforms(q.platform), }; }; @@ -74,7 +79,6 @@ const ManageQueriesPage = ({ router, location, }: IManageQueriesPageProps): JSX.Element => { - const queryParams = location.query; const { isGlobalAdmin, isGlobalMaintainer, @@ -118,46 +122,54 @@ const ManageQueriesPage = ({ const [showPreviewDataModal, setShowPreviewDataModal] = useState(false); const [isUpdatingQueries, setIsUpdatingQueries] = useState(false); const [isUpdatingAutomations, setIsUpdatingAutomations] = useState(false); - const [queriesAvailableToAutomate, setQueriesAvailableToAutomate] = useState< - IEnhancedQuery[] | [] - >([]); + + const curPageFromURL = location.query.page + ? parseInt(location.query.page, 10) + : 0; const { - data: enhancedQueries, + data: queriesResponse, error: queriesError, isFetching: isFetchingQueries, + isLoading: isLoadingQueries, refetch: refetchQueries, } = useQuery< - IEnhancedQuery[], + IQueriesResponse, Error, - IEnhancedQuery[], + IQueriesResponse, IQueryKeyQueriesLoadAll[] >( - [{ scope: "queries", teamId: teamIdForApi }], - ({ queryKey: [{ teamId }] }) => - queriesAPI - .loadAll(teamId, teamId !== API_ALL_TEAMS_ID) - .then(({ queries }) => queries.map(enhanceQuery)), + [ + { + scope: "queries", + teamId: teamIdForApi, + page: curPageFromURL, + perPage: DEFAULT_PAGE_SIZE, + // a search match query, not a Fleet Query + query: location.query.query, + orderDirection: location.query.order_direction, + orderKey: location.query.order_key, + mergeInherited: teamIdForApi !== API_ALL_TEAMS_ID, + targetedPlatform: location.query.platform, + }, + ], + ({ queryKey }) => queriesAPI.loadAll(queryKey[0]), { refetchOnWindowFocus: false, enabled: isRouteOk, staleTime: 5000, - onSuccess: (data) => { - if (data) { - const enhancedAllQueries = data.map(enhanceQuery); - - const allQueriesAvailableToAutomate = teamIdForApi - ? enhancedAllQueries.filter( - (query: IEnhancedQuery) => query.team_id === currentTeamId - ) - : enhancedAllQueries; - - setQueriesAvailableToAutomate(allQueriesAvailableToAutomate); - } - }, } ); + const enhancedQueries = queriesResponse?.queries.map(enhanceQuery); + + const queriesAvailableToAutomate = + (teamIdForApi + ? enhancedQueries?.filter( + (query: IEnhancedQuery) => query.team_id === currentTeamId + ) + : enhancedQueries) ?? []; + const onlyInheritedQueries = useMemo(() => { if (teamIdForApi === API_ALL_TEAMS_ID) { // global scope @@ -166,11 +178,9 @@ const ManageQueriesPage = ({ return !enhancedQueries?.some((query) => query.team_id === teamIdForApi); }, [teamIdForApi, enhancedQueries]); - const automatedQueryIds = useMemo(() => { - return queriesAvailableToAutomate - .filter((query) => query.automations_enabled) - .map((query) => query.id); - }, [queriesAvailableToAutomate]); + const automatedQueryIds = queriesAvailableToAutomate + .filter((query) => query.automations_enabled) + .map((query) => query.id); useEffect(() => { const path = location.pathname + location.search; @@ -263,7 +273,7 @@ const ManageQueriesPage = ({ }; const renderQueriesTable = () => { - if (isFetchingQueries) { + if (isLoadingQueries) { return ; } if (queriesError) { @@ -271,7 +281,9 @@ const ManageQueriesPage = ({ } return ( ); @@ -327,7 +340,12 @@ const ManageQueriesPage = ({ setIsUpdatingAutomations(false); } }, - [refetchQueries, automatedQueryIds, toggleManageAutomationsModal] + [ + automatedQueryIds, + renderFlash, + refetchQueries, + toggleManageAutomationsModal, + ] ); const renderModals = () => { @@ -367,6 +385,13 @@ const ManageQueriesPage = ({ isTeamMaintainer || isObserverPlus; // isObserverPlus checks global and selected team + const hideQueryActions = + // there are no filters and no returned queries, indicating there are no global/team queries at all + !(!!location.query.query || !!location.query.platform) && + !queriesResponse?.count && + // the user has permission + (!isOnlyObserver || isObserverPlus || isAnyTeamObserverPlus); + return (
@@ -376,7 +401,8 @@ const ManageQueriesPage = ({
{renderHeader()}
- {!!enhancedQueries?.length && ( + + {!hideQueryActions && (
{(isGlobalAdmin || isTeamAdmin) && !onlyInheritedQueries && (