fleet/server/service/team_schedule.go
Nico e5849ee720
Show Manage Automations disabled button with tooltip on Queries page (#39302)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #39303 (child of #25080).

- Added `inherited_query_count` to `ListQueriesResponse` (thought of
adding a brand new endpoint just for counting, but felt like extending
the current one was good enough). In the parent task, [it was
suggested](https://github.com/fleetdm/fleet/issues/25080#issuecomment-3326071574)
to `"Depend on team list entity endpoint's count field / team entity
count endpoint for whether or not to disable the manage automations
button"`, which Rachael approved, so I went for this approach.
- The `ManageQueryAutomationsModal` now fetches its own data with
`merge_inherited = false` (meaning it only fetches non-inherited queries
only). Previously, queries were passed down as props to it, which would
not show the queries available to automate if the first page of queries
were all inherited and the second page contained queries for that team
(the user would have to navigate to the second page for the button to be
enabled).


^ The fact that the modal fetches its own data is similar behavior to
what is currently done in `Policies`. For queries, I noticed that we
would need to add pagination within the `Manage Automations` modal, but
that can be a follow-up.

<img width="2480" height="1309" alt="Screenshot 2026-02-04 at 11 48
42 AM"
src="https://github.com/user-attachments/assets/ebac79a5-a793-4708-9313-d9a697dfd7de"
/>


# Checklist for submitter

- [x] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files)
for more information.

## Testing

- [x] QA'd all new/changed functionality manually



https://github.com/user-attachments/assets/119f03b9-dde1-4bb9-9fee-6204b1a58879
2026-02-09 15:16:28 -03:00

200 lines
6.6 KiB
Go

package service
import (
"context"
"fmt"
"time"
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/ptr"
"gopkg.in/guregu/null.v3"
)
/////////////////////////////////////////////////////////////////////////////////
// Get Scheduled Queries of a team.
/////////////////////////////////////////////////////////////////////////////////
type getTeamScheduleRequest struct {
TeamID uint `url:"team_id"`
ListOptions fleet.ListOptions `url:"list_options"`
}
type getTeamScheduleResponse struct {
Scheduled []scheduledQueryResponse `json:"scheduled"`
Err error `json:"error,omitempty"`
}
func (r getTeamScheduleResponse) Error() error { return r.Err }
func getTeamScheduleEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) {
req := request.(*getTeamScheduleRequest)
resp := getTeamScheduleResponse{Scheduled: []scheduledQueryResponse{}}
queries, err := svc.GetTeamScheduledQueries(ctx, req.TeamID, req.ListOptions)
if err != nil {
return getTeamScheduleResponse{Err: err}, nil
}
for _, q := range queries {
resp.Scheduled = append(resp.Scheduled, scheduledQueryResponse{
ScheduledQuery: *q,
})
}
return resp, nil
}
func (svc Service) GetTeamScheduledQueries(ctx context.Context, teamID uint, opts fleet.ListOptions) ([]*fleet.ScheduledQuery, error) {
var teamID_ *uint
if teamID != 0 {
teamID_ = &teamID
}
queries, _, _, _, err := svc.ListQueries(ctx, opts, teamID_, ptr.Bool(true), false, nil)
if err != nil {
return nil, err
}
scheduledQueries := make([]*fleet.ScheduledQuery, 0, len(queries))
for _, query := range queries {
scheduledQueries = append(scheduledQueries, fleet.ScheduledQueryFromQuery(query))
}
return scheduledQueries, nil
}
/////////////////////////////////////////////////////////////////////////////////
// Add schedule query to a team.
/////////////////////////////////////////////////////////////////////////////////
type teamScheduleQueryRequest struct {
TeamID uint `url:"team_id"`
fleet.ScheduledQueryPayload
}
type teamScheduleQueryResponse struct {
Scheduled *fleet.ScheduledQuery `json:"scheduled,omitempty"`
Err error `json:"error,omitempty"`
}
func (r teamScheduleQueryResponse) Error() error { return r.Err }
func uintValueOrZero(v *uint) uint {
if v == nil {
return 0
}
return *v
}
func nullIntToPtrUint(v *null.Int) *uint {
if v == nil {
return nil
}
return ptr.Uint(uint(v.ValueOrZero())) //nolint:gosec // dismiss G115
}
func teamScheduleQueryEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) {
req := request.(*teamScheduleQueryRequest)
resp, err := svc.TeamScheduleQuery(ctx, req.TeamID, &fleet.ScheduledQuery{
QueryID: uintValueOrZero(req.QueryID),
Interval: uintValueOrZero(req.Interval),
Snapshot: req.Snapshot,
Removed: req.Removed,
Platform: req.Platform,
Version: req.Version,
Shard: nullIntToPtrUint(req.Shard),
})
if err != nil {
return teamScheduleQueryResponse{Err: err}, nil
}
return teamScheduleQueryResponse{
Scheduled: resp,
}, nil
}
func nameForCopiedQuery(originalName string) string {
return "Copy of " + originalName + " (" + fmt.Sprintf("%d", time.Now().Unix()) + ")"
}
func (svc Service) TeamScheduleQuery(ctx context.Context, teamID uint, scheduledQuery *fleet.ScheduledQuery) (*fleet.ScheduledQuery, error) {
originalQuery, err := svc.ds.Query(ctx, scheduledQuery.QueryID)
if err != nil {
setAuthCheckedOnPreAuthErr(ctx)
return nil, ctxerr.Wrap(ctx, err, "get query from id")
}
if originalQuery.TeamID != nil {
setAuthCheckedOnPreAuthErr(ctx)
return nil, ctxerr.New(ctx, "cannot create a team schedule from a team query")
}
originalQuery.Name = nameForCopiedQuery(originalQuery.Name)
originalQuery.TeamID = &teamID
newQuery, err := svc.NewQuery(ctx, fleet.ScheduledQueryToQueryPayloadForNewQuery(originalQuery, scheduledQuery))
if err != nil {
return nil, err
}
return fleet.ScheduledQueryFromQuery(newQuery), nil
}
/////////////////////////////////////////////////////////////////////////////////
// Modify team scheduled query.
/////////////////////////////////////////////////////////////////////////////////
type modifyTeamScheduleRequest struct {
TeamID uint `url:"team_id"`
ScheduledQueryID uint `url:"scheduled_query_id"`
fleet.ScheduledQueryPayload
}
type modifyTeamScheduleResponse struct {
Scheduled *fleet.ScheduledQuery `json:"scheduled,omitempty"`
Err error `json:"error,omitempty"`
}
func (r modifyTeamScheduleResponse) Error() error { return r.Err }
func modifyTeamScheduleEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) {
req := request.(*modifyTeamScheduleRequest)
if _, err := svc.ModifyTeamScheduledQueries(ctx, req.TeamID, req.ScheduledQueryID, req.ScheduledQueryPayload); err != nil {
return modifyTeamScheduleResponse{Err: err}, nil
}
return modifyTeamScheduleResponse{}, nil
}
// teamID is not used because of mismatch between old internal representation and API.
func (svc Service) ModifyTeamScheduledQueries(
ctx context.Context,
teamID uint,
scheduledQueryID uint,
scheduledQueryPayload fleet.ScheduledQueryPayload,
) (*fleet.ScheduledQuery, error) {
query, err := svc.ModifyQuery(ctx, scheduledQueryID, fleet.ScheduledQueryPayloadToQueryPayloadForModifyQuery(scheduledQueryPayload))
if err != nil {
return nil, err
}
return fleet.ScheduledQueryFromQuery(query), nil
}
/////////////////////////////////////////////////////////////////////////////////
// Delete a scheduled query from a team.
/////////////////////////////////////////////////////////////////////////////////
type deleteTeamScheduleRequest struct {
TeamID uint `url:"team_id"`
ScheduledQueryID uint `url:"scheduled_query_id"`
}
type deleteTeamScheduleResponse struct {
Scheduled *fleet.ScheduledQuery `json:"scheduled,omitempty"`
Err error `json:"error,omitempty"`
}
func (r deleteTeamScheduleResponse) Error() error { return r.Err }
func deleteTeamScheduleEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) {
req := request.(*deleteTeamScheduleRequest)
err := svc.DeleteTeamScheduledQueries(ctx, req.TeamID, req.ScheduledQueryID)
if err != nil {
return deleteTeamScheduleResponse{Err: err}, nil
}
return deleteTeamScheduleResponse{}, nil
}
// teamID is not used because of mismatch between old internal representation and API.
func (svc Service) DeleteTeamScheduledQueries(ctx context.Context, teamID uint, scheduledQueryID uint) error {
return svc.DeleteQueryByID(ctx, scheduledQueryID)
}