From 8d98f34365c842c66fb630821e4d76d679a6b3f8 Mon Sep 17 00:00:00 2001 From: nulmete Date: Wed, 4 Feb 2026 11:27:53 -0300 Subject: [PATCH 1/5] Show Manage Automations disabled button with tooltip on Queries page --- cmd/fleetctl/fleetctl/get_test.go | 22 ++-- cmd/fleetctl/fleetctl/gitops_test.go | 36 +++--- cmd/fleetctl/fleetctl/query_test.go | 6 +- cmd/fleetctl/fleetctl/session_test.go | 4 +- .../fleetctl/testing_utils/testing_utils.go | 4 +- cmd/fleetctl/fleetctl/upgrade_packs_test.go | 8 +- .../gitops_enterprise_integration_test.go | 4 +- .../ManageQueriesPage/ManageQueriesPage.tsx | 78 +++++++----- .../ManageQueryAutomationsModal.tsx | 111 ++++++++++++++---- frontend/services/entities/queries.ts | 1 + server/datastore/mysql/queries.go | 29 +++-- server/datastore/mysql/queries_test.go | 48 ++++---- server/fleet/datastore.go | 7 +- server/fleet/service.go | 4 +- server/mock/datastore_mock.go | 4 +- server/mock/service/service_mock.go | 4 +- server/service/global_schedule.go | 2 +- server/service/global_schedule_test.go | 4 +- server/service/osquery_test.go | 4 +- server/service/queries.go | 34 +++--- server/service/queries_test.go | 6 +- server/service/team_schedule.go | 2 +- server/service/team_schedule_test.go | 4 +- server/service/testing_client.go | 2 +- 24 files changed, 264 insertions(+), 164 deletions(-) diff --git a/cmd/fleetctl/fleetctl/get_test.go b/cmd/fleetctl/fleetctl/get_test.go index 6ecf1edc27f..d854bbd4dd5 100644 --- a/cmd/fleetctl/fleetctl/get_test.go +++ b/cmd/fleetctl/fleetctl/get_test.go @@ -1438,7 +1438,7 @@ func TestGetQueries(t *testing.T) { } return nil, ¬FoundError{} } - ds.ListQueriesFunc = func(ctx context.Context, opt fleet.ListQueryOptions) ([]*fleet.Query, int, *fleet.PaginationMetadata, error) { + ds.ListQueriesFunc = func(ctx context.Context, opt fleet.ListQueryOptions) ([]*fleet.Query, int, int, *fleet.PaginationMetadata, error) { if opt.TeamID == nil { //nolint:gocritic // ignore ifElseChain return []*fleet.Query{ { @@ -1475,7 +1475,7 @@ func TestGetQueries(t *testing.T) { Saved: true, // ListQueries always returns the saved ones. ObserverCanRun: true, }, - }, 3, nil, nil + }, 3, 0, nil, nil } else if *opt.TeamID == 1 { return []*fleet.Query{ { @@ -1492,11 +1492,11 @@ func TestGetQueries(t *testing.T) { TeamID: ptr.Uint(1), ObserverCanRun: true, }, - }, 1, nil, nil + }, 1, 0, nil, nil } else if *opt.TeamID == 2 { - return []*fleet.Query{}, 0, nil, nil + return []*fleet.Query{}, 0, 0, nil, nil } - return nil, 0, nil, errors.New("invalid team ID") + return nil, 0, 0, nil, errors.New("invalid team ID") } expectedGlobal := `+--------+-------------+-----------+-----------+--------------------------------+ @@ -1765,7 +1765,7 @@ spec: func TestGetQueriesAsObserver(t *testing.T) { _, ds := testing_utils.RunServerWithMockedDS(t) - ds.ListQueriesFunc = func(ctx context.Context, opt fleet.ListQueryOptions) ([]*fleet.Query, int, *fleet.PaginationMetadata, error) { + ds.ListQueriesFunc = func(ctx context.Context, opt fleet.ListQueryOptions) ([]*fleet.Query, int, int, *fleet.PaginationMetadata, error) { return []*fleet.Query{ { ID: 42, @@ -1788,7 +1788,7 @@ func TestGetQueriesAsObserver(t *testing.T) { Query: "select 3;", ObserverCanRun: false, }, - }, 3, nil, nil + }, 3, 0, nil, nil } for _, tc := range []struct { @@ -2004,7 +2004,7 @@ spec: GlobalRole: nil, Teams: []fleet.UserTeam{{Role: fleet.RoleObserver}}, }) - ds.ListQueriesFunc = func(ctx context.Context, opt fleet.ListQueryOptions) ([]*fleet.Query, int, *fleet.PaginationMetadata, error) { + ds.ListQueriesFunc = func(ctx context.Context, opt fleet.ListQueryOptions) ([]*fleet.Query, int, int, *fleet.PaginationMetadata, error) { return []*fleet.Query{ { ID: 42, @@ -2020,12 +2020,12 @@ spec: Query: "select 2;", ObserverCanRun: false, }, - }, 2, nil, nil + }, 2, 0, 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, int, *fleet.PaginationMetadata, error) { + ds.ListQueriesFunc = func(ctx context.Context, opt fleet.ListQueryOptions) ([]*fleet.Query, int, int, *fleet.PaginationMetadata, error) { return []*fleet.Query{ { ID: 42, @@ -2041,7 +2041,7 @@ spec: Query: "select 2;", ObserverCanRun: true, }, - }, 2, nil, nil + }, 2, 0, nil, nil } expected = `+--------+-------------+-----------+-----------+----------------------------+ | NAME | DESCRIPTION | QUERY | TEAM | SCHEDULE | diff --git a/cmd/fleetctl/fleetctl/gitops_test.go b/cmd/fleetctl/fleetctl/gitops_test.go index 46eb13bc568..cfdf2a497f1 100644 --- a/cmd/fleetctl/fleetctl/gitops_test.go +++ b/cmd/fleetctl/fleetctl/gitops_test.go @@ -108,8 +108,8 @@ 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, int, *fleet.PaginationMetadata, error) { - return nil, 0, nil, nil + ds.ListQueriesFunc = func(ctx context.Context, opts fleet.ListQueryOptions) ([]*fleet.Query, int, int, *fleet.PaginationMetadata, error) { + return nil, 0, 0, nil, nil } ds.ListTeamPoliciesFunc = func( ctx context.Context, teamID uint, opts fleet.ListOptions, iopts fleet.ListOptions, @@ -291,8 +291,8 @@ 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, int, *fleet.PaginationMetadata, error) { - return nil, 0, nil, nil + ds.ListQueriesFunc = func(ctx context.Context, opts fleet.ListQueryOptions) ([]*fleet.Query, int, int, *fleet.PaginationMetadata, error) { + return nil, 0, 0, nil, nil } ds.ListTeamsFunc = func(ctx context.Context, filter fleet.TeamFilter, opt fleet.ListOptions) ([]*fleet.Team, error) { return nil, nil @@ -645,8 +645,8 @@ 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, int, *fleet.PaginationMetadata, error) { - return nil, 0, nil, nil + ds.ListQueriesFunc = func(ctx context.Context, opts fleet.ListQueryOptions) ([]*fleet.Query, int, int, *fleet.PaginationMetadata, error) { + return nil, 0, 0, nil, nil } ds.GetLabelSpecsFunc = func(ctx context.Context, filter fleet.TeamFilter) ([]*fleet.LabelSpec, error) { return nil, nil @@ -956,8 +956,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, int, *fleet.PaginationMetadata, error) { - return []*fleet.Query{&query}, 1, nil, nil + ds.ListQueriesFunc = func(ctx context.Context, opts fleet.ListQueryOptions) ([]*fleet.Query, int, int, *fleet.PaginationMetadata, error) { + return []*fleet.Query{&query}, 1, 0, nil, nil } ds.DeleteQueriesFunc = func(ctx context.Context, ids []uint) (uint, error) { queryDeleted = true @@ -1361,8 +1361,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, int, *fleet.PaginationMetadata, error) { - return []*fleet.Query{&query}, 1, nil, nil + ds.ListQueriesFunc = func(ctx context.Context, opts fleet.ListQueryOptions) ([]*fleet.Query, int, int, *fleet.PaginationMetadata, error) { + return []*fleet.Query{&query}, 1, 0, nil, nil } ds.DeleteQueriesFunc = func(ctx context.Context, ids []uint) (uint, error) { queryDeleted = true @@ -1648,8 +1648,8 @@ func TestGitOpsBasicGlobalAndTeam(t *testing.T) { } return nil, nil } - ds.ListQueriesFunc = func(ctx context.Context, opts fleet.ListQueryOptions) ([]*fleet.Query, int, *fleet.PaginationMetadata, error) { - return nil, 0, nil, nil + ds.ListQueriesFunc = func(ctx context.Context, opts fleet.ListQueryOptions) ([]*fleet.Query, int, int, *fleet.PaginationMetadata, error) { + return nil, 0, 0, nil, nil } ds.DeleteIconsAssociatedWithTitlesWithoutInstallersFunc = func(ctx context.Context, teamID uint) error { return nil @@ -2054,8 +2054,8 @@ 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, int, *fleet.PaginationMetadata, error) { - return nil, 0, nil, nil + ds.ListQueriesFunc = func(ctx context.Context, opts fleet.ListQueryOptions) ([]*fleet.Query, int, int, *fleet.PaginationMetadata, error) { + return nil, 0, 0, nil, nil } testing_utils.AddLabelMocks(ds) @@ -4528,8 +4528,8 @@ func TestGitOpsWindowsUpdates(t *testing.T) { savedTeam = newTeam return newTeam, nil } - ds.ListQueriesFunc = func(ctx context.Context, opts fleet.ListQueryOptions) ([]*fleet.Query, int, *fleet.PaginationMetadata, error) { - return nil, 0, nil, nil + ds.ListQueriesFunc = func(ctx context.Context, opts fleet.ListQueryOptions) ([]*fleet.Query, int, int, *fleet.PaginationMetadata, error) { + return nil, 0, 0, nil, nil } ds.ListGlobalPoliciesFunc = func(ctx context.Context, opts fleet.ListOptions) ([]*fleet.Policy, error) { return nil, nil @@ -4934,8 +4934,8 @@ func TestGitOpsAppStoreAppAutoUpdate(t *testing.T) { savedTeam = newTeam return newTeam, nil } - ds.ListQueriesFunc = func(ctx context.Context, opts fleet.ListQueryOptions) ([]*fleet.Query, int, *fleet.PaginationMetadata, error) { - return nil, 0, nil, nil + ds.ListQueriesFunc = func(ctx context.Context, opts fleet.ListQueryOptions) ([]*fleet.Query, int, int, *fleet.PaginationMetadata, error) { + return nil, 0, 0, nil, nil } ds.ListGlobalPoliciesFunc = func(ctx context.Context, opts fleet.ListOptions) ([]*fleet.Policy, error) { return nil, nil diff --git a/cmd/fleetctl/fleetctl/query_test.go b/cmd/fleetctl/fleetctl/query_test.go index 0edfb267f92..2cedbbf1de2 100644 --- a/cmd/fleetctl/fleetctl/query_test.go +++ b/cmd/fleetctl/fleetctl/query_test.go @@ -61,11 +61,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, int, *fleet.PaginationMetadata, error) { + ds.ListQueriesFunc = func(ctx context.Context, opt fleet.ListQueryOptions) ([]*fleet.Query, int, int, *fleet.PaginationMetadata, error) { if opt.MatchQuery == queryName { - return []*fleet.Query{&query}, 1, nil, nil + return []*fleet.Query{&query}, 1, 0, nil, nil } - return []*fleet.Query{}, 0, nil, nil + return []*fleet.Query{}, 0, 0, nil, nil } ds.NewDistributedQueryCampaignFunc = func(ctx context.Context, camp *fleet.DistributedQueryCampaign) (*fleet.DistributedQueryCampaign, error) { camp.ID = 321 diff --git a/cmd/fleetctl/fleetctl/session_test.go b/cmd/fleetctl/fleetctl/session_test.go index 83706a2b5ba..6671215c6bb 100644 --- a/cmd/fleetctl/fleetctl/session_test.go +++ b/cmd/fleetctl/fleetctl/session_test.go @@ -15,8 +15,8 @@ import ( func TestEarlySessionCheck(t *testing.T) { _, ds := testing_utils.RunServerWithMockedDS(t) - ds.ListQueriesFunc = func(ctx context.Context, opt fleet.ListQueryOptions) ([]*fleet.Query, int, *fleet.PaginationMetadata, error) { - return nil, 0, nil, nil + ds.ListQueriesFunc = func(ctx context.Context, opt fleet.ListQueryOptions) ([]*fleet.Query, int, int, *fleet.PaginationMetadata, error) { + return nil, 0, 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/fleetctl/testing_utils/testing_utils.go b/cmd/fleetctl/fleetctl/testing_utils/testing_utils.go index ddaf069d69e..b1904aca883 100644 --- a/cmd/fleetctl/fleetctl/testing_utils/testing_utils.go +++ b/cmd/fleetctl/fleetctl/testing_utils/testing_utils.go @@ -367,8 +367,8 @@ func SetupFullGitOpsPremiumServer(t *testing.T) (*mock.Store, **fleet.AppConfig, } return summary, nil } - ds.ListQueriesFunc = func(ctx context.Context, opts fleet.ListQueryOptions) ([]*fleet.Query, int, *fleet.PaginationMetadata, error) { - return nil, 0, nil, nil + ds.ListQueriesFunc = func(ctx context.Context, opts fleet.ListQueryOptions) ([]*fleet.Query, int, int, *fleet.PaginationMetadata, error) { + return nil, 0, 0, nil, nil } ds.NewActivityFunc = func( ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time, diff --git a/cmd/fleetctl/fleetctl/upgrade_packs_test.go b/cmd/fleetctl/fleetctl/upgrade_packs_test.go index d2ccd887981..47771b080d0 100644 --- a/cmd/fleetctl/fleetctl/upgrade_packs_test.go +++ b/cmd/fleetctl/fleetctl/upgrade_packs_test.go @@ -248,8 +248,8 @@ func TestFleetctlUpgradePacks_EmptyPacks(t *testing.T) { return fleet.TargetMetrics{}, nil } - ds.ListQueriesFunc = func(ctx context.Context, opt fleet.ListQueryOptions) ([]*fleet.Query, int, *fleet.PaginationMetadata, error) { - return nil, 0, nil, nil + ds.ListQueriesFunc = func(ctx context.Context, opt fleet.ListQueryOptions) ([]*fleet.Query, int, int, *fleet.PaginationMetadata, error) { + return nil, 0, 0, nil, nil } tempDir := t.TempDir() @@ -315,12 +315,12 @@ func TestFleetctlUpgradePacks_NonEmpty(t *testing.T) { return fleet.TargetMetrics{}, nil } - ds.ListQueriesFunc = func(ctx context.Context, opt fleet.ListQueryOptions) ([]*fleet.Query, int, *fleet.PaginationMetadata, error) { + ds.ListQueriesFunc = func(ctx context.Context, opt fleet.ListQueryOptions) ([]*fleet.Query, int, int, *fleet.PaginationMetadata, error) { return []*fleet.Query{ {Name: "q1", Query: "select 1"}, {Name: "q2", Query: "select 2"}, {Name: "q3", Query: "select 3"}, - }, 3, nil, nil + }, 3, 0, nil, nil } const expected = ` diff --git a/cmd/fleetctl/integrationtest/gitops/gitops_enterprise_integration_test.go b/cmd/fleetctl/integrationtest/gitops/gitops_enterprise_integration_test.go index 76aea305d00..b8313a32fb6 100644 --- a/cmd/fleetctl/integrationtest/gitops/gitops_enterprise_integration_test.go +++ b/cmd/fleetctl/integrationtest/gitops/gitops_enterprise_integration_test.go @@ -2825,7 +2825,7 @@ team_settings: require.NoError(t, err) require.NotNil(t, team) - queries, _, _, err := s.DS.ListQueries(ctx, fleet.ListQueryOptions{TeamID: &team.ID}) + queries, _, _, _, err := s.DS.ListQueries(ctx, fleet.ListQueryOptions{TeamID: &team.ID}) require.NoError(t, err) require.Len(t, queries, 1) require.Equal(t, fmt.Sprintf("query-%d", i), queries[0].Name) @@ -2872,7 +2872,7 @@ team_settings: require.NoError(t, err) require.NotNil(t, team) - queries, _, _, err := s.DS.ListQueries(ctx, fleet.ListQueryOptions{TeamID: &team.ID}) + queries, _, _, _, err := s.DS.ListQueries(ctx, fleet.ListQueryOptions{TeamID: &team.ID}) require.NoError(t, err) require.Len(t, queries, 1) require.Equal(t, fmt.Sprintf("query-%d", i), queries[0].Name) diff --git a/frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx b/frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx index 97ed7eb395c..c0e6d568186 100644 --- a/frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx +++ b/frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx @@ -38,9 +38,12 @@ import TableDataError from "components/DataError"; import MainContent from "components/MainContent"; import TeamsDropdown from "components/TeamsDropdown"; import useTeamIdParam from "hooks/useTeamIdParam"; +import TooltipWrapper from "components/TooltipWrapper"; import QueriesTable from "./components/QueriesTable"; import DeleteQueryModal from "./components/DeleteQueryModal"; -import ManageQueryAutomationsModal from "./components/ManageQueryAutomationsModal/ManageQueryAutomationsModal"; +import ManageQueryAutomationsModal, { + IQueryAutomationsSubmitData, +} from "./components/ManageQueryAutomationsModal/ManageQueryAutomationsModal"; import PreviewDataModal from "./components/PreviewDataModal/PreviewDataModal"; const DEFAULT_PAGE_SIZE = 20; @@ -169,17 +172,6 @@ const ManageQueriesPage = ({ return queriesResponse?.queries.map(enhanceQuery) || []; }, [queriesResponse]); - const queriesAvailableToAutomate = - (teamIdForApi !== API_ALL_TEAMS_ID - ? enhancedQueries?.filter( - (query: IEnhancedQuery) => query.team_id === currentTeamId - ) - : enhancedQueries) ?? []; - - const automatedQueryIds = queriesAvailableToAutomate - .filter((query) => query.automations_enabled) - .map((query) => query.id); - useEffect(() => { const path = location.pathname + location.search; if (filteredQueriesPath !== path) { @@ -287,7 +279,11 @@ const ManageQueriesPage = ({ queries={enhancedQueries || []} totalQueriesCount={queriesResponse?.count} hasNextResults={!!queriesResponse?.meta.has_next_results} - curTeamScopeQueriesPresent={!!queriesAvailableToAutomate.length} + curTeamScopeQueriesPresent={ + teamIdForApi !== API_ALL_TEAMS_ID + ? enhancedQueries.some((q) => q.team_id === currentTeamId) + : enhancedQueries.length > 0 + } isLoading={isLoadingQueries || isFetchingQueries} onDeleteQueryClick={onDeleteQueryClick} isOnlyObserver={isOnlyObserver} @@ -303,26 +299,29 @@ const ManageQueriesPage = ({ }; const onSaveQueryAutomations = useCallback( - async (newAutomatedQueryIds: any) => { + async ({ + newAutomatedQueryIds, + previousAutomatedQueryIds, + }: IQueryAutomationsSubmitData) => { setIsUpdatingAutomations(true); // Query ids added to turn on automations const turnOnAutomations = newAutomatedQueryIds.filter( - (query: number) => !automatedQueryIds.includes(query) + (id) => !previousAutomatedQueryIds.includes(id) ); // Query ids removed to turn off automations - const turnOffAutomations = automatedQueryIds.filter( - (query: number) => !newAutomatedQueryIds.includes(query) + const turnOffAutomations = previousAutomatedQueryIds.filter( + (id) => !newAutomatedQueryIds.includes(id) ); // Update query automations using queries/{id} manage_automations parameter const updateAutomatedQueries: Promise[] = []; - turnOnAutomations.map((id: number) => + turnOnAutomations.map((id) => updateAutomatedQueries.push( queriesAPI.update(id, { automations_enabled: true }) ) ); - turnOffAutomations.map((id: number) => + turnOffAutomations.map((id) => updateAutomatedQueries.push( queriesAPI.update(id, { automations_enabled: false }) ) @@ -343,12 +342,7 @@ const ManageQueriesPage = ({ setIsUpdatingAutomations(false); } }, - [ - automatedQueryIds, - renderFlash, - refetchQueries, - toggleManageAutomationsModal, - ] + [renderFlash, refetchQueries, toggleManageAutomationsModal] ); const renderModals = () => { @@ -368,8 +362,7 @@ const ManageQueriesPage = ({ onCancel={toggleManageAutomationsModal} isShowingPreviewDataModal={showPreviewDataModal} togglePreviewDataModal={togglePreviewDataModal} - availableQueries={queriesAvailableToAutomate} - automatedQueryIds={automatedQueryIds} + teamId={teamIdForApi} logDestination={config?.logging.result.plugin || ""} webhookDestination={config?.logging.result.config?.result_url} filesystemDestination={ @@ -401,6 +394,12 @@ const ManageQueriesPage = ({ dropdownHelpText = "Gather data about all hosts."; } + const canManageAutomations = isGlobalAdmin || isTeamAdmin; + const isManageAutomationsEnabled = isAnyTeamSelected + ? (queriesResponse?.count ?? 0) > + (queriesResponse?.inherited_query_count ?? 0) + : (queriesResponse?.count ?? 0) > 0; + return ( <> @@ -411,8 +410,8 @@ const ManageQueriesPage = ({ {canCustomQuery && (
- {(isGlobalAdmin || isTeamAdmin) && - !!queriesAvailableToAutomate.length && ( + {canManageAutomations && + (isManageAutomationsEnabled ? ( - )} + ) : ( + 0 + ? 'To manage automations add a query to this team. For inherited queries select "All teams".' + : "To manage automations add a query." + } + underline={false} + position="top" + showArrow + > + + + ))} {canCustomQuery && (
- {availableQueries?.length ? ( + {availableQueries.length ? (
Choose which queries will send data: diff --git a/frontend/services/entities/queries.ts b/frontend/services/entities/queries.ts index 3d2a6d1c26a..34983afe6f2 100644 --- a/frontend/services/entities/queries.ts +++ b/frontend/services/entities/queries.ts @@ -35,6 +35,7 @@ interface ICreateQueryResponse { export interface IQueriesResponse { queries: ISchedulableQuery[]; count: number; + inherited_query_count: number; meta: { has_next_results: boolean; has_previous_results: boolean; diff --git a/server/datastore/mysql/queries.go b/server/datastore/mysql/queries.go index 59e387d9302..315ad949b3b 100644 --- a/server/datastore/mysql/queries.go +++ b/server/datastore/mysql/queries.go @@ -638,7 +638,7 @@ func query(ctx context.Context, db sqlx.QueryerContext, id uint) (*fleet.Query, // ListQueries returns a list of queries with sort order and results limit // 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) { +func (ds *Datastore) ListQueries(ctx context.Context, opt fleet.ListQueryOptions) ([]*fleet.Query, int, int, *fleet.PaginationMetadata, error) { getQueriesStmt := ` SELECT q.id, @@ -704,28 +704,39 @@ func (ds *Datastore) ListQueries(ctx context.Context, opt fleet.ListQueryOptions getQueriesStmt += whereClauses // build the count statement before adding pagination constraints - getQueriesCountStmt := fmt.Sprintf("SELECT COUNT(DISTINCT id) FROM (%s) AS s", getQueriesStmt) + var getQueriesCountStmt string + if opt.TeamID != nil && opt.MergeInherited { + getQueriesCountStmt = fmt.Sprintf( + `SELECT COUNT(DISTINCT id) AS total, COUNT(DISTINCT CASE WHEN team_id IS NULL THEN id END) AS inherited FROM (%s) AS s`, + getQueriesStmt, + ) + } else { + getQueriesCountStmt = fmt.Sprintf("SELECT COUNT(DISTINCT id) AS total, 0 AS inherited 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") + return nil, 0, 0, nil, ctxerr.Wrap(ctx, err, "listing 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") + var counts struct { + Total int `db:"total"` + Inherited int `db:"inherited"` + } + if err := sqlx.GetContext(ctx, dbReader, &counts, getQueriesCountStmt, args...); err != nil { + return nil, 0, 0, nil, ctxerr.Wrap(ctx, err, "get queries count") } if err := ds.loadPacksForQueries(ctx, queries); err != nil { - return nil, 0, nil, ctxerr.Wrap(ctx, err, "loading packs for queries") + return nil, 0, 0, nil, ctxerr.Wrap(ctx, err, "loading packs for queries") } if err := ds.loadLabelsForQueries(ctx, queries); err != nil { - return nil, 0, nil, ctxerr.Wrap(ctx, err, "loading labels for queries") + return nil, 0, 0, nil, ctxerr.Wrap(ctx, err, "loading labels for queries") } var meta *fleet.PaginationMetadata @@ -739,7 +750,7 @@ func (ds *Datastore) ListQueries(ctx context.Context, opt fleet.ListQueryOptions } } - return queries, count, meta, nil + return queries, counts.Total, counts.Inherited, meta, nil } // loadPacksForQueries loads the user packs (aka 2017 packs) associated with the provided queries. diff --git a/server/datastore/mysql/queries_test.go b/server/datastore/mysql/queries_test.go index 1502c7a68c5..4f3fb03ae43 100644 --- a/server/datastore/mysql/queries_test.go +++ b/server/datastore/mysql/queries_test.go @@ -106,7 +106,7 @@ func testQueriesApply(t *testing.T, ds *Datastore) { err = ds.ApplyQueries(context.Background(), zwass.ID, expectedQueries, nil) require.NoError(t, err) - queries, count, _, err := ds.ListQueries(context.Background(), fleet.ListQueryOptions{}) + queries, count, _, _, err := ds.ListQueries(context.Background(), fleet.ListQueryOptions{}) require.NoError(t, err) require.Len(t, queries, len(expectedQueries)) require.Equal(t, count, len(expectedQueries)) @@ -127,7 +127,7 @@ func testQueriesApply(t *testing.T, ds *Datastore) { err = ds.ApplyQueries(context.Background(), zwass.ID, expectedQueries, nil) require.NoError(t, err) - queries, count, _, err = ds.ListQueries(context.Background(), fleet.ListQueryOptions{}) + queries, count, _, _, err = ds.ListQueries(context.Background(), fleet.ListQueryOptions{}) require.NoError(t, err) require.Len(t, queries, len(expectedQueries)) require.Equal(t, count, len(expectedQueries)) @@ -140,7 +140,7 @@ func testQueriesApply(t *testing.T, ds *Datastore) { err = ds.ApplyQueries(context.Background(), groob.ID, expectedQueries, nil) require.NoError(t, err) - queries, count, _, err = ds.ListQueries(context.Background(), fleet.ListQueryOptions{}) + queries, count, _, _, err = ds.ListQueries(context.Background(), fleet.ListQueryOptions{}) require.NoError(t, err) require.Len(t, queries, len(expectedQueries)) require.Equal(t, count, len(expectedQueries)) @@ -168,7 +168,7 @@ func testQueriesApply(t *testing.T, ds *Datastore) { err = ds.ApplyQueries(context.Background(), zwass.ID, []*fleet.Query{expectedQueries[2]}, nil) require.NoError(t, err) - queries, count, _, err = ds.ListQueries(context.Background(), fleet.ListQueryOptions{}) + queries, count, _, _, err = ds.ListQueries(context.Background(), fleet.ListQueryOptions{}) require.NoError(t, err) require.Len(t, queries, len(expectedQueries)) require.Equal(t, count, len(expectedQueries)) @@ -318,7 +318,7 @@ func testQueriesDeleteMany(t *testing.T, ds *Datastore) { q3 := test.NewQuery(t, ds, nil, "q3", "select 1", user.ID, true) q4 := test.NewQuery(t, ds, nil, "q4", "select * from osquery_info", user.ID, true) - queries, count, _, err := ds.ListQueries(context.Background(), fleet.ListQueryOptions{}) + queries, count, _, _, err := ds.ListQueries(context.Background(), fleet.ListQueryOptions{}) require.Nil(t, err) assert.Len(t, queries, 4) require.Equal(t, count, 4) @@ -356,7 +356,7 @@ func testQueriesDeleteMany(t *testing.T, ds *Datastore) { require.Nil(t, err) assert.Equal(t, uint(2), deleted) - queries, count, _, err = ds.ListQueries(context.Background(), fleet.ListQueryOptions{}) + queries, count, _, _, err = ds.ListQueries(context.Background(), fleet.ListQueryOptions{}) require.Nil(t, err) assert.Len(t, queries, 2) assert.Equal(t, count, 2) @@ -393,7 +393,7 @@ func testQueriesDeleteMany(t *testing.T, ds *Datastore) { require.Nil(t, err) assert.Equal(t, uint(1), deleted) - queries, count, _, err = ds.ListQueries(context.Background(), fleet.ListQueryOptions{}) + queries, count, _, _, err = ds.ListQueries(context.Background(), fleet.ListQueryOptions{}) require.Nil(t, err) assert.Len(t, queries, 1) assert.Equal(t, count, 1) @@ -402,7 +402,7 @@ func testQueriesDeleteMany(t *testing.T, ds *Datastore) { require.Nil(t, err) assert.Equal(t, uint(1), deleted) - queries, count, _, err = ds.ListQueries(context.Background(), fleet.ListQueryOptions{}) + queries, count, _, _, err = ds.ListQueries(context.Background(), fleet.ListQueryOptions{}) require.Nil(t, err) assert.Len(t, queries, 0) assert.Equal(t, count, 0) @@ -541,7 +541,7 @@ func testQueriesList(t *testing.T, ds *Datastore) { opts.Platform = ptr.String("darwin") // filtered by platform - results, count, meta, err := ds.ListQueries(context.Background(), opts) + results, count, _, meta, err := ds.ListQueries(context.Background(), opts) require.NoError(t, err) require.Equal(t, 8, len(results)) assert.Equal(t, count, 8) @@ -551,7 +551,7 @@ func testQueriesList(t *testing.T, ds *Datastore) { require.Equal(t, "darwin,windows,linux", results[1].Platform) opts.Platform = ptr.String("windows") - results, count, meta, err = ds.ListQueries(context.Background(), opts) + results, count, _, meta, err = ds.ListQueries(context.Background(), opts) require.NoError(t, err) require.Equal(t, 8, len(results)) assert.Equal(t, count, 8) @@ -561,7 +561,7 @@ func testQueriesList(t *testing.T, ds *Datastore) { require.Equal(t, "darwin,windows,linux", results[1].Platform) opts.Platform = ptr.String("linux") - results, count, meta, err = ds.ListQueries(context.Background(), opts) + results, count, _, meta, err = ds.ListQueries(context.Background(), opts) require.NoError(t, err) require.Equal(t, 8, len(results)) assert.Equal(t, count, 8) @@ -571,7 +571,7 @@ func testQueriesList(t *testing.T, ds *Datastore) { require.Equal(t, "darwin,windows,linux", results[1].Platform) opts.Platform = ptr.String("lucas") - results, count, meta, err = ds.ListQueries(context.Background(), opts) + results, count, _, meta, err = ds.ListQueries(context.Background(), opts) require.NoError(t, err) // only returns queries set to run on all platforms with platform == "" require.Equal(t, 6, len(results)) @@ -584,7 +584,7 @@ func testQueriesList(t *testing.T, ds *Datastore) { // paginated - beginning opts.PerPage = 3 opts.Page = 0 - results, count, meta, err = ds.ListQueries(context.Background(), opts) + results, count, _, meta, err = ds.ListQueries(context.Background(), opts) require.NoError(t, err) require.Equal(t, 3, len(results)) require.Equal(t, "Zach", results[0].AuthorName) @@ -596,7 +596,7 @@ func testQueriesList(t *testing.T, ds *Datastore) { // paginated - middle opts.Page = 1 - results, count, meta, err = ds.ListQueries(context.Background(), opts) + results, count, _, meta, err = ds.ListQueries(context.Background(), opts) require.NoError(t, err) require.Equal(t, 3, len(results)) require.Equal(t, "Zach", results[0].AuthorName) @@ -608,7 +608,7 @@ func testQueriesList(t *testing.T, ds *Datastore) { // paginated - end opts.Page = 3 - results, count, meta, err = ds.ListQueries(context.Background(), opts) + results, count, _, meta, err = ds.ListQueries(context.Background(), opts) require.NoError(t, err) require.Equal(t, 1, len(results)) require.Equal(t, "Zach", results[0].AuthorName) @@ -620,7 +620,7 @@ func testQueriesList(t *testing.T, ds *Datastore) { // paginated - past end opts.Page = 4 - results, count, meta, err = ds.ListQueries(context.Background(), opts) + results, count, _, meta, err = ds.ListQueries(context.Background(), opts) require.NoError(t, err) require.Equal(t, 0, len(results)) assert.Equal(t, count, 10) @@ -629,7 +629,7 @@ func testQueriesList(t *testing.T, ds *Datastore) { opts.PerPage = 0 opts.Page = 0 - results, count, meta, err = ds.ListQueries(context.Background(), opts) + results, count, _, meta, err = ds.ListQueries(context.Background(), opts) require.NoError(t, err) require.Equal(t, 10, len(results)) require.Equal(t, "Zach", results[0].AuthorName) @@ -648,7 +648,7 @@ func testQueriesList(t *testing.T, ds *Datastore) { ) require.NoError(t, err) - results, _, _, err = ds.ListQueries(context.Background(), opts) + results, _, _, _, err = ds.ListQueries(context.Background(), opts) require.NoError(t, err) require.Equal(t, 10, len(results)) @@ -856,7 +856,7 @@ func testObserverCanRunQuery(t *testing.T, ds *Datastore) { }) require.NoError(t, err) - queries, _, _, err := ds.ListQueries(context.Background(), fleet.ListQueryOptions{}) + queries, _, _, _, err := ds.ListQueries(context.Background(), fleet.ListQueryOptions{}) require.NoError(t, err) for _, q := range queries { @@ -889,7 +889,7 @@ func testListQueriesFiltersByTeamID(t *testing.T, ds *Datastore) { }) require.NoError(t, err) - queries, count, _, err := ds.ListQueries(context.Background(), fleet.ListQueryOptions{}) + queries, count, _, _, err := ds.ListQueries(context.Background(), fleet.ListQueryOptions{}) require.NoError(t, err) test.QueryElementsMatch(t, queries, []*fleet.Query{globalQ1, globalQ2, globalQ3}) assert.Equal(t, count, 3) @@ -925,7 +925,7 @@ func testListQueriesFiltersByTeamID(t *testing.T, ds *Datastore) { }) require.NoError(t, err) - queries, count, _, err = ds.ListQueries( + queries, count, _, _, err = ds.ListQueries( context.Background(), fleet.ListQueryOptions{ TeamID: &team.ID, @@ -936,7 +936,7 @@ func testListQueriesFiltersByTeamID(t *testing.T, ds *Datastore) { assert.Equal(t, count, 3) // test merge inherited - queries, count, _, err = ds.ListQueries( + queries, count, _, _, err = ds.ListQueries( context.Background(), fleet.ListQueryOptions{ TeamID: &team.ID, @@ -948,7 +948,7 @@ func testListQueriesFiltersByTeamID(t *testing.T, ds *Datastore) { assert.Equal(t, count, 6) // merge inherited ignored for global queries - queries, count, _, err = ds.ListQueries( + queries, count, _, _, err = ds.ListQueries( context.Background(), fleet.ListQueryOptions{ MergeInherited: true, @@ -1007,7 +1007,7 @@ func testListQueriesFiltersByIsScheduled(t *testing.T, ds *Datastore) { } for i, tCase := range testCases { - queries, count, _, err := ds.ListQueries( + queries, count, _, _, err := ds.ListQueries( context.Background(), tCase.opts, ) diff --git a/server/fleet/datastore.go b/server/fleet/datastore.go index ddd0d58c8d2..9b9cd205ced 100644 --- a/server/fleet/datastore.go +++ b/server/fleet/datastore.go @@ -106,9 +106,10 @@ type Datastore interface { // 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 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, int, *PaginationMetadata, error) + // options, a count of total queries on all pages, a count of inherited (global) queries, and + // pagination metadata. Associated packs should also be loaded. + // The inherited count is only computed when TeamID is set and MergeInherited is true; otherwise it is 0. + ListQueries(ctx context.Context, opt ListQueryOptions) ([]*Query, int, int, *PaginationMetadata, error) // ListScheduledQueriesForAgents returns a list of scheduled queries (without stats) for the // given teamID and hostID. If teamID is nil, then scheduled queries for the 'global' team are returned. ListScheduledQueriesForAgents(ctx context.Context, teamID *uint, hostID *uint, queryReportsDisabled bool) ([]*Query, error) diff --git a/server/fleet/service.go b/server/fleet/service.go index aad2801d6a9..ff07465ec77 100644 --- a/server/fleet/service.go +++ b/server/fleet/service.go @@ -309,8 +309,8 @@ type Service interface { // When is set to scheduled != nil, then only scheduled queries will be returned if `*scheduled == true` // and only non-scheduled queries will be returned if `*scheduled == false`. // If mergeInherited is true and a teamID is provided, then queries from the global team will be - // included in the results. - ListQueries(ctx context.Context, opt ListOptions, teamID *uint, scheduled *bool, mergeInherited bool, platform *string) ([]*Query, int, *PaginationMetadata, error) + // included in the results. The inherited count is only meaningful when mergeInherited is true. + ListQueries(ctx context.Context, opt ListOptions, teamID *uint, scheduled *bool, mergeInherited bool, platform *string) ([]*Query, int, 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 ade6812da7b..3fe060b3fc1 100644 --- a/server/mock/datastore_mock.go +++ b/server/mock/datastore_mock.go @@ -101,7 +101,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, int, *fleet.PaginationMetadata, error) +type ListQueriesFunc func(ctx context.Context, opt fleet.ListQueryOptions) ([]*fleet.Query, int, int, *fleet.PaginationMetadata, error) type ListScheduledQueriesForAgentsFunc func(ctx context.Context, teamID *uint, hostID *uint, queryReportsDisabled bool) ([]*fleet.Query, error) @@ -4680,7 +4680,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, int, *fleet.PaginationMetadata, error) { +func (s *DataStore) ListQueries(ctx context.Context, opt fleet.ListQueryOptions) ([]*fleet.Query, int, int, *fleet.PaginationMetadata, error) { s.mu.Lock() s.ListQueriesFuncInvoked = true s.mu.Unlock() diff --git a/server/mock/service/service_mock.go b/server/mock/service/service_mock.go index 594c44da75d..a1fa3ddf8f4 100644 --- a/server/mock/service/service_mock.go +++ b/server/mock/service/service_mock.go @@ -173,7 +173,7 @@ type GetQuerySpecsFunc func(ctx context.Context, teamID *uint) ([]*fleet.QuerySp type GetQuerySpecFunc func(ctx context.Context, teamID *uint, name string) (*fleet.QuerySpec, error) -type ListQueriesFunc func(ctx context.Context, opt fleet.ListOptions, teamID *uint, scheduled *bool, mergeInherited bool, platform *string) ([]*fleet.Query, int, *fleet.PaginationMetadata, error) +type ListQueriesFunc func(ctx context.Context, opt fleet.ListOptions, teamID *uint, scheduled *bool, mergeInherited bool, platform *string) ([]*fleet.Query, int, int, *fleet.PaginationMetadata, error) type GetQueryFunc func(ctx context.Context, id uint) (*fleet.Query, error) @@ -2712,7 +2712,7 @@ func (s *Service) GetQuerySpec(ctx context.Context, teamID *uint, name string) ( return s.GetQuerySpecFunc(ctx, teamID, name) } -func (s *Service) ListQueries(ctx context.Context, opt fleet.ListOptions, teamID *uint, scheduled *bool, mergeInherited bool, platform *string) ([]*fleet.Query, int, *fleet.PaginationMetadata, error) { +func (s *Service) ListQueries(ctx context.Context, opt fleet.ListOptions, teamID *uint, scheduled *bool, mergeInherited bool, platform *string) ([]*fleet.Query, int, 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 86761ba9397..8aa942bbe01 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, nil) // 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/global_schedule_test.go b/server/service/global_schedule_test.go index 5bc20d00e77..bb4a093dfa6 100644 --- a/server/service/global_schedule_test.go +++ b/server/service/global_schedule_test.go @@ -36,8 +36,8 @@ func TestGlobalScheduleAuth(t *testing.T) { ) error { return nil } - ds.ListQueriesFunc = func(ctx context.Context, opt fleet.ListQueryOptions) ([]*fleet.Query, int, *fleet.PaginationMetadata, error) { - return nil, 0, nil, nil + ds.ListQueriesFunc = func(ctx context.Context, opt fleet.ListQueryOptions) ([]*fleet.Query, int, int, *fleet.PaginationMetadata, error) { + return nil, 0, 0, nil, nil } ds.NewQueryFunc = func(ctx context.Context, query *fleet.Query, opts ...fleet.OptionalArg) (*fleet.Query, error) { return &fleet.Query{}, nil diff --git a/server/service/osquery_test.go b/server/service/osquery_test.go index 7844f4da917..71c055e45db 100644 --- a/server/service/osquery_test.go +++ b/server/service/osquery_test.go @@ -2789,8 +2789,8 @@ func TestUpdateHostIntervals(t *testing.T) { ds.ListPacksForHostFunc = func(ctx context.Context, hid uint) ([]*fleet.Pack, error) { return []*fleet.Pack{}, nil } - ds.ListQueriesFunc = func(ctx context.Context, opt fleet.ListQueryOptions) ([]*fleet.Query, int, *fleet.PaginationMetadata, error) { - return nil, 0, nil, nil + ds.ListQueriesFunc = func(ctx context.Context, opt fleet.ListQueryOptions) ([]*fleet.Query, int, int, *fleet.PaginationMetadata, error) { + return nil, 0, 0, nil, nil } testCases := []struct { diff --git a/server/service/queries.go b/server/service/queries.go index aa1b142aaff..55d041901b0 100644 --- a/server/service/queries.go +++ b/server/service/queries.go @@ -66,10 +66,11 @@ 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"` + InheritedQueryCount int `json:"inherited_query_count"` + Meta *fleet.PaginationMetadata `json:"meta"` + Err error `json:"error,omitempty"` } func (r listQueriesResponse) Error() error { return r.Err } @@ -87,7 +88,7 @@ func listQueriesEndpoint(ctx context.Context, request interface{}, svc fleet.Ser urlPlatform = &req.Platform } - queries, count, meta, err := svc.ListQueries(ctx, req.ListOptions, teamID, nil, req.MergeInherited, urlPlatform) + queries, count, inheritedCount, meta, err := svc.ListQueries(ctx, req.ListOptions, teamID, nil, req.MergeInherited, urlPlatform) if err != nil { return listQueriesResponse{Err: err}, nil } @@ -98,18 +99,19 @@ func listQueriesEndpoint(ctx context.Context, request interface{}, svc fleet.Ser } return listQueriesResponse{ - Queries: respQueries, - Count: count, - Meta: meta, + Queries: respQueries, + Count: count, + InheritedQueryCount: inheritedCount, + Meta: meta, }, nil } -func (svc *Service) ListQueries(ctx context.Context, opt fleet.ListOptions, teamID *uint, scheduled *bool, mergeInherited bool, urlPlatform *string) ([]*fleet.Query, int, *fleet.PaginationMetadata, error) { +func (svc *Service) ListQueries(ctx context.Context, opt fleet.ListOptions, teamID *uint, scheduled *bool, mergeInherited bool, urlPlatform *string) ([]*fleet.Query, int, 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, 0, nil, err + return nil, 0, 0, nil, err } // always include metadata for queries @@ -125,15 +127,15 @@ func (svc *Service) ListQueries(ctx context.Context, opt fleet.ListOptions, team dbPlatform = urlPlatform } if strings.Contains(*urlPlatform, ",") { - return nil, 0, nil, &fleet.BadRequestError{Message: "queries can only be filtered by one platform at a time"} + return nil, 0, 0, nil, &fleet.BadRequestError{Message: "queries can only be filtered by one platform at a time"} } targetableDBPlatforms := []string{"darwin", "windows", "linux"} if !slices.Contains(targetableDBPlatforms, *dbPlatform) { - return nil, 0, nil, &fleet.BadRequestError{Message: fmt.Sprintf("platform %q cannot be a scheduled query target, supported platforms are: %s", *dbPlatform, strings.Join(targetableDBPlatforms, ","))} + return nil, 0, 0, nil, &fleet.BadRequestError{Message: fmt.Sprintf("platform %q cannot be a scheduled query target, supported platforms are: %s", *dbPlatform, strings.Join(targetableDBPlatforms, ","))} } } - queries, count, meta, err := svc.ds.ListQueries(ctx, fleet.ListQueryOptions{ + queries, count, inheritedCount, meta, err := svc.ds.ListQueries(ctx, fleet.ListQueryOptions{ ListOptions: opt, TeamID: teamID, IsScheduled: scheduled, @@ -141,10 +143,10 @@ func (svc *Service) ListQueries(ctx context.Context, opt fleet.ListOptions, team Platform: dbPlatform, }) if err != nil { - return nil, 0, nil, err + return nil, 0, 0, nil, err } - return queries, count, meta, nil + return queries, count, inheritedCount, meta, nil } //////////////////////////////////////////////////////////////////////////////// @@ -967,7 +969,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, nil) + 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/queries_test.go b/server/service/queries_test.go index 9b67dd34f00..b3e4d1de437 100644 --- a/server/service/queries_test.go +++ b/server/service/queries_test.go @@ -401,8 +401,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, int, *fleet.PaginationMetadata, error) { - return nil, 0, nil, nil + ds.ListQueriesFunc = func(ctx context.Context, opts fleet.ListQueryOptions) ([]*fleet.Query, int, int, *fleet.PaginationMetadata, error) { + return nil, 0, 0, nil, nil } ds.ApplyQueriesFunc = func(ctx context.Context, authID uint, queries []*fleet.Query, queriesToDiscardResults map[uint]struct{}) error { return nil @@ -648,7 +648,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, nil) + _, _, _, _, err = svc.ListQueries(ctx, fleet.ListOptions{}, query.TeamID, nil, false, nil) checkAuthErr(t, tt.shouldFailRead, err) teamName := "" diff --git a/server/service/team_schedule.go b/server/service/team_schedule.go index 6db344afd1d..2f542ddd387 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, nil) + queries, _, _, _, err := svc.ListQueries(ctx, opts, teamID_, ptr.Bool(true), false, nil) if err != nil { return nil, err } diff --git a/server/service/team_schedule_test.go b/server/service/team_schedule_test.go index 3c504df5209..267473e3bfc 100644 --- a/server/service/team_schedule_test.go +++ b/server/service/team_schedule_test.go @@ -15,8 +15,8 @@ func TestTeamScheduleAuth(t *testing.T) { ds := new(mock.Store) svc, ctx := newTestService(t, ds, nil, nil) - ds.ListQueriesFunc = func(ctx context.Context, opt fleet.ListQueryOptions) ([]*fleet.Query, int, *fleet.PaginationMetadata, error) { - return nil, 0, nil, nil + ds.ListQueriesFunc = func(ctx context.Context, opt fleet.ListQueryOptions) ([]*fleet.Query, int, int, *fleet.PaginationMetadata, error) { + return nil, 0, 0, nil, nil } ds.QueryFunc = func(ctx context.Context, id uint) (*fleet.Query, error) { if id == 99 { // for testing modify and delete of a schedule diff --git a/server/service/testing_client.go b/server/service/testing_client.go index eca77ea85b2..3cdceb05181 100644 --- a/server/service/testing_client.go +++ b/server/service/testing_client.go @@ -172,7 +172,7 @@ func (ts *withServer) commonTearDownTest(t *testing.T) { } } - queries, _, _, err := ts.ds.ListQueries(ctx, fleet.ListQueryOptions{}) + queries, _, _, _, err := ts.ds.ListQueries(ctx, fleet.ListQueryOptions{}) require.NoError(t, err) queryIDs := make([]uint, 0, len(queries)) for _, query := range queries { From e4c67c92d56ef90f7a8829f273669ce11101cdc6 Mon Sep 17 00:00:00 2001 From: nulmete Date: Wed, 4 Feb 2026 11:54:19 -0300 Subject: [PATCH 2/5] add changes file --- changes/25080-show-manage-automations-queries-policies-pages | 1 + 1 file changed, 1 insertion(+) create mode 100644 changes/25080-show-manage-automations-queries-policies-pages diff --git a/changes/25080-show-manage-automations-queries-policies-pages b/changes/25080-show-manage-automations-queries-policies-pages new file mode 100644 index 00000000000..b5a9a16bace --- /dev/null +++ b/changes/25080-show-manage-automations-queries-policies-pages @@ -0,0 +1 @@ +- The "Manage automations" button on the queries page is now always visible, and disabled only when the current team has no queries of its own. \ No newline at end of file From 8632a895a9b0b147813a29f9bc6c2051264d83f0 Mon Sep 17 00:00:00 2001 From: nulmete Date: Wed, 4 Feb 2026 11:56:05 -0300 Subject: [PATCH 3/5] use uppercase Queries --- changes/25080-show-manage-automations-queries-policies-pages | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changes/25080-show-manage-automations-queries-policies-pages b/changes/25080-show-manage-automations-queries-policies-pages index b5a9a16bace..6a6081fcb91 100644 --- a/changes/25080-show-manage-automations-queries-policies-pages +++ b/changes/25080-show-manage-automations-queries-policies-pages @@ -1 +1 @@ -- The "Manage automations" button on the queries page is now always visible, and disabled only when the current team has no queries of its own. \ No newline at end of file +- The "Manage automations" button on the Queries page is now always visible, and disabled only when the current team has no queries of its own. \ No newline at end of file From be40423366ef128d20a901acb6b7081cd58564e2 Mon Sep 17 00:00:00 2001 From: nulmete Date: Fri, 6 Feb 2026 11:21:15 -0300 Subject: [PATCH 4/5] Lucas's feedback --- server/datastore/mysql/queries.go | 19 +++++++++---------- server/service/integration_core_test.go | 8 ++++++-- server/service/integration_enterprise_test.go | 4 ++++ 3 files changed, 19 insertions(+), 12 deletions(-) diff --git a/server/datastore/mysql/queries.go b/server/datastore/mysql/queries.go index 315ad949b3b..4c8c662d2bc 100644 --- a/server/datastore/mysql/queries.go +++ b/server/datastore/mysql/queries.go @@ -638,7 +638,7 @@ func query(ctx context.Context, db sqlx.QueryerContext, id uint) (*fleet.Query, // ListQueries returns a list of queries with sort order and results limit // 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, int, *fleet.PaginationMetadata, error) { +func (ds *Datastore) ListQueries(ctx context.Context, opt fleet.ListQueryOptions) (queries []*fleet.Query, total int, inherited int, metadata *fleet.PaginationMetadata, err error) { getQueriesStmt := ` SELECT q.id, @@ -717,8 +717,8 @@ func (ds *Datastore) ListQueries(ctx context.Context, opt fleet.ListQueryOptions 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 { + queries = []*fleet.Query{} + if err = sqlx.SelectContext(ctx, dbReader, &queries, getQueriesStmt, args...); err != nil { return nil, 0, 0, nil, ctxerr.Wrap(ctx, err, "listing queries") } @@ -727,30 +727,29 @@ func (ds *Datastore) ListQueries(ctx context.Context, opt fleet.ListQueryOptions Total int `db:"total"` Inherited int `db:"inherited"` } - if err := sqlx.GetContext(ctx, dbReader, &counts, getQueriesCountStmt, args...); err != nil { + if err = sqlx.GetContext(ctx, dbReader, &counts, getQueriesCountStmt, args...); err != nil { return nil, 0, 0, nil, ctxerr.Wrap(ctx, err, "get queries count") } - if err := ds.loadPacksForQueries(ctx, queries); err != nil { + if err = ds.loadPacksForQueries(ctx, queries); err != nil { return nil, 0, 0, nil, ctxerr.Wrap(ctx, err, "loading packs for queries") } - if err := ds.loadLabelsForQueries(ctx, queries); err != nil { + if err = ds.loadLabelsForQueries(ctx, queries); err != nil { return nil, 0, 0, nil, ctxerr.Wrap(ctx, err, "loading labels for queries") } - var meta *fleet.PaginationMetadata if opt.ListOptions.IncludeMetadata { - meta = &fleet.PaginationMetadata{HasPreviousResults: opt.ListOptions.Page > 0} + metadata = &fleet.PaginationMetadata{HasPreviousResults: opt.ListOptions.Page > 0} // `appendListOptionsWithCursorToSQL` used above to build the query statement will cause this // discrepancy if len(queries) > int(opt.ListOptions.PerPage) { //nolint:gosec // dismiss G115 - meta.HasNextResults = true + metadata.HasNextResults = true queries = queries[:len(queries)-1] } } - return queries, counts.Total, counts.Inherited, meta, nil + return queries, counts.Total, counts.Inherited, metadata, nil } // loadPacksForQueries loads the user packs (aka 2017 packs) associated with the provided queries. diff --git a/server/service/integration_core_test.go b/server/service/integration_core_test.go index 1a080b25fd2..71e5da002a3 100644 --- a/server/service/integration_core_test.go +++ b/server/service/integration_core_test.go @@ -3644,7 +3644,8 @@ func (s *integrationTestSuite) TestScheduledQueries() { var listQryResp listQueriesResponse s.DoJSON("GET", "/api/latest/fleet/queries", nil, http.StatusOK, &listQryResp) assert.Len(t, listQryResp.Queries, 0) - assert.Equal(t, listQryResp.Count, 0) + assert.Equal(t, 0, listQryResp.Count) + assert.Equal(t, 0, listQryResp.InheritedQueryCount) // create a query var createQueryResp createQueryResponse @@ -3659,6 +3660,8 @@ func (s *integrationTestSuite) TestScheduledQueries() { s.DoJSON("GET", "/api/latest/fleet/queries", nil, http.StatusOK, &listQryResp) require.Len(t, listQryResp.Queries, 1) assert.Equal(t, query.Name, listQryResp.Queries[0].Name) + assert.Equal(t, 1, listQryResp.Count) + assert.Equal(t, 0, listQryResp.InheritedQueryCount) // listing with matching name returns that query s.DoJSON("GET", "/api/latest/fleet/queries", nil, http.StatusOK, &listQryResp, "query", query.Name) @@ -3682,7 +3685,8 @@ func (s *integrationTestSuite) TestScheduledQueries() { // next page returns nothing, count and meta are correct s.DoJSON("GET", "/api/latest/fleet/queries", nil, http.StatusOK, &listQryResp, "per_page", "2", "page", "1") require.Len(t, listQryResp.Queries, 0) - require.Equal(t, listQryResp.Count, 1) + require.Equal(t, 1, listQryResp.Count) + require.Equal(t, 0, listQryResp.InheritedQueryCount) require.True(t, listQryResp.Meta.HasPreviousResults) require.False(t, listQryResp.Meta.HasNextResults) diff --git a/server/service/integration_enterprise_test.go b/server/service/integration_enterprise_test.go index 09d404c363e..efe589bae98 100644 --- a/server/service/integration_enterprise_test.go +++ b/server/service/integration_enterprise_test.go @@ -1485,12 +1485,16 @@ func (s *integrationEnterpriseTestSuite) TestTeamQueries() { s.DoJSON("GET", "/api/latest/fleet/queries", nil, http.StatusOK, &listQueriesResp, "team_id", fmt.Sprint(team1.ID)) require.Len(t, listQueriesResp.Queries, 1) assert.Equal(t, "team1", listQueriesResp.Queries[0].Name) + assert.Equal(t, 1, listQueriesResp.Count) + assert.Equal(t, 0, listQueriesResp.InheritedQueryCount) // list merged team queries s.DoJSON("GET", "/api/latest/fleet/queries", nil, http.StatusOK, &listQueriesResp, "team_id", fmt.Sprint(team1.ID), "merge_inherited", "true", "order_key", "team_id", "order_direction", "desc") require.Len(t, listQueriesResp.Queries, 2) assert.Equal(t, "team1", listQueriesResp.Queries[0].Name) assert.Equal(t, "global1", listQueriesResp.Queries[1].Name) + assert.Equal(t, 2, listQueriesResp.Count) + assert.Equal(t, 1, listQueriesResp.InheritedQueryCount) } func (s *integrationEnterpriseTestSuite) TestModifyTeamEnrollSecrets() { From 33d10e4cfb3e1f72a2cdc3beae7368d1010aebe9 Mon Sep 17 00:00:00 2001 From: nulmete Date: Fri, 6 Feb 2026 15:34:41 -0300 Subject: [PATCH 5/5] Sarah's feedback --- .../ManageQueryAutomationsModal.tsx | 50 +++++-------------- 1 file changed, 13 insertions(+), 37 deletions(-) diff --git a/frontend/pages/queries/ManageQueriesPage/components/ManageQueryAutomationsModal/ManageQueryAutomationsModal.tsx b/frontend/pages/queries/ManageQueriesPage/components/ManageQueryAutomationsModal/ManageQueryAutomationsModal.tsx index 675dafb5029..ab8815db129 100644 --- a/frontend/pages/queries/ManageQueriesPage/components/ManageQueryAutomationsModal/ManageQueryAutomationsModal.tsx +++ b/frontend/pages/queries/ManageQueriesPage/components/ManageQueryAutomationsModal/ManageQueryAutomationsModal.tsx @@ -3,10 +3,7 @@ import { useQuery } from "react-query"; import { AppContext } from "context/app"; -import { - IQueryKeyQueriesLoadAll, - ISchedulableQuery, -} from "interfaces/schedulable_query"; +import { IQueryKeyQueriesLoadAll } from "interfaces/schedulable_query"; import { LogDestination } from "interfaces/config"; import queriesAPI, { IQueriesResponse } from "services/entities/queries"; @@ -43,30 +40,6 @@ interface ICheckedQuery { interval: number; } -const useCheckboxListStateManagement = ( - allQueries: ISchedulableQuery[], - automatedQueryIds: number[] | undefined -) => { - const [queryItems, setQueryItems] = useState(() => { - return allQueries.map(({ name, id, interval }) => ({ - name, - id, - isChecked: !!automatedQueryIds?.includes(id), - interval, - })); - }); - - const updateQueryItems = (queryId: number) => { - setQueryItems((prevItems) => - prevItems.map((query) => - query.id !== queryId ? query : { ...query, isChecked: !query.isChecked } - ) - ); - }; - - return { queryItems, setQueryItems, updateQueryItems }; -}; - const baseClass = "manage-query-automations-modal"; const ManageQueryAutomationsModal = ({ @@ -127,26 +100,29 @@ const ManageQueryAutomationsModal = ({ [availableQueries] ); - const { - queryItems, - setQueryItems, - updateQueryItems, - } = useCheckboxListStateManagement(sortedAvailableQueries, automatedQueryIds); + const [queryItems, setQueryItems] = useState([]); - // Sync queryItems when the async fetch completes (the useState initializer - // only runs once on mount, when availableQueries is still empty). + // Sync queryItems when the async fetch completes. useEffect(() => { if (sortedAvailableQueries.length > 0) { setQueryItems( sortedAvailableQueries.map(({ name, id, interval }) => ({ name, id, - isChecked: automatedQueryIds.includes(id), + isChecked: !!automatedQueryIds?.includes(id), interval, })) ); } - }, [sortedAvailableQueries, automatedQueryIds, setQueryItems]); + }, [sortedAvailableQueries, automatedQueryIds]); + + const updateQueryItems = (queryId: number) => { + setQueryItems((prevItems) => + prevItems.map((query) => + query.id !== queryId ? query : { ...query, isChecked: !query.isChecked } + ) + ); + }; const onSubmitQueryAutomations = ( evt: React.MouseEvent | KeyboardEvent