From d71944ad15a4257e0d732a36625640159c572dff Mon Sep 17 00:00:00 2001 From: Jacob Shandling Date: Mon, 21 Oct 2024 12:27:43 -0700 Subject: [PATCH 01/16] Define `IQueriesResponse`; scaffold new `per_page` param --- .../ManageQueriesPage/ManageQueriesPage.tsx | 50 +++++++++---------- 1 file changed, 23 insertions(+), 27 deletions(-) diff --git a/frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx b/frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx index 34550c98728e..1880dff54093 100644 --- a/frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx +++ b/frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx @@ -22,7 +22,7 @@ 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"; @@ -37,6 +37,8 @@ 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 @@ -118,9 +120,6 @@ const ManageQueriesPage = ({ const [showPreviewDataModal, setShowPreviewDataModal] = useState(false); const [isUpdatingQueries, setIsUpdatingQueries] = useState(false); const [isUpdatingAutomations, setIsUpdatingAutomations] = useState(false); - const [queriesAvailableToAutomate, setQueriesAvailableToAutomate] = useState< - IEnhancedQuery[] | [] - >([]); const { data: enhancedQueries, @@ -128,35 +127,27 @@ const ManageQueriesPage = ({ isFetching: isFetchingQueries, refetch: refetchQueries, } = useQuery< - IEnhancedQuery[], + IQueriesResponse, Error, IEnhancedQuery[], IQueryKeyQueriesLoadAll[] >( [{ scope: "queries", teamId: teamIdForApi }], ({ queryKey: [{ teamId }] }) => - queriesAPI - .loadAll(teamId, teamId !== API_ALL_TEAMS_ID) - .then(({ queries }) => queries.map(enhanceQuery)), + queriesAPI.loadAll(teamId, teamId !== API_ALL_TEAMS_ID), { 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); - } - }, + select: ({ queries }) => queries.map(enhanceQuery), } ); + const queriesAvailableToAutomate = + (teamIdForApi + ? enhancedQueries?.filter( + (query: IEnhancedQuery) => query.team_id === currentTeamId + ) + : enhancedQueries) ?? []; const onlyInheritedQueries = useMemo(() => { if (teamIdForApi === API_ALL_TEAMS_ID) { @@ -166,11 +157,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; @@ -282,6 +271,8 @@ const ManageQueriesPage = ({ router={router} queryParams={queryParams} currentTeamId={teamIdForApi} + // on PoliciesTable, this is passed down and set as TableContainer.defaultPageIndex - TBD if necessary? + // page={page} /> ); }; @@ -327,7 +318,12 @@ const ManageQueriesPage = ({ setIsUpdatingAutomations(false); } }, - [refetchQueries, automatedQueryIds, toggleManageAutomationsModal] + [ + automatedQueryIds, + renderFlash, + refetchQueries, + toggleManageAutomationsModal, + ] ); const renderModals = () => { From 027451a80fd17341d0d970f8e3ca0a4ac81c67e0 Mon Sep 17 00:00:00 2001 From: Jacob Shandling Date: Mon, 21 Oct 2024 12:29:02 -0700 Subject: [PATCH 02/16] Streamline Queries page logic; update type; scaffolding --- frontend/services/entities/queries.ts | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/frontend/services/entities/queries.ts b/frontend/services/entities/queries.ts index 4ff7afc1b1a0..8fd3f07a3593 100644 --- a/frontend/services/entities/queries.ts +++ b/frontend/services/entities/queries.ts @@ -6,9 +6,19 @@ import { ISelectedTargetsForApi } from "interfaces/target"; import { ICreateQueryRequestBody, IModifyQueryRequestBody, + ISchedulableQuery, } from "interfaces/schedulable_query"; import { buildQueryStringFromParams } from "utilities/url"; +export interface IQueriesResponse { + queries: ISchedulableQuery[]; + count: number; + meta: { + has_next_results: boolean; + has_previous_results: boolean; + }; +} + export default { create: (createQueryRequestBody: ICreateQueryRequestBody) => { const { QUERIES } = endpoints; @@ -35,11 +45,16 @@ export default { return sendRequest("GET", path); }, - loadAll: (teamId?: number, mergeInherited = false) => { + // loadAll: (teamId?: number, perPage?: number, mergeInherited = false) => { + loadAll: ( + teamId?: number, + mergeInherited = false + ): Promise => { const { QUERIES } = endpoints; const queryString = buildQueryStringFromParams({ team_id: teamId, merge_inherited: mergeInherited || null, + // per_page: perPage || null, }); const path = `${QUERIES}`; From d53d655dd10d179b0bfd34e894f60896bf2fc604 Mon Sep 17 00:00:00 2001 From: Jacob Shandling Date: Mon, 21 Oct 2024 15:36:04 -0700 Subject: [PATCH 03/16] Include count and pagination metadata in list queries response --- server/datastore/mysql/queries.go | 45 ++++++++++++++++++++++--------- server/fleet/datastore.go | 5 ++-- server/fleet/service.go | 3 ++- server/mock/datastore_mock.go | 4 +-- server/service/global_schedule.go | 3 ++- server/service/queries.go | 24 ++++++++++++----- server/service/queries_test.go | 6 ++--- server/service/team_schedule.go | 3 ++- server/service/testing_client.go | 3 ++- 9 files changed, 66 insertions(+), 30 deletions(-) diff --git a/server/datastore/mysql/queries.go b/server/datastore/mysql/queries.go index c52e33eacf3f..758562283a7e 100644 --- a/server/datastore/mysql/queries.go +++ b/server/datastore/mysql/queries.go @@ -468,9 +468,10 @@ func (ds *Datastore) Query(ctx context.Context, id uint) (*fleet.Query, error) { } // ListQueries returns a list of queries with sort order and results limit -// determined by passed in fleet.ListOptions -func (ds *Datastore) ListQueries(ctx context.Context, opt fleet.ListQueryOptions) ([]*fleet.Query, error) { - sql := ` +// determined by passed in fleet.ListOptions, count of total queries returned without limits, and +// pagination metadata +func (ds *Datastore) ListQueries(ctx context.Context, opt fleet.ListQueryOptions) ([]*fleet.Query, int, *fleet.PaginationMetadata, error) { + getQueriesStmt := ` SELECT q.id, q.team_id, @@ -488,7 +489,6 @@ func (ds *Datastore) ListQueries(ctx context.Context, opt fleet.ListQueryOptions q.discard_data, q.created_at, q.updated_at, - q.discard_data, COALESCE(u.name, '') AS author_name, COALESCE(u.email, '') AS author_email, JSON_EXTRACT(json_value, '$.user_time_p50') as user_time_p50, @@ -528,19 +528,40 @@ func (ds *Datastore) ListQueries(ctx context.Context, opt fleet.ListQueryOptions args = append(args, opt.MatchQuery) } - sql += whereClauses - sql, args = appendListOptionsWithCursorToSQL(sql, args, &opt.ListOptions) + getQueriesStmt += whereClauses - results := []*fleet.Query{} - if err := sqlx.SelectContext(ctx, ds.reader(ctx), &results, sql, args...); err != nil { - return nil, ctxerr.Wrap(ctx, err, "listing queries") + // build the count statement before adding pagination constraints + getQueriesCountStmt := fmt.Sprintf("SELECT COUNT(DISTINCT id) FROM (%s) AS s", getQueriesStmt) + + getQueriesStmt, args = appendListOptionsWithCursorToSQL(getQueriesStmt, args, &opt.ListOptions) + + dbReader := ds.reader(ctx) + queries := []*fleet.Query{} + if err := sqlx.SelectContext(ctx, dbReader, &queries, getQueriesStmt, args...); err != nil { + return nil, 0, nil, ctxerr.Wrap(ctx, err, "listing queries") } - if err := ds.loadPacksForQueries(ctx, results); err != nil { - return nil, ctxerr.Wrap(ctx, err, "loading packs for queries") + // perform a second query to grab the count + var count int + if err := sqlx.GetContext(ctx, dbReader, &count, getQueriesCountStmt, args...); err != nil { + return nil, 0, nil, ctxerr.Wrap(ctx, err, "get queries count") } - return results, nil + if err := ds.loadPacksForQueries(ctx, queries); err != nil { + return nil, 0, nil, ctxerr.Wrap(ctx, err, "loading packs for queries") + } + + var meta *fleet.PaginationMetadata + if opt.ListOptions.IncludeMetadata { + meta = &fleet.PaginationMetadata{HasPreviousResults: opt.ListOptions.Page > 0} + if len(queries) > int(opt.ListOptions.PerPage) { + meta.HasNextResults = true + queries = queries[:len(queries)-1] + } + } + + + return queries, count, meta, nil } // loadPacksForQueries loads the user packs (aka 2017 packs) associated with the provided queries. diff --git a/server/fleet/datastore.go b/server/fleet/datastore.go index 8da56903196d..bc36a9625f08 100644 --- a/server/fleet/datastore.go +++ b/server/fleet/datastore.go @@ -86,9 +86,10 @@ type Datastore interface { DeleteQueries(ctx context.Context, ids []uint) (uint, error) // Query returns the query associated with the provided ID. Associated packs should also be loaded. Query(ctx context.Context, id uint) (*Query, error) - // ListQueries returns a list of queries with the provided sorting and paging options. Associated packs should also + // ListQueries returns a list of queries filtered with the provided sorting and pagination + // options, a count of total queries on all pages, and pagination metadata. Associated packs should also // be loaded. - ListQueries(ctx context.Context, opt ListQueryOptions) ([]*Query, error) + ListQueries(ctx context.Context, opt ListQueryOptions) ([]*Query, int, *PaginationMetadata, error) // ListScheduledQueriesForAgents returns a list of scheduled queries (without stats) for the // given teamID. If teamID is nil, then all scheduled queries for the 'global' team are returned. ListScheduledQueriesForAgents(ctx context.Context, teamID *uint, queryReportsDisabled bool) ([]*Query, error) diff --git a/server/fleet/service.go b/server/fleet/service.go index 029f22cbdc77..8e0711924c0d 100644 --- a/server/fleet/service.go +++ b/server/fleet/service.go @@ -262,6 +262,7 @@ type Service interface { // ApplyQuerySpecs applies a list of queries (creating or updating them as necessary) ApplyQuerySpecs(ctx context.Context, specs []*QuerySpec) error // GetQuerySpecs gets the YAML file representing all the stored queries. + // TODO - return count and meta from this endpoint? GetQuerySpecs(ctx context.Context, teamID *uint) ([]*QuerySpec, error) // GetQuerySpec gets the spec for the query with the given name on a team. // A nil or 0 teamID means the query is looked for in the global domain. @@ -273,7 +274,7 @@ type Service interface { // and only non-scheduled queries will be returned if `*scheduled == false`. // If mergeInherited is true and a teamID is provided, then queries from the global team will be // included in the results. - ListQueries(ctx context.Context, opt ListOptions, teamID *uint, scheduled *bool, mergeInherited bool) ([]*Query, error) + ListQueries(ctx context.Context, opt ListOptions, teamID *uint, scheduled *bool, mergeInherited bool) ([]*Query, int, *PaginationMetadata, 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. // Returns a boolean indicating whether the report is clipped. diff --git a/server/mock/datastore_mock.go b/server/mock/datastore_mock.go index d43aa82f2c62..e38f690dc123 100644 --- a/server/mock/datastore_mock.go +++ b/server/mock/datastore_mock.go @@ -71,7 +71,7 @@ type DeleteQueriesFunc func(ctx context.Context, ids []uint) (uint, error) type QueryFunc func(ctx context.Context, id uint) (*fleet.Query, error) -type ListQueriesFunc func(ctx context.Context, opt fleet.ListQueryOptions) ([]*fleet.Query, error) +type ListQueriesFunc func(ctx context.Context, opt fleet.ListQueryOptions) ([]*fleet.Query, int, *fleet.PaginationMetadata, error) type ListScheduledQueriesForAgentsFunc func(ctx context.Context, teamID *uint, queryReportsDisabled bool) ([]*fleet.Query, error) @@ -2920,7 +2920,7 @@ func (s *DataStore) Query(ctx context.Context, id uint) (*fleet.Query, error) { return s.QueryFunc(ctx, id) } -func (s *DataStore) ListQueries(ctx context.Context, opt fleet.ListQueryOptions) ([]*fleet.Query, error) { +func (s *DataStore) ListQueries(ctx context.Context, opt fleet.ListQueryOptions) ([]*fleet.Query, int, *fleet.PaginationMetadata, error) { s.mu.Lock() s.ListQueriesFuncInvoked = true s.mu.Unlock() diff --git a/server/service/global_schedule.go b/server/service/global_schedule.go index c75d86048650..964520eea0b4 100644 --- a/server/service/global_schedule.go +++ b/server/service/global_schedule.go @@ -37,7 +37,8 @@ func getGlobalScheduleEndpoint(ctx context.Context, request interface{}, svc fle } func (svc *Service) GetGlobalScheduledQueries(ctx context.Context, opts fleet.ListOptions) ([]*fleet.ScheduledQuery, error) { - queries, err := svc.ListQueries(ctx, opts, nil, ptr.Bool(true), false) // teamID == nil means global + // TODO - count and meta? + queries, _, _, err := svc.ListQueries(ctx, opts, nil, ptr.Bool(true), false) // teamID == nil means global if err != nil { return nil, err } diff --git a/server/service/queries.go b/server/service/queries.go index 07b9c43a69b1..eab520901bcf 100644 --- a/server/service/queries.go +++ b/server/service/queries.go @@ -64,6 +64,8 @@ type listQueriesRequest struct { type listQueriesResponse struct { Queries []fleet.Query `json:"queries"` + Count int `json:"count"` + Meta *fleet.PaginationMetadata `json:"meta"` Err error `json:"error,omitempty"` } @@ -77,7 +79,7 @@ func listQueriesEndpoint(ctx context.Context, request interface{}, svc fleet.Ser teamID = &req.TeamID } - queries, err := svc.ListQueries(ctx, req.ListOptions, teamID, nil, req.MergeInherited) + queries, count, meta, err := svc.ListQueries(ctx, req.ListOptions, teamID, nil, req.MergeInherited) if err != nil { return listQueriesResponse{Err: err}, nil } @@ -86,30 +88,37 @@ func listQueriesEndpoint(ctx context.Context, request interface{}, svc fleet.Ser for _, query := range queries { respQueries = append(respQueries, *query) } + + return listQueriesResponse{ Queries: respQueries, + Count: count, + Meta: meta, }, nil } -func (svc *Service) ListQueries(ctx context.Context, opt fleet.ListOptions, teamID *uint, scheduled *bool, mergeInherited bool) ([]*fleet.Query, error) { +func (svc *Service) ListQueries(ctx context.Context, opt fleet.ListOptions, teamID *uint, scheduled *bool, mergeInherited bool) ([]*fleet.Query, int, *fleet.PaginationMetadata, error) { // Check the user is allowed to list queries on the given team. if err := svc.authz.Authorize(ctx, &fleet.Query{ TeamID: teamID, }, fleet.ActionRead); err != nil { - return nil, err + return nil, 0, nil, err } - queries, err := svc.ds.ListQueries(ctx, fleet.ListQueryOptions{ + // always include metadata for queries + opt.IncludeMetadata = true + + queries, count, meta, err := svc.ds.ListQueries(ctx, fleet.ListQueryOptions{ ListOptions: opt, TeamID: teamID, IsScheduled: scheduled, MergeInherited: mergeInherited, }) if err != nil { - return nil, err + return nil, 0, nil, err } - return queries, nil + return queries, count, meta, nil } //////////////////////////////////////////////////////////////////////////////// @@ -745,7 +754,8 @@ func getQuerySpecsEndpoint(ctx context.Context, request interface{}, svc fleet.S } func (svc *Service) GetQuerySpecs(ctx context.Context, teamID *uint) ([]*fleet.QuerySpec, error) { - queries, err := svc.ListQueries(ctx, fleet.ListOptions{}, teamID, nil, false) + // TODO - return count and meta from this endpoint? + queries, _, _, err := svc.ListQueries(ctx, fleet.ListOptions{}, teamID, nil, false) if err != nil { return nil, ctxerr.Wrap(ctx, err, "getting queries") } diff --git a/server/service/queries_test.go b/server/service/queries_test.go index 95ae244d7aa3..1eb272de4215 100644 --- a/server/service/queries_test.go +++ b/server/service/queries_test.go @@ -400,8 +400,8 @@ func TestQueryAuth(t *testing.T) { ds.DeleteQueriesFunc = func(ctx context.Context, ids []uint) (uint, error) { return 0, 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.ApplyQueriesFunc = func(ctx context.Context, authID uint, queries []*fleet.Query, queriesToDiscardResults map[uint]struct{}) error { return nil @@ -647,7 +647,7 @@ func TestQueryAuth(t *testing.T) { _, err = svc.QueryReportIsClipped(ctx, tt.qid, fleet.DefaultMaxQueryReportRows) checkAuthErr(t, tt.shouldFailRead, err) - _, err = svc.ListQueries(ctx, fleet.ListOptions{}, query.TeamID, nil, false) + _, _, _, err = svc.ListQueries(ctx, fleet.ListOptions{}, query.TeamID, nil, false) checkAuthErr(t, tt.shouldFailRead, err) teamName := "" diff --git a/server/service/team_schedule.go b/server/service/team_schedule.go index da31740a770f..a10db07839ea 100644 --- a/server/service/team_schedule.go +++ b/server/service/team_schedule.go @@ -47,7 +47,8 @@ func (svc Service) GetTeamScheduledQueries(ctx context.Context, teamID uint, opt if teamID != 0 { teamID_ = &teamID } - queries, err := svc.ListQueries(ctx, opts, teamID_, ptr.Bool(true), false) + // TODO - count and meta? + queries, _, _, err := svc.ListQueries(ctx, opts, teamID_, ptr.Bool(true), false) if err != nil { return nil, err } diff --git a/server/service/testing_client.go b/server/service/testing_client.go index 2250f4cf3331..6227e60e069d 100644 --- a/server/service/testing_client.go +++ b/server/service/testing_client.go @@ -127,7 +127,8 @@ func (ts *withServer) commonTearDownTest(t *testing.T) { } } - queries, err := ts.ds.ListQueries(ctx, fleet.ListQueryOptions{}) + // TODO - count, meta? + queries, _, _, err := ts.ds.ListQueries(ctx, fleet.ListQueryOptions{}) require.NoError(t, err) queryIDs := make([]uint, 0, len(queries)) for _, query := range queries { From 618884a7c6fa992c6263f6890d315bf7745086e9 Mon Sep 17 00:00:00 2001 From: Jacob Shandling Date: Tue, 22 Oct 2024 12:26:13 -0700 Subject: [PATCH 04/16] remove comments after clarifying scope --- server/fleet/service.go | 1 - server/service/global_schedule.go | 1 - server/service/queries.go | 1 - server/service/team_schedule.go | 1 - server/service/testing_client.go | 1 - 5 files changed, 5 deletions(-) diff --git a/server/fleet/service.go b/server/fleet/service.go index 8e0711924c0d..2558f7075220 100644 --- a/server/fleet/service.go +++ b/server/fleet/service.go @@ -262,7 +262,6 @@ type Service interface { // ApplyQuerySpecs applies a list of queries (creating or updating them as necessary) ApplyQuerySpecs(ctx context.Context, specs []*QuerySpec) error // GetQuerySpecs gets the YAML file representing all the stored queries. - // TODO - return count and meta from this endpoint? GetQuerySpecs(ctx context.Context, teamID *uint) ([]*QuerySpec, error) // GetQuerySpec gets the spec for the query with the given name on a team. // A nil or 0 teamID means the query is looked for in the global domain. diff --git a/server/service/global_schedule.go b/server/service/global_schedule.go index 964520eea0b4..cf05873b94e9 100644 --- a/server/service/global_schedule.go +++ b/server/service/global_schedule.go @@ -37,7 +37,6 @@ func getGlobalScheduleEndpoint(ctx context.Context, request interface{}, svc fle } func (svc *Service) GetGlobalScheduledQueries(ctx context.Context, opts fleet.ListOptions) ([]*fleet.ScheduledQuery, error) { - // TODO - count and meta? queries, _, _, err := svc.ListQueries(ctx, opts, nil, ptr.Bool(true), false) // teamID == nil means global if err != nil { return nil, err diff --git a/server/service/queries.go b/server/service/queries.go index eab520901bcf..b9da5df0db99 100644 --- a/server/service/queries.go +++ b/server/service/queries.go @@ -754,7 +754,6 @@ func getQuerySpecsEndpoint(ctx context.Context, request interface{}, svc fleet.S } func (svc *Service) GetQuerySpecs(ctx context.Context, teamID *uint) ([]*fleet.QuerySpec, error) { - // TODO - return count and meta from this endpoint? queries, _, _, err := svc.ListQueries(ctx, fleet.ListOptions{}, teamID, nil, false) if err != nil { return nil, ctxerr.Wrap(ctx, err, "getting queries") diff --git a/server/service/team_schedule.go b/server/service/team_schedule.go index a10db07839ea..b430749caaeb 100644 --- a/server/service/team_schedule.go +++ b/server/service/team_schedule.go @@ -47,7 +47,6 @@ func (svc Service) GetTeamScheduledQueries(ctx context.Context, teamID uint, opt if teamID != 0 { teamID_ = &teamID } - // TODO - count and meta? queries, _, _, err := svc.ListQueries(ctx, opts, teamID_, ptr.Bool(true), false) if err != nil { return nil, err diff --git a/server/service/testing_client.go b/server/service/testing_client.go index 6227e60e069d..759feea0a261 100644 --- a/server/service/testing_client.go +++ b/server/service/testing_client.go @@ -127,7 +127,6 @@ func (ts *withServer) commonTearDownTest(t *testing.T) { } } - // TODO - count, meta? queries, _, _, err := ts.ds.ListQueries(ctx, fleet.ListQueryOptions{}) require.NoError(t, err) queryIDs := make([]uint, 0, len(queries)) From 4b527c077a5e76be395d3d8556ee76d207d8c6e1 Mon Sep 17 00:00:00 2001 From: Jacob Shandling Date: Wed, 23 Oct 2024 12:15:27 -0700 Subject: [PATCH 05/16] Convert the manage queries page to use server-side pagination --- frontend/interfaces/query.ts | 4 - frontend/interfaces/schedulable_query.ts | 8 +- .../pages/DashboardPage/DashboardPage.tsx | 2 +- .../HostDetailsPage/HostDetailsPage.tsx | 22 ++- .../pages/packs/EditPackPage/EditPackPage.tsx | 39 ++-- .../ManagePoliciesPage/ManagePoliciesPage.tsx | 20 +- .../PoliciesTable/PoliciesTable.tsx | 13 +- .../ManageQueriesPage/ManageQueriesPage.tsx | 41 ++++- .../QueriesTable/QueriesTable.tests.tsx | 19 +- .../components/QueriesTable/QueriesTable.tsx | 171 +++++++++--------- frontend/services/entities/global_policies.ts | 20 +- frontend/services/entities/queries.ts | 49 +++-- server/datastore/mysql/queries.go | 1 - server/service/queries.go | 13 +- 14 files changed, 251 insertions(+), 171 deletions(-) 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 23d2a9cd27d0..44c15b23aaeb 100644 --- a/frontend/interfaces/schedulable_query.ts +++ b/frontend/interfaces/schedulable_query.ts @@ -67,7 +67,13 @@ 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; } // Create a new query /** POST /api/v1/fleet/queries */ diff --git a/frontend/pages/DashboardPage/DashboardPage.tsx b/frontend/pages/DashboardPage/DashboardPage.tsx index 1816ab4ceec6..b3026df04151 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 0c56d7c6ca4d..da50879a6d46 100644 --- a/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx +++ b/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx @@ -213,19 +213,25 @@ const HostDetailsPage = ({ >("past"); const [activityPage, setActivityPage] = useState(0); + // 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, - }); + // TODO - paginate this call and below UI that uses this response? + >( + [{ 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/packs/EditPackPage/EditPackPage.tsx b/frontend/pages/packs/EditPackPage/EditPackPage.tsx index 3359e74af3f8..01c578791929 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,19 @@ 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 }], + // TODO - paginate? + ({ queryKey }) => queriesAPI.loadAll(queryKey[0]), + { + select: (data) => data.queries, + } + ); const { data: storedPack } = useQuery( ["stored pack"], @@ -244,17 +255,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 +227,7 @@ const ManagePolicyPage = ({ [ { scope: "globalPolicies", - page: tableQueryData?.pageIndex, + page: tableQueryDataForApi?.pageIndex, perPage: DEFAULT_PAGE_SIZE, query: searchQuery, orderDirection: sortDirection, @@ -281,7 +283,7 @@ const ManagePolicyPage = ({ [ { scope: "teamPolicies", - page: tableQueryData?.pageIndex, + page: tableQueryDataForApi?.pageIndex, perPage: DEFAULT_PAGE_SIZE, query: searchQuery, orderDirection: sortDirection, @@ -389,7 +391,7 @@ const ManagePolicyPage = ({ // NOTE: used to reset page number to 0 when modifying filters const handleResetPageIndex = () => { - setTableQueryData( + setTableQueryDataForApi( (prevState) => ({ ...prevState, @@ -411,11 +413,13 @@ 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 }); + // sets state before any of the below logic factored in. This would seem to have potential + // discrepancies for page, which is potentially modified to be "0" - maybe improve this logic + setTableQueryDataForApi({ ...newTableQuery }); const { pageIndex: newPageIndex, @@ -454,7 +458,7 @@ const ManagePolicyPage = ({ }, [ 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..29bd047bcef6 100644 --- a/frontend/pages/policies/ManagePoliciesPage/components/PoliciesTable/PoliciesTable.tsx +++ b/frontend/pages/policies/ManagePoliciesPage/components/PoliciesTable/PoliciesTable.tsx @@ -55,11 +55,11 @@ const PoliciesTable = ({ }: IPoliciesTableProps): JSX.Element => { const { config } = useContext(AppContext); - const onTableQueryChange = (newTableQuery: ITableQueryData) => { - onQueryChange({ - ...newTableQuery, - }); - }; + // const onTableQueryChange = (newTableQuery: ITableQueryData) => { + // onQueryChange({ + // ...newTableQuery, + // }); + // }; const emptyState = () => { const emptyPolicies: IEmptyTableProps = { @@ -135,7 +135,8 @@ const PoliciesTable = ({ }) } renderCount={renderPoliciesCount} - onQueryChange={onTableQueryChange} + // 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 1880dff54093..d349c9327997 100644 --- a/frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx +++ b/frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx @@ -37,7 +37,7 @@ import DeleteQueryModal from "./components/DeleteQueryModal"; import ManageQueryAutomationsModal from "./components/ManageQueryAutomationsModal/ManageQueryAutomationsModal"; import PreviewDataModal from "./components/PreviewDataModal/PreviewDataModal"; -// const DEFAULT_PAGE_SIZE = 20; +const DEFAULT_PAGE_SIZE = 20; const baseClass = "manage-queries-page"; interface IManageQueriesPageProps { @@ -121,27 +121,44 @@ const ManageQueriesPage = ({ const [isUpdatingQueries, setIsUpdatingQueries] = useState(false); const [isUpdatingAutomations, setIsUpdatingAutomations] = useState(false); + const curPageFromURL = location.query.page + ? parseInt(location.query.page, 10) + : 0; + const { - data: enhancedQueries, + data: queriesResponse, error: queriesError, isFetching: isFetchingQueries, refetch: refetchQueries, } = useQuery< IQueriesResponse, Error, - IEnhancedQuery[], + IQueriesResponse, IQueryKeyQueriesLoadAll[] >( - [{ scope: "queries", teamId: teamIdForApi }], - ({ queryKey: [{ teamId }] }) => - queriesAPI.loadAll(teamId, teamId !== API_ALL_TEAMS_ID), + [ + { + scope: "queries", + teamId: teamIdForApi, + page: curPageFromURL, + perPage: DEFAULT_PAGE_SIZE, + query: location.query.query, + orderDirection: location.query.order_direction, + orderKey: location.query.order_key, + mergeInherited: teamIdForApi !== API_ALL_TEAMS_ID, + }, + ], + ({ queryKey }) => queriesAPI.loadAll(queryKey[0]), { refetchOnWindowFocus: false, enabled: isRouteOk, staleTime: 5000, - select: ({ queries }) => queries.map(enhanceQuery), } ); + + // select: ({ queries }) => queries.map(enhanceQuery), + const enhancedQueries = queriesResponse?.queries.map(enhanceQuery); + const queriesAvailableToAutomate = (teamIdForApi ? enhancedQueries?.filter( @@ -258,9 +275,17 @@ const ManageQueriesPage = ({ if (queriesError) { return ; } + + // TODO - coordinate these properties with useQuery and the below table + // page: tableQueryData?.pageIndex, + // perPage: DEFAULT_PAGE_SIZE, + // query: searchQuery, + // orderDirection: orderBy, + // orderKey: orderKey, return ( { it("Renders the page-wide empty state when no queries are present", () => { const testData: IQueriesTableProps[] = [ { - queriesList: [], + queries: [], + totalQueriesCount: 0, onlyInheritedQueries: false, isLoading: false, onDeleteQueryClick: jest.fn(), @@ -169,7 +171,8 @@ describe("QueriesTable", () => { 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], + queries: [...testGlobalQueries, ...testTeamQueries], + totalQueriesCount: 4, onlyInheritedQueries: false, isLoading: false, onDeleteQueryClick: jest.fn(), @@ -228,7 +231,8 @@ describe("QueriesTable", () => { const { user } = render( { const { user } = render( { render( { const { container, user } = render( void; @@ -87,7 +88,8 @@ const PLATFORM_FILTER_OPTIONS = [ ]; const QueriesTable = ({ - queriesList, + queries, + totalQueriesCount, onlyInheritedQueries, isLoading, onDeleteQueryClick, @@ -105,33 +107,33 @@ const QueriesTable = ({ // queriesState tracks search filter and compatible platform filter // to correctly show filtered queries and filtered count // isQueryStateLoading prevents flashing of unfiltered count during clientside filtering - const [queriesState, setQueriesState] = useState([]); - const [isQueriesStateLoading, setIsQueriesStateLoading] = useState(true); + // const [queriesState, setQueriesState] = useState([]); + // const [isQueriesStateLoading, setIsQueriesStateLoading] = useState(true); - useEffect(() => { - setIsQueriesStateLoading(true); - if (queriesList) { - setQueriesState( - queriesList.filter((query) => { - const filterSearchQuery = queryParams?.query - ? query.name - .toLowerCase() - .includes(queryParams?.query.toLowerCase()) - : true; - const compatiblePlatforms = - checkPlatformCompatibility(query.query).platforms || []; + // useEffect(() => { + // setIsQueriesStateLoading(true); + // if (queries) { + // setQueriesState( + // queries.filter((query) => { + // const filterSearchQuery = queryParams?.query + // ? query.name + // .toLowerCase() + // .includes(queryParams?.query.toLowerCase()) + // : true; + // const compatiblePlatforms = + // checkPlatformCompatibility(query.query).platforms || []; - const filterCompatiblePlatform = - queryParams?.platform && queryParams?.platform !== "all" - ? compatiblePlatforms.includes(queryParams?.platform) - : true; + // const filterCompatiblePlatform = + // queryParams?.platform && queryParams?.platform !== "all" + // ? compatiblePlatforms.includes(queryParams?.platform) + // : true; - return filterSearchQuery && filterCompatiblePlatform; - }) || [] - ); - } - setIsQueriesStateLoading(false); - }, [queriesList, queryParams]); + // return filterSearchQuery && filterCompatiblePlatform; + // }) || [] + // ); + // } + // setIsQueriesStateLoading(false); + // }, [queries, queryParams]); // Functions to avoid race conditions const initialSearchQuery = (() => queryParams?.query ?? "")(); @@ -156,8 +158,9 @@ const QueriesTable = ({ const sortHeader = initialSortHeader; // TODO: Look into useDebounceCallback with dependencies + // TODO - ensure the events this triggers correctly lead to the updates intended const onQueryChange = useCallback( - async (newTableQuery: ITableQueryData) => { + (newTableQuery: ITableQueryData) => { const { pageIndex: newPageIndex, searchQuery: newSearchQuery, @@ -165,10 +168,7 @@ const QueriesTable = ({ sortHeader: newSortHeader, } = newTableQuery; - // Rebuild queryParams to dispatch new browser location to react-router - const newQueryParams: { [key: string]: string | number | undefined } = {}; - - // Updates URL params + const newQueryParams: Record = {}; newQueryParams.order_key = newSortHeader; newQueryParams.order_direction = newSortDirection; newQueryParams.platform = platform; // must set from URL @@ -182,8 +182,8 @@ const QueriesTable = ({ ) { newQueryParams.page = "0"; } - newQueryParams.team_id = queryParams?.team_id; + const locationPath = getNextLocationPath({ pathPrefix: PATHS.MANAGE_QUERIES, queryParams: { ...queryParams, ...newQueryParams }, @@ -194,22 +194,22 @@ const QueriesTable = ({ [sortHeader, sortDirection, searchQuery, platform, router, page] ); - const onClientSidePaginationChange = useCallback( - (pageIndex: number) => { - const newQueryParams = { - ...queryParams, - page: pageIndex, // update main table index - query: searchQuery, - }; + // const onClientSidePaginationChange = useCallback( + // (pageIndex: number) => { + // const newQueryParams = { + // ...queryParams, + // page: pageIndex, // update main table index + // query: searchQuery, + // }; - const locationPath = getNextLocationPath({ - pathPrefix: PATHS.MANAGE_QUERIES, - queryParams: newQueryParams, - }); - router?.replace(locationPath); - }, - [platform, searchQuery, sortDirection, sortHeader] // Dependencies required for correct variable state - ); + // const locationPath = getNextLocationPath({ + // pathPrefix: PATHS.MANAGE_QUERIES, + // queryParams: newQueryParams, + // }); + // router?.replace(locationPath); + // }, + // [platform, searchQuery, sortDirection, sortHeader] // Dependencies required for correct variable state + // ); const getEmptyStateParams = useCallback(() => { const emptyQueries: IEmptyTableProps = { @@ -277,14 +277,14 @@ const QueriesTable = ({ ); }, [platform, queryParams, router]); - const renderQueriesCount = useCallback(() => { - // Fixes flashing incorrect count before clientside filtering - if (isQueriesStateLoading) { - return null; - } + // const renderQueriesCount = useCallback(() => { + // // Fixes flashing incorrect count before clientside filtering + // if (isQueriesStateLoading) { + // return null; + // } - return ; - }, [queriesState, isQueriesStateLoading]); + // return ; + // }, [queriesState, isQueriesStateLoading]); const columnConfigs = useMemo( () => @@ -297,7 +297,7 @@ const QueriesTable = ({ [currentUser, currentTeamId, onlyInheritedQueries] ); - const searchable = !(queriesList?.length === 0 && searchQuery === ""); + const searchable = !(queries?.length === 0 && searchQuery === ""); const emptyComponent = useCallback(() => { const { @@ -318,48 +318,57 @@ const QueriesTable = ({ const trimmedSearchQuery = searchQuery.trim(); - const deleteQueryTableActionButtonProps = useMemo( - () => - ({ - name: "delete query", - buttonText: "Delete", - iconSvg: "trash", - variant: "text-icon", - onActionButtonClick: onDeleteQueryClick, - // this maintains the existing typing, which is not actually correct - // TODO - update this object to actually implement IActionButtonProps - } as IActionButtonProps), - [onDeleteQueryClick] - ); + // const deleteQueryTableActionButtonProps = useMemo( + // () => + // ( as IActionButtonProps), + // [onDeleteQueryClick] + // ); return columnConfigs && !isLoading ? (
( + // TODO - is more logic necessary here? Can we omit this? + + )} + // pageSize={DEFAULT_PAGE_SIZE} + inputPlaceHolder="Search by name" + onQueryChange={onQueryChange} searchable={searchable} - searchQueryColumn="name" + // TODO - will likely need to implement this somehow. Looks messy for policies, so avoid if + // not necessary. + // resetPageIndex= + customControl={searchable ? renderPlatformDropdown : undefined} - isClientSidePagination - onClientSidePaginationChange={onClientSidePaginationChange} - isClientSideFilter - primarySelectAction={deleteQueryTableActionButtonProps} + // searchQueryColumn="name" + // onClientSidePaginationChange={onClientSidePaginationChange} + // isClientSideFilter // TODO - consolidate this functionality within `filters` selectedDropdownFilter={platform} - renderCount={renderQueriesCount} />
) : ( diff --git a/frontend/services/entities/global_policies.ts b/frontend/services/entities/global_policies.ts index 340f5b5e4b1f..5f6ee03227c3 100644 --- a/frontend/services/entities/global_policies.ts +++ b/frontend/services/entities/global_policies.ts @@ -8,7 +8,10 @@ import { ILoadAllPoliciesResponse, IPoliciesCountResponse, } from "interfaces/policy"; -import { buildQueryStringFromParams, QueryParams } from "utilities/url"; +import { + buildQueryStringFromParams, + convertParamsToSnakeCase, +} from "utilities/url"; interface IPoliciesApiParams { page?: number; @@ -30,17 +33,6 @@ export interface IPoliciesCountQueryKey const ORDER_KEY = "name"; const ORDER_DIRECTION = "asc"; -const convertParamsToSnakeCase = (params: IPoliciesApiParams) => { - return reduce( - params, - (result, val, key) => { - result[snakeCase(key)] = val; - return result; - }, - {} - ); -}; - export default { // TODO: How does the frontend need to support legacy policies? create: (data: IPolicyFormData) => { @@ -71,7 +63,7 @@ export default { return sendRequest("GET", GLOBAL_POLICIES); }, - loadAllNew: async ({ + loadAllNew: ({ page, perPage, orderKey = ORDER_KEY, @@ -94,7 +86,7 @@ export default { return sendRequest("GET", path); }, - getCount: async ({ + getCount: ({ query, }: Pick): Promise => { const { GLOBAL_POLICIES } = endpoints; diff --git a/frontend/services/entities/queries.ts b/frontend/services/entities/queries.ts index 8fd3f07a3593..9455f3b7d5b8 100644 --- a/frontend/services/entities/queries.ts +++ b/frontend/services/entities/queries.ts @@ -8,7 +8,23 @@ import { IModifyQueryRequestBody, ISchedulableQuery, } from "interfaces/schedulable_query"; -import { buildQueryStringFromParams } from "utilities/url"; +import { + buildQueryStringFromParams, + convertParamsToSnakeCase, +} from "utilities/url"; + +export interface ILoadQueriesParams { + teamId?: number; + page?: number; + perPage?: number; + query?: string; + orderDirection?: "asc" | "desc"; + orderKey?: string; + mergeInherited?: boolean; +} +export interface IQueryKeyLoadQueries extends ILoadQueriesParams { + scope: "queries"; +} export interface IQueriesResponse { queries: ISchedulableQuery[]; @@ -45,22 +61,31 @@ export default { return sendRequest("GET", path); }, - // loadAll: (teamId?: number, perPage?: number, mergeInherited = false) => { - loadAll: ( - teamId?: number, - mergeInherited = false - ): Promise => { + loadAll: ({ + teamId, + page, + perPage, + query, + orderDirection, + orderKey, + mergeInherited, + }: IQueryKeyLoadQueries): Promise => { const { QUERIES } = endpoints; - const queryString = buildQueryStringFromParams({ - team_id: teamId, - merge_inherited: mergeInherited || null, - // per_page: perPage || null, + + const snakeCaseParams = convertParamsToSnakeCase({ + teamId, + page, + perPage, + query, + orderDirection, + orderKey, + mergeInherited, }); - const path = `${QUERIES}`; + const queryString = buildQueryStringFromParams(snakeCaseParams); return sendRequest( "GET", - queryString ? path.concat(`?${queryString}`) : path + queryString ? QUERIES.concat(`?${queryString}`) : QUERIES ); }, run: async ({ diff --git a/server/datastore/mysql/queries.go b/server/datastore/mysql/queries.go index 758562283a7e..c7088459aa20 100644 --- a/server/datastore/mysql/queries.go +++ b/server/datastore/mysql/queries.go @@ -560,7 +560,6 @@ func (ds *Datastore) ListQueries(ctx context.Context, opt fleet.ListQueryOptions } } - return queries, count, meta, nil } diff --git a/server/service/queries.go b/server/service/queries.go index b9da5df0db99..e0d124a6e12f 100644 --- a/server/service/queries.go +++ b/server/service/queries.go @@ -63,10 +63,10 @@ type listQueriesRequest struct { } type listQueriesResponse struct { - Queries []fleet.Query `json:"queries"` - Count int `json:"count"` - Meta *fleet.PaginationMetadata `json:"meta"` - Err error `json:"error,omitempty"` + Queries []fleet.Query `json:"queries"` + Count int `json:"count"` + Meta *fleet.PaginationMetadata `json:"meta"` + Err error `json:"error,omitempty"` } func (r listQueriesResponse) error() error { return r.Err } @@ -88,12 +88,11 @@ func listQueriesEndpoint(ctx context.Context, request interface{}, svc fleet.Ser for _, query := range queries { respQueries = append(respQueries, *query) } - return listQueriesResponse{ Queries: respQueries, - Count: count, - Meta: meta, + Count: count, + Meta: meta, }, nil } From 5db60e8c06d4d123eba9ac3e74ee5b9ffa79ca78 Mon Sep 17 00:00:00 2001 From: Jacob Shandling Date: Wed, 23 Oct 2024 17:29:29 -0700 Subject: [PATCH 06/16] Match querie with LIKE --- server/datastore/mysql/queries.go | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/server/datastore/mysql/queries.go b/server/datastore/mysql/queries.go index c7088459aa20..afced6fca553 100644 --- a/server/datastore/mysql/queries.go +++ b/server/datastore/mysql/queries.go @@ -6,6 +6,8 @@ import ( "fmt" "strings" + "golang.org/x/text/unicode/norm" + "github.com/fleetdm/fleet/v4/server/contexts/ctxerr" "github.com/fleetdm/fleet/v4/server/fleet" "github.com/go-kit/log/level" @@ -17,6 +19,8 @@ const ( statsLiveQueryType ) +var querySearchColumns = []string{"q.name"} + func (ds *Datastore) ApplyQueries(ctx context.Context, authorID uint, queries []*fleet.Query, queriesToDiscardResults map[uint]struct{}) error { if err := ds.applyQueriesInTx(ctx, authorID, queries); err != nil { return ctxerr.Wrap(ctx, err, "apply queries in tx") @@ -523,10 +527,9 @@ func (ds *Datastore) ListQueries(ctx context.Context, opt fleet.ListQueryOptions } } - if opt.MatchQuery != "" { - whereClauses += " AND q.name = ?" - args = append(args, opt.MatchQuery) - } + // normalize the name for full Unicode support (Unicode equivalence). + normMatch := norm.NFC.String(opt.MatchQuery) + whereClauses, args = searchLike(whereClauses, args, normMatch, querySearchColumns...) getQueriesStmt += whereClauses From 7591b3f1a13754c1dbc4c896adf44fe4a8b17479 Mon Sep 17 00:00:00 2001 From: Jacob Shandling Date: Thu, 31 Oct 2024 08:53:32 -0700 Subject: [PATCH 07/16] Frontend `compatible_platform` logic --- frontend/interfaces/schedulable_query.ts | 7 ++- .../details/cards/Software/HostSoftware.tsx | 8 +-- .../ManageQueriesPage/ManageQueriesPage.tsx | 16 +++--- .../components/QueriesTable/QueriesTable.tsx | 57 +++++++++++++------ frontend/services/entities/queries.ts | 13 ++++- server/fleet/app.go | 3 + server/fleet/service.go | 2 +- server/service/global_schedule.go | 2 +- server/service/queries.go | 46 +++++++++++++-- server/service/team_schedule.go | 2 +- 10 files changed, 117 insertions(+), 39 deletions(-) diff --git a/frontend/interfaces/schedulable_query.ts b/frontend/interfaces/schedulable_query.ts index 44c15b23aaeb..385ff191f2ab 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 { @@ -74,6 +78,7 @@ export interface IQueryKeyQueriesLoadAll { orderDirection?: "asc" | "desc"; orderKey?: string; mergeInherited?: boolean; + compatiblePlatform?: SelectedPlatform; } // Create a new query /** POST /api/v1/fleet/queries */ diff --git a/frontend/pages/hosts/details/cards/Software/HostSoftware.tsx b/frontend/pages/hosts/details/cards/Software/HostSoftware.tsx index 1853d0aeee34..0601288837e6 100644 --- a/frontend/pages/hosts/details/cards/Software/HostSoftware.tsx +++ b/frontend/pages/hosts/details/cards/Software/HostSoftware.tsx @@ -248,13 +248,13 @@ const HostSoftware = ({ return isMyDevicePage ? generateDeviceSoftwareTableConfig() : generateHostSoftwareTableConfig({ - router, - softwareIdActionPending, userHasSWWritePermission, hostScriptsEnabled, - onSelectAction, - teamId: hostTeamId, hostCanWriteSoftware, + softwareIdActionPending, + router, + teamId: hostTeamId, + onSelectAction, }); }, [ isMyDevicePage, diff --git a/frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx b/frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx index d349c9327997..36478c25a03a 100644 --- a/frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx +++ b/frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx @@ -45,7 +45,8 @@ interface IManageQueriesPageProps { location: { pathname: string; query: { - platform?: SelectedPlatform; + // note that the URL value "darwin" will correspond to the request query param "macos" - TODO: reconcile + compatible_platform?: SelectedPlatform; page?: string; query?: string; order_key?: string; @@ -68,6 +69,8 @@ export const enhanceQuery = (q: ISchedulableQuery): IEnhancedQuery => { performance: getPerformanceImpactDescription( pick(q.stats, ["user_time_p50", "system_time_p50", "total_executions"]) ), + // TODO - once we are storing platform compatibility in a db column, remove this processing and + // rely on that instead platforms: getPlatforms(q.query), }; }; @@ -142,10 +145,12 @@ const ManageQueriesPage = ({ 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, + compatiblePlatform: location.query.compatible_platform, }, ], ({ queryKey }) => queriesAPI.loadAll(queryKey[0]), @@ -156,7 +161,6 @@ const ManageQueriesPage = ({ } ); - // select: ({ queries }) => queries.map(enhanceQuery), const enhancedQueries = queriesResponse?.queries.map(enhanceQuery); const queriesAvailableToAutomate = @@ -275,13 +279,6 @@ const ManageQueriesPage = ({ if (queriesError) { return ; } - - // TODO - coordinate these properties with useQuery and the below table - // page: tableQueryData?.pageIndex, - // perPage: DEFAULT_PAGE_SIZE, - // query: searchQuery, - // orderDirection: orderBy, - // orderKey: orderKey, return ( (queryParams?.order_direction as "asc" | "desc") ?? DEFAULT_SORT_DIRECTION)(); - const initialPlatform = (() => - (queryParams?.platform as "all" | "windows" | "linux" | "darwin") ?? - DEFAULT_PLATFORM)(); const initialPage = (() => queryParams && queryParams.page ? parseInt(queryParams?.page, 10) : 0)(); // Source of truth is state held within TableContainer. That state is initialized using URL // params, then subsequent updates to that state are pushed to the URL. + // TODO - remove extaneous defintions around these values const searchQuery = initialSearchQuery; - const platform = initialPlatform; const page = initialPage; const sortDirection = initialSortDirection; const sortHeader = initialSortHeader; + const curCompatiblePlatformFilter = + (queryParams?.compatible_platform as + | "all" + | "windows" + | "linux" + | "darwin") ?? DEFAULT_PLATFORM; + // TODO: Look into useDebounceCallback with dependencies // TODO - ensure the events this triggers correctly lead to the updates intended const onQueryChange = useCallback( @@ -171,7 +176,10 @@ const QueriesTable = ({ const newQueryParams: Record = {}; newQueryParams.order_key = newSortHeader; newQueryParams.order_direction = newSortDirection; - newQueryParams.platform = platform; // must set from URL + newQueryParams.compatible_platform = + curCompatiblePlatformFilter === "all" + ? undefined + : curCompatiblePlatformFilter; newQueryParams.page = newPageIndex; newQueryParams.query = newSearchQuery; // Reset page number to 0 for new filters @@ -189,9 +197,16 @@ const QueriesTable = ({ queryParams: { ...queryParams, ...newQueryParams }, }); - router?.replace(locationPath); + router?.push(locationPath); }, - [sortHeader, sortDirection, searchQuery, platform, router, page] + [ + sortHeader, + sortDirection, + searchQuery, + curCompatiblePlatformFilter, + router, + page, + ] ); // const onClientSidePaginationChange = useCallback( @@ -251,23 +266,33 @@ const QueriesTable = ({ searchQuery, ]); - const renderPlatformDropdown = useCallback(() => { - const handlePlatformFilterDropdownChange = (platformSelected: string) => { - router?.replace( + // TODO - remove comment once stable + // if there are issues with the platform dropdown rendering stability, look here + const handlePlatformFilterDropdownChange = useCallback( + (selectedCompatiblePlatform: string) => { + router?.push( getNextLocationPath({ pathPrefix: PATHS.MANAGE_QUERIES, queryParams: { ...queryParams, page: 0, - platform: platformSelected, + compatible_platform: + // separate URL & API 0-values of "compatible_platform" (undefined) from dropdown + // 0-value of "all" + selectedCompatiblePlatform === "all" + ? undefined + : selectedCompatiblePlatform, }, }) ); - }; + }, + [queryParams, router] + ); + const renderPlatformDropdown = useCallback(() => { return ( ); - }, [platform, queryParams, router]); + }, [curCompatiblePlatformFilter, queryParams, router]); // const renderQueriesCount = useCallback(() => { // // Fixes flashing incorrect count before clientside filtering @@ -368,7 +393,7 @@ const QueriesTable = ({ // onClientSidePaginationChange={onClientSidePaginationChange} // isClientSideFilter // TODO - consolidate this functionality within `filters` - selectedDropdownFilter={platform} + selectedDropdownFilter={curCompatiblePlatformFilter} /> ) : ( diff --git a/frontend/services/entities/queries.ts b/frontend/services/entities/queries.ts index 9455f3b7d5b8..e6497ba29a87 100644 --- a/frontend/services/entities/queries.ts +++ b/frontend/services/entities/queries.ts @@ -6,12 +6,14 @@ import { ISelectedTargetsForApi } from "interfaces/target"; import { ICreateQueryRequestBody, IModifyQueryRequestBody, + IQueryKeyQueriesLoadAll, ISchedulableQuery, } from "interfaces/schedulable_query"; import { buildQueryStringFromParams, convertParamsToSnakeCase, } from "utilities/url"; +import { SelectedPlatform } from "interfaces/platform"; export interface ILoadQueriesParams { teamId?: number; @@ -21,6 +23,7 @@ export interface ILoadQueriesParams { orderDirection?: "asc" | "desc"; orderKey?: string; mergeInherited?: boolean; + compatiblePlatform?: SelectedPlatform; } export interface IQueryKeyLoadQueries extends ILoadQueriesParams { scope: "queries"; @@ -69,7 +72,8 @@ export default { orderDirection, orderKey, mergeInherited, - }: IQueryKeyLoadQueries): Promise => { + compatiblePlatform, + }: IQueryKeyQueriesLoadAll): Promise => { const { QUERIES } = endpoints; const snakeCaseParams = convertParamsToSnakeCase({ @@ -80,7 +84,14 @@ export default { orderDirection, orderKey, mergeInherited, + compatiblePlatform, }); + + // API expects "macos" instead of "darwin" + if (snakeCaseParams.compatible_platform === "darwin") { + snakeCaseParams.compatible_platform = "macos"; + } + const queryString = buildQueryStringFromParams(snakeCaseParams); return sendRequest( diff --git a/server/fleet/app.go b/server/fleet/app.go index c56c3bebfa0a..f416db09e14f 100644 --- a/server/fleet/app.go +++ b/server/fleet/app.go @@ -1090,6 +1090,9 @@ type ListQueryOptions struct { // MergeInherited merges inherited global queries into the team list. Is only valid when TeamID // is set. MergeInherited bool + // Return queries that are compatible (not a strict check) with this platform. One of "macos", + // "windows", "linux", or "chrome" + CompatiblePlatform string } type ListActivitiesOptions struct { diff --git a/server/fleet/service.go b/server/fleet/service.go index 2558f7075220..cc5ed816c790 100644 --- a/server/fleet/service.go +++ b/server/fleet/service.go @@ -273,7 +273,7 @@ type Service interface { // and only non-scheduled queries will be returned if `*scheduled == false`. // If mergeInherited is true and a teamID is provided, then queries from the global team will be // included in the results. - ListQueries(ctx context.Context, opt ListOptions, teamID *uint, scheduled *bool, mergeInherited bool) ([]*Query, int, *PaginationMetadata, error) + ListQueries(ctx context.Context, opt ListOptions, teamID *uint, scheduled *bool, mergeInherited bool, compatiblePlatform *string) ([]*Query, int, *PaginationMetadata, 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. // Returns a boolean indicating whether the report is clipped. diff --git a/server/service/global_schedule.go b/server/service/global_schedule.go index cf05873b94e9..b2a01d813d39 100644 --- a/server/service/global_schedule.go +++ b/server/service/global_schedule.go @@ -37,7 +37,7 @@ func getGlobalScheduleEndpoint(ctx context.Context, request interface{}, svc fle } func (svc *Service) GetGlobalScheduledQueries(ctx context.Context, opts fleet.ListOptions) ([]*fleet.ScheduledQuery, error) { - queries, _, _, err := svc.ListQueries(ctx, opts, nil, ptr.Bool(true), false) // teamID == nil means global + queries, _, _, err := svc.ListQueries(ctx, opts, nil, ptr.Bool(true), false, nil) // teamID == nil means global if err != nil { return nil, err } diff --git a/server/service/queries.go b/server/service/queries.go index e0d124a6e12f..9547bf8336e2 100644 --- a/server/service/queries.go +++ b/server/service/queries.go @@ -14,6 +14,25 @@ import ( "github.com/fleetdm/fleet/v4/server/ptr" ) +func getCompatiblePlatformsFromSQL(query string) ([]string, error) { + // TODO! + return []string{"macos", "linux", "windows"}, nil +} + +func filterQueriesByCompatiblePlatform(queries []*fleet.Query, compatiblePlatform *string) ([]*fleet.Query, error) { + var filteredQueries []*fleet.Query + for _, query := range queries { + compatiblePlatforms, err := getCompatiblePlatformsFromSQL(query.Query) + if err != nil { + return nil, err + } + if slices.Contains(compatiblePlatforms, *compatiblePlatform) { + filteredQueries = append(filteredQueries, query) + } + } + return filteredQueries, nil +} + //////////////////////////////////////////////////////////////////////////////// // Get Query //////////////////////////////////////////////////////////////////////////////// @@ -58,8 +77,9 @@ func (svc *Service) GetQuery(ctx context.Context, id uint) (*fleet.Query, error) type listQueriesRequest struct { ListOptions fleet.ListOptions `url:"list_options"` // TeamID url argument set to 0 means global. - TeamID uint `query:"team_id,optional"` - MergeInherited bool `query:"merge_inherited,optional"` + TeamID uint `query:"team_id,optional"` + MergeInherited bool `query:"merge_inherited,optional"` + CompatiblePlatform string `query:"compatible_platform,optional"` } type listQueriesResponse struct { @@ -79,7 +99,12 @@ func listQueriesEndpoint(ctx context.Context, request interface{}, svc fleet.Ser teamID = &req.TeamID } - queries, count, meta, err := svc.ListQueries(ctx, req.ListOptions, teamID, nil, req.MergeInherited) + var compatiblePlaform *string + if req.CompatiblePlatform != "" { + compatiblePlaform = &req.CompatiblePlatform + } + + queries, count, meta, err := svc.ListQueries(ctx, req.ListOptions, teamID, nil, req.MergeInherited, compatiblePlaform) if err != nil { return listQueriesResponse{Err: err}, nil } @@ -96,7 +121,7 @@ func listQueriesEndpoint(ctx context.Context, request interface{}, svc fleet.Ser }, nil } -func (svc *Service) ListQueries(ctx context.Context, opt fleet.ListOptions, teamID *uint, scheduled *bool, mergeInherited bool) ([]*fleet.Query, int, *fleet.PaginationMetadata, error) { +func (svc *Service) ListQueries(ctx context.Context, opt fleet.ListOptions, teamID *uint, scheduled *bool, mergeInherited bool, compatiblePlatform *string) ([]*fleet.Query, int, *fleet.PaginationMetadata, error) { // Check the user is allowed to list queries on the given team. if err := svc.authz.Authorize(ctx, &fleet.Query{ TeamID: teamID, @@ -117,6 +142,17 @@ func (svc *Service) ListQueries(ctx context.Context, opt fleet.ListOptions, team return nil, 0, nil, err } + if compatiblePlatform != nil { + // TODO on future iteration - instead of filtering per call like this, calculate each query's compatible platform + // and store it in the db on save/edit (query.compatible_platform, different from + // query.platform), then update svc.ds.ListQueries to filter by compatible platform. + // This avoids overhead of a migration for now + queries, err = filterQueriesByCompatiblePlatform(queries, compatiblePlatform) + if err != nil { + return nil, 0, nil, err + } + } + return queries, count, meta, nil } @@ -753,7 +789,7 @@ func getQuerySpecsEndpoint(ctx context.Context, request interface{}, svc fleet.S } func (svc *Service) GetQuerySpecs(ctx context.Context, teamID *uint) ([]*fleet.QuerySpec, error) { - queries, _, _, err := svc.ListQueries(ctx, fleet.ListOptions{}, teamID, nil, false) + queries, _, _, err := svc.ListQueries(ctx, fleet.ListOptions{}, teamID, nil, false, nil) if err != nil { return nil, ctxerr.Wrap(ctx, err, "getting queries") } diff --git a/server/service/team_schedule.go b/server/service/team_schedule.go index b430749caaeb..8cb087545583 100644 --- a/server/service/team_schedule.go +++ b/server/service/team_schedule.go @@ -47,7 +47,7 @@ func (svc Service) GetTeamScheduledQueries(ctx context.Context, teamID uint, opt if teamID != 0 { teamID_ = &teamID } - queries, _, _, err := svc.ListQueries(ctx, opts, teamID_, ptr.Bool(true), false) + queries, _, _, err := svc.ListQueries(ctx, opts, teamID_, ptr.Bool(true), false, nil) if err != nil { return nil, err } From 12ffdf6ac541449e5f0d952604c037d7d8cd76de Mon Sep 17 00:00:00 2001 From: Jacob Shandling Date: Thu, 31 Oct 2024 11:14:27 -0700 Subject: [PATCH 08/16] Handle invalid `queryable_platform` URL params; improve typing --- frontend/hooks/usePlatformCompatibility.tsx | 4 ++-- frontend/hooks/usePlatformSelector.tsx | 4 ++-- frontend/interfaces/platform.ts | 7 ++++++- .../components/QueriesTable/QueriesTable.tsx | 21 +++++++++++-------- frontend/utilities/sql_tools.ts | 6 +++--- 5 files changed, 25 insertions(+), 17 deletions(-) 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 2be1f412d4b3..4d17a1af7131 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/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTable.tsx b/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTable.tsx index 900f0ca22b8d..2c8142a8aef4 100644 --- a/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTable.tsx +++ b/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTable.tsx @@ -10,7 +10,12 @@ import { InjectedRouter } from "react-router"; import { AppContext } from "context/app"; import { IEmptyTableProps } from "interfaces/empty_table"; -import { SelectedPlatform } from "interfaces/platform"; +import { + isQueryablePlatform, + QUERYABLE_PLATFORMS, + QueryablePlatform, + SelectedPlatform, +} from "interfaces/platform"; import { IEnhancedQuery } from "interfaces/schedulable_query"; import { ITableQueryData } from "components/TableContainer/TableContainer"; import { IActionButtonProps } from "components/TableContainer/DataTable/ActionButton/ActionButton"; @@ -39,7 +44,7 @@ export interface IQueriesTableProps { isAnyTeamObserverPlus: boolean; router?: InjectedRouter; queryParams?: { - compatible_platform?: SelectedPlatform; + compatible_platform?: string; page?: string; query?: string; order_key?: string; @@ -53,7 +58,7 @@ const DEFAULT_SORT_DIRECTION = "asc"; const DEFAULT_SORT_HEADER = "name"; const DEFAULT_PAGE_SIZE = 20; // all platforms -const DEFAULT_PLATFORM = "all"; +const DEFAULT_PLATFORM: SelectedPlatform = "all"; const PLATFORM_FILTER_OPTIONS = [ { @@ -155,12 +160,10 @@ const QueriesTable = ({ const sortDirection = initialSortDirection; const sortHeader = initialSortHeader; - const curCompatiblePlatformFilter = - (queryParams?.compatible_platform as - | "all" - | "windows" - | "linux" - | "darwin") ?? DEFAULT_PLATFORM; + const compatPlatformParam = queryParams?.compatible_platform; + const curCompatiblePlatformFilter = isQueryablePlatform(compatPlatformParam) + ? compatPlatformParam + : DEFAULT_PLATFORM; // TODO: Look into useDebounceCallback with dependencies // TODO - ensure the events this triggers correctly lead to the updates intended diff --git a/frontend/utilities/sql_tools.ts b/frontend/utilities/sql_tools.ts index edf845fc0d79..6fdca47fd6b6 100644 --- a/frontend/utilities/sql_tools.ts +++ b/frontend/utilities/sql_tools.ts @@ -4,7 +4,7 @@ import { intersection, isPlainObject } from "lodash"; import { osqueryTablesAvailable } from "utilities/osquery_tables"; import { MACADMINS_EXTENSION_TABLES, - SUPPORTED_PLATFORMS, + QUERYABLE_PLATFORMS, QueryablePlatform, } from "interfaces/platform"; import { TableSchemaPlatform } from "interfaces/osquery_table"; @@ -59,7 +59,7 @@ const filterCompatiblePlatforms = ( sqlTables: string[] ): QueryablePlatform[] => { if (!sqlTables.length) { - return [...SUPPORTED_PLATFORMS]; // if a query has no tables but is still syntatically valid sql, it is treated as compatible with all platforms + return [...QUERYABLE_PLATFORMS]; // if a query has no tables but is still syntatically valid sql, it is treated as compatible with all platforms } const compatiblePlatforms = intersection( @@ -68,7 +68,7 @@ const filterCompatiblePlatforms = ( ) ); - return SUPPORTED_PLATFORMS.filter((p) => compatiblePlatforms.includes(p)); + return QUERYABLE_PLATFORMS.filter((p) => compatiblePlatforms.includes(p)); }; export const parseSqlTables = ( From 0d40632f4c46e7d46d6477102925411e15c1ccea Mon Sep 17 00:00:00 2001 From: Jacob Shandling Date: Thu, 31 Oct 2024 11:53:34 -0700 Subject: [PATCH 09/16] update searchability logic for correct rendering of platform dropdown --- .../ManageQueriesPage/components/QueriesTable/QueriesTable.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTable.tsx b/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTable.tsx index 2c8142a8aef4..192f9a4e56c7 100644 --- a/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTable.tsx +++ b/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTable.tsx @@ -325,7 +325,7 @@ const QueriesTable = ({ [currentUser, currentTeamId, onlyInheritedQueries] ); - const searchable = !(queries?.length === 0 && searchQuery === ""); + const searchable = (totalQueriesCount ?? 0) > 0; const emptyComponent = useCallback(() => { const { From 37dd470becf2c06a09320b8bd51c25bb8e70258d Mon Sep 17 00:00:00 2001 From: Jacob Shandling Date: Thu, 31 Oct 2024 11:58:21 -0700 Subject: [PATCH 10/16] update empty table props for correct messaging --- .../components/QueriesTable/QueriesTable.tsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTable.tsx b/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTable.tsx index 192f9a4e56c7..0aedc3d07d93 100644 --- a/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTable.tsx +++ b/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTable.tsx @@ -230,16 +230,16 @@ const QueriesTable = ({ // ); const getEmptyStateParams = useCallback(() => { - const emptyQueries: IEmptyTableProps = { + const emptyParams: IEmptyTableProps = { graphicName: "empty-queries", header: "You don't have any queries", }; - if (searchQuery) { - delete emptyQueries.graphicName; - emptyQueries.header = "No matching queries"; - emptyQueries.info = "No queries match the current filters."; + if (searchQuery || curCompatiblePlatformFilter !== "all") { + delete emptyParams.graphicName; + emptyParams.header = "No matching queries"; + emptyParams.info = "No queries match the current filters."; } else if (!isOnlyObserver || isObserverPlus || isAnyTeamObserverPlus) { - emptyQueries.additionalInfo = ( + emptyParams.additionalInfo = ( <> Create a new query, or{" "} ); - emptyQueries.primaryButton = ( + emptyParams.primaryButton = (