diff --git a/internal/alerts/facets.go b/internal/alerts/facets.go new file mode 100644 index 000000000..0651c2b8c --- /dev/null +++ b/internal/alerts/facets.go @@ -0,0 +1,44 @@ +package alerts + +import ( + "context" + "slices" + "strings" + + "github.com/nais/api/internal/graph/model" +) + +func (f *AlertFacets) Filtered(ctx context.Context) []Alert { + f.filteredOnce.Do(func() { + f.filteredAlerts = SortFilter.Filter(ctx, f.AllAlerts, f.Filter) + }) + return f.filteredAlerts +} + +func (f *AlertFacets) Environments(ctx context.Context) []model.StringFacetItem { + filtered := f.Filtered(ctx) + return model.ComputeEnvironmentsFacet(f.AllAlerts, filtered, func(a Alert) string { + return a.GetEnvironmentName() + }) +} + +func (f *AlertFacets) States(ctx context.Context) []AlertStateFacetItem { + stateCounts := map[AlertState]int{} + for _, a := range f.AllAlerts { + if _, ok := stateCounts[a.GetState()]; !ok { + stateCounts[a.GetState()] = 0 + } + } + for _, a := range f.Filtered(ctx) { + stateCounts[a.GetState()]++ + } + + items := make([]AlertStateFacetItem, 0, len(stateCounts)) + for state, count := range stateCounts { + items = append(items, AlertStateFacetItem{State: state, Count: count}) + } + slices.SortFunc(items, func(a, b AlertStateFacetItem) int { + return strings.Compare(a.State.String(), b.State.String()) + }) + return items +} diff --git a/internal/alerts/facets_test.go b/internal/alerts/facets_test.go new file mode 100644 index 000000000..df40296f3 --- /dev/null +++ b/internal/alerts/facets_test.go @@ -0,0 +1,137 @@ +package alerts + +import ( + "context" + "reflect" + "testing" + + "github.com/nais/api/internal/graph/model" + "github.com/nais/api/internal/slug" +) + +func makeAlert(name, env string, state AlertState) Alert { + return &PrometheusAlert{ + BaseAlert: BaseAlert{ + Name: name, + State: state, + TeamSlug: slug.Slug("myteam"), + EnvironmentName: env, + }, + } +} + +func TestAlertFacets_Environments(t *testing.T) { + ctx := context.Background() + + all := []Alert{ + makeAlert("a1", "prod", AlertStateFiring), + makeAlert("a2", "prod", AlertStateInactive), + makeAlert("a3", "dev", AlertStateFiring), + makeAlert("a4", "dev", AlertStatePending), + } + + tests := []struct { + name string + filter *TeamAlertsFilter + want []model.StringFacetItem + }{ + { + name: "no filter: all counts match totals", + filter: nil, + want: []model.StringFacetItem{ + {Value: "dev", Count: 2}, + {Value: "prod", Count: 2}, + }, + }, + { + name: "filter by environment: other env count is zero but still present", + filter: &TeamAlertsFilter{Environments: []string{"prod"}}, + want: []model.StringFacetItem{ + {Value: "dev", Count: 0}, + {Value: "prod", Count: 2}, + }, + }, + { + name: "filter by state: env counts reflect matching alerts", + filter: &TeamAlertsFilter{States: []AlertState{AlertStateFiring}}, + want: []model.StringFacetItem{ + {Value: "dev", Count: 1}, + {Value: "prod", Count: 1}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f := &AlertFacets{AllAlerts: all, Filter: tt.filter} + got := f.Environments(ctx) + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("Environments =\n %v\nwant\n %v", got, tt.want) + } + }) + } +} + +func TestAlertFacets_States(t *testing.T) { + ctx := context.Background() + + all := []Alert{ + makeAlert("a1", "prod", AlertStateFiring), + makeAlert("a2", "prod", AlertStateInactive), + makeAlert("a3", "dev", AlertStateFiring), + makeAlert("a4", "dev", AlertStatePending), + } + + tests := []struct { + name string + filter *TeamAlertsFilter + want []AlertStateFacetItem + }{ + { + name: "no filter: all states present with full counts", + filter: nil, + want: []AlertStateFacetItem{ + {State: AlertStateFiring, Count: 2}, + {State: AlertStateInactive, Count: 1}, + {State: AlertStatePending, Count: 1}, + }, + }, + { + name: "filter by state: non-matching states have count 0", + filter: &TeamAlertsFilter{States: []AlertState{AlertStateFiring}}, + want: []AlertStateFacetItem{ + {State: AlertStateFiring, Count: 2}, + {State: AlertStateInactive, Count: 0}, + {State: AlertStatePending, Count: 0}, + }, + }, + { + name: "filter by environment: state counts reflect only matching env", + filter: &TeamAlertsFilter{Environments: []string{"prod"}}, + want: []AlertStateFacetItem{ + {State: AlertStateFiring, Count: 1}, + {State: AlertStateInactive, Count: 1}, + {State: AlertStatePending, Count: 0}, + }, + }, + { + name: "empty alerts: no facet items", + filter: nil, + want: []AlertStateFacetItem{}, + }, + } + + for i, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + alertsSlice := all + if i == len(tests)-1 { + alertsSlice = nil + } + f := &AlertFacets{AllAlerts: alertsSlice, Filter: tt.filter} + got := f.States(ctx) + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("States =\n %v\nwant\n %v", got, tt.want) + } + }) + } +} diff --git a/internal/alerts/models.go b/internal/alerts/models.go index b5ffc9a13..03254c7f1 100644 --- a/internal/alerts/models.go +++ b/internal/alerts/models.go @@ -6,6 +6,7 @@ import ( "fmt" "io" "strconv" + "sync" "time" "github.com/nais/api/internal/graph/ident" @@ -15,10 +16,22 @@ import ( ) type ( - AlertConnection = pagination.Connection[Alert] + AlertConnection = pagination.FacetableConnection[Alert, *TeamAlertsFilter] AlertEdge = pagination.Edge[Alert] ) +type AlertFacets struct { + AllAlerts []Alert + Filter *TeamAlertsFilter + filteredOnce sync.Once + filteredAlerts []Alert +} + +type AlertStateFacetItem struct { + State AlertState `json:"state"` + Count int `json:"count"` +} + type Alert interface { model.Node GetName() string diff --git a/internal/graph/alerts.resolvers.go b/internal/graph/alerts.resolvers.go index abf575b58..6b45563fc 100644 --- a/internal/graph/alerts.resolvers.go +++ b/internal/graph/alerts.resolvers.go @@ -11,6 +11,13 @@ import ( "github.com/nais/api/internal/team" ) +func (r *alertConnectionResolver) Facets(ctx context.Context, obj *pagination.FacetableConnection[alerts.Alert, *alerts.TeamAlertsFilter]) (*alerts.AlertFacets, error) { + return &alerts.AlertFacets{ + AllAlerts: obj.GetAllItems(), + Filter: obj.GetFilter(), + }, nil +} + func (r *prometheusAlertResolver) Team(ctx context.Context, obj *alerts.PrometheusAlert) (*team.Team, error) { team, err := team.Get(ctx, obj.TeamSlug) if err != nil { @@ -24,7 +31,7 @@ func (r *prometheusAlertResolver) TeamEnvironment(ctx context.Context, obj *aler return team.GetTeamEnvironment(ctx, obj.TeamSlug, obj.EnvironmentName) } -func (r *teamResolver) Alerts(ctx context.Context, obj *team.Team, first *int, after *pagination.Cursor, last *int, before *pagination.Cursor, orderBy *alerts.AlertOrder, filter *alerts.TeamAlertsFilter) (*pagination.Connection[alerts.Alert], error) { +func (r *teamResolver) Alerts(ctx context.Context, obj *team.Team, first *int, after *pagination.Cursor, last *int, before *pagination.Cursor, orderBy *alerts.AlertOrder, filter *alerts.TeamAlertsFilter) (*pagination.FacetableConnection[alerts.Alert, *alerts.TeamAlertsFilter], error) { page, err := pagination.ParsePage(first, after, last, before) if err != nil { return nil, err @@ -40,20 +47,13 @@ func (r *teamResolver) Alerts(ctx context.Context, obj *team.Team, first *int, a a = append(a, alert) } - filtered := alerts.SortFilter.Filter(ctx, a, filter) if orderBy == nil { - orderBy = &alerts.AlertOrder{ - Field: "NAME", - Direction: model.OrderDirectionAsc, - } + orderBy = &alerts.AlertOrder{Field: "NAME", Direction: model.OrderDirectionAsc} } - alerts.SortFilter.Sort(ctx, filtered, orderBy.Field, orderBy.Direction) - - ret := pagination.Slice(filtered, page) - return pagination.NewConnection(ret, page, len(filtered)), nil + return alerts.SortFilter.PaginatedList(ctx, a, page, orderBy.Field, orderBy.Direction, filter), nil } -func (r *teamEnvironmentResolver) Alerts(ctx context.Context, obj *team.TeamEnvironment, first *int, after *pagination.Cursor, last *int, before *pagination.Cursor, orderBy *alerts.AlertOrder, filter *alerts.TeamAlertsFilter) (*pagination.Connection[alerts.Alert], error) { +func (r *teamEnvironmentResolver) Alerts(ctx context.Context, obj *team.TeamEnvironment, first *int, after *pagination.Cursor, last *int, before *pagination.Cursor, orderBy *alerts.AlertOrder, filter *alerts.TeamAlertsFilter) (*pagination.FacetableConnection[alerts.Alert, *alerts.TeamAlertsFilter], error) { page, err := pagination.ParsePage(first, after, last, before) if err != nil { return nil, err @@ -69,21 +69,21 @@ func (r *teamEnvironmentResolver) Alerts(ctx context.Context, obj *team.TeamEnvi a = append(a, alert) } - filtered := alerts.SortFilter.Filter(ctx, a, filter) if orderBy == nil { - orderBy = &alerts.AlertOrder{ - Field: "NAME", - Direction: model.OrderDirectionAsc, - } + orderBy = &alerts.AlertOrder{Field: "NAME", Direction: model.OrderDirectionAsc} } - alerts.SortFilter.Sort(ctx, filtered, orderBy.Field, orderBy.Direction) + return alerts.SortFilter.PaginatedList(ctx, a, page, orderBy.Field, orderBy.Direction, filter), nil +} - ret := pagination.Slice(filtered, page) - return pagination.NewConnection(ret, page, len(filtered)), nil +func (r *Resolver) AlertConnection() gengql.AlertConnectionResolver { + return &alertConnectionResolver{r} } func (r *Resolver) PrometheusAlert() gengql.PrometheusAlertResolver { return &prometheusAlertResolver{r} } -type prometheusAlertResolver struct{ *Resolver } +type ( + alertConnectionResolver struct{ *Resolver } + prometheusAlertResolver struct{ *Resolver } +) diff --git a/internal/graph/applications.resolvers.go b/internal/graph/applications.resolvers.go index 2040f038f..e4461d19f 100644 --- a/internal/graph/applications.resolvers.go +++ b/internal/graph/applications.resolvers.go @@ -75,24 +75,23 @@ func (r *applicationResolver) State(ctx context.Context, obj *application.Applic return application.GetState(ctx, obj) } -func (r *applicationResolver) Issues(ctx context.Context, obj *application.Application, first *int, after *pagination.Cursor, last *int, before *pagination.Cursor, orderBy *issue.IssueOrder, filter *issue.ResourceIssueFilter) (*pagination.Connection[issue.Issue], error) { +func (r *applicationResolver) Issues(ctx context.Context, obj *application.Application, first *int, after *pagination.Cursor, last *int, before *pagination.Cursor, orderBy *issue.IssueOrder, filter *issue.ResourceIssueFilter) (*issue.IssueConnection, error) { page, err := pagination.ParsePage(first, after, last, before) if err != nil { return nil, err } - t := issue.ResourceTypeApplication - f := &issue.IssueFilter{ - ResourceName: &obj.Name, - ResourceType: &t, - Environments: []string{obj.EnvironmentName}, + scope := &issue.IssueScope{ + ResourceName: obj.Name, + ResourceType: issue.ResourceTypeApplication, + Env: obj.EnvironmentName, } + var f *issue.IssueFilter if filter != nil { - f.Severity = filter.Severity - f.IssueType = filter.IssueType + f = &issue.IssueFilter{ResourceIssueFilter: issue.ResourceIssueFilter{Severity: filter.Severity, IssueType: filter.IssueType}} } - return issue.ListIssues(ctx, obj.TeamSlug, page, orderBy, f) + return issue.ListIssues(ctx, obj.TeamSlug, page, orderBy, scope, f) } func (r *applicationResolver) History(ctx context.Context, obj *application.Application) ([]*instancegroup.ApplicationHistory, error) { diff --git a/internal/graph/gengql/alerts.generated.go b/internal/graph/gengql/alerts.generated.go index 56f5edbfa..3a1970697 100644 --- a/internal/graph/gengql/alerts.generated.go +++ b/internal/graph/gengql/alerts.generated.go @@ -14,6 +14,7 @@ import ( "github.com/99designs/gqlgen/graphql" "github.com/nais/api/internal/alerts" "github.com/nais/api/internal/graph/ident" + "github.com/nais/api/internal/graph/model" "github.com/nais/api/internal/graph/pagination" "github.com/nais/api/internal/team" "github.com/vektah/gqlparser/v2/ast" @@ -21,6 +22,9 @@ import ( // region ************************** generated!.gotpl ************************** +type AlertConnectionResolver interface { + Facets(ctx context.Context, obj *pagination.FacetableConnection[alerts.Alert, *alerts.TeamAlertsFilter]) (*alerts.AlertFacets, error) +} type PrometheusAlertResolver interface { Team(ctx context.Context, obj *alerts.PrometheusAlert) (*team.Team, error) TeamEnvironment(ctx context.Context, obj *alerts.PrometheusAlert) (*team.TeamEnvironment, error) @@ -38,7 +42,7 @@ type PrometheusAlertResolver interface { // region **************************** field.gotpl ***************************** -func (ec *executionContext) _AlertConnection_pageInfo(ctx context.Context, field graphql.CollectedField, obj *pagination.Connection[alerts.Alert]) (ret graphql.Marshaler) { +func (ec *executionContext) _AlertConnection_pageInfo(ctx context.Context, field graphql.CollectedField, obj *pagination.FacetableConnection[alerts.Alert, *alerts.TeamAlertsFilter]) (ret graphql.Marshaler) { return graphql.ResolveField( ctx, ec.OperationContext, @@ -70,7 +74,7 @@ func (ec *executionContext) fieldContext_AlertConnection_pageInfo(_ context.Cont return fc, nil } -func (ec *executionContext) _AlertConnection_nodes(ctx context.Context, field graphql.CollectedField, obj *pagination.Connection[alerts.Alert]) (ret graphql.Marshaler) { +func (ec *executionContext) _AlertConnection_nodes(ctx context.Context, field graphql.CollectedField, obj *pagination.FacetableConnection[alerts.Alert, *alerts.TeamAlertsFilter]) (ret graphql.Marshaler) { return graphql.ResolveField( ctx, ec.OperationContext, @@ -102,7 +106,7 @@ func (ec *executionContext) fieldContext_AlertConnection_nodes(_ context.Context return fc, nil } -func (ec *executionContext) _AlertConnection_edges(ctx context.Context, field graphql.CollectedField, obj *pagination.Connection[alerts.Alert]) (ret graphql.Marshaler) { +func (ec *executionContext) _AlertConnection_edges(ctx context.Context, field graphql.CollectedField, obj *pagination.FacetableConnection[alerts.Alert, *alerts.TeamAlertsFilter]) (ret graphql.Marshaler) { return graphql.ResolveField( ctx, ec.OperationContext, @@ -134,6 +138,38 @@ func (ec *executionContext) fieldContext_AlertConnection_edges(_ context.Context return fc, nil } +func (ec *executionContext) _AlertConnection_facets(ctx context.Context, field graphql.CollectedField, obj *pagination.FacetableConnection[alerts.Alert, *alerts.TeamAlertsFilter]) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_AlertConnection_facets(ctx, field) + }, + func(ctx context.Context) (any, error) { + return ec.Resolvers.AlertConnection().Facets(ctx, obj) + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *alerts.AlertFacets) graphql.Marshaler { + return ec.marshalOAlertFacets2ᚖgithubᚗcomᚋnaisᚋapiᚋinternalᚋalertsᚐAlertFacets(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext_AlertConnection_facets(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "AlertConnection", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields_AlertFacets(ctx, field) + }, + } + return fc, nil +} + func (ec *executionContext) _AlertEdge_cursor(ctx context.Context, field graphql.CollectedField, obj *pagination.Edge[alerts.Alert]) (ret graphql.Marshaler) { return graphql.ResolveField( ctx, @@ -189,6 +225,116 @@ func (ec *executionContext) fieldContext_AlertEdge_node(_ context.Context, field return fc, nil } +func (ec *executionContext) _AlertFacets_environments(ctx context.Context, field graphql.CollectedField, obj *alerts.AlertFacets) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_AlertFacets_environments(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Environments(ctx), nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v []model.StringFacetItem) graphql.Marshaler { + return ec.marshalNStringFacetItem2ᚕgithubᚗcomᚋnaisᚋapiᚋinternalᚋgraphᚋmodelᚐStringFacetItemᚄ(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_AlertFacets_environments(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "AlertFacets", + Field: field, + IsMethod: true, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields_StringFacetItem(ctx, field) + }, + } + return fc, nil +} + +func (ec *executionContext) _AlertFacets_states(ctx context.Context, field graphql.CollectedField, obj *alerts.AlertFacets) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_AlertFacets_states(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.States(ctx), nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v []alerts.AlertStateFacetItem) graphql.Marshaler { + return ec.marshalNAlertStateFacetItem2ᚕgithubᚗcomᚋnaisᚋapiᚋinternalᚋalertsᚐAlertStateFacetItemᚄ(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_AlertFacets_states(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "AlertFacets", + Field: field, + IsMethod: true, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields_AlertStateFacetItem(ctx, field) + }, + } + return fc, nil +} + +func (ec *executionContext) _AlertStateFacetItem_state(ctx context.Context, field graphql.CollectedField, obj *alerts.AlertStateFacetItem) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_AlertStateFacetItem_state(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.State, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v alerts.AlertState) graphql.Marshaler { + return ec.marshalNAlertState2githubᚗcomᚋnaisᚋapiᚋinternalᚋalertsᚐAlertState(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_AlertStateFacetItem_state(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("AlertStateFacetItem", field, false, false, errors.New("field of type AlertState does not have child fields")) +} + +func (ec *executionContext) _AlertStateFacetItem_count(ctx context.Context, field graphql.CollectedField, obj *alerts.AlertStateFacetItem) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_AlertStateFacetItem_count(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Count, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v int) graphql.Marshaler { + return ec.marshalNInt2int(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_AlertStateFacetItem_count(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("AlertStateFacetItem", field, false, false, errors.New("field of type Int does not have child fields")) +} + func (ec *executionContext) _PrometheusAlarm_action(ctx context.Context, field graphql.CollectedField, obj *alerts.PrometheusAlarm) (ret graphql.Marshaler) { return graphql.ResolveField( ctx, @@ -676,7 +822,7 @@ func (ec *executionContext) _Alert(ctx context.Context, sel ast.SelectionSet, ob var alertConnectionImplementors = []string{"AlertConnection"} -func (ec *executionContext) _AlertConnection(ctx context.Context, sel ast.SelectionSet, obj *pagination.Connection[alerts.Alert]) graphql.Marshaler { +func (ec *executionContext) _AlertConnection(ctx context.Context, sel ast.SelectionSet, obj *pagination.FacetableConnection[alerts.Alert, *alerts.TeamAlertsFilter]) graphql.Marshaler { fields := graphql.CollectFields(ec.OperationContext, sel, alertConnectionImplementors) out := graphql.NewFieldSet(fields) @@ -688,18 +834,51 @@ func (ec *executionContext) _AlertConnection(ctx context.Context, sel ast.Select case "pageInfo": out.Values[i] = ec._AlertConnection_pageInfo(ctx, field, obj) if out.Values[i] == graphql.Null { - out.Invalids++ + atomic.AddUint32(&out.Invalids, 1) } case "nodes": out.Values[i] = ec._AlertConnection_nodes(ctx, field, obj) if out.Values[i] == graphql.Null { - out.Invalids++ + atomic.AddUint32(&out.Invalids, 1) } case "edges": out.Values[i] = ec._AlertConnection_edges(ctx, field, obj) if out.Values[i] == graphql.Null { - out.Invalids++ + atomic.AddUint32(&out.Invalids, 1) } + case "facets": + field := field + + innerFunc := func(ctx context.Context, _ *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._AlertConnection_facets(ctx, field, obj) + return res + } + + if field.Deferrable != nil { + dfs, ok := deferred[field.Deferrable.Label] + di := 0 + if ok { + dfs.AddField(field) + di = len(dfs.Values) - 1 + } else { + dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) + deferred[field.Deferrable.Label] = dfs + } + dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { + return innerFunc(ctx, dfs) + }) + + // don't run the out.Concurrently() call below + out.Values[i] = graphql.Null + continue + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) default: panic("unknown field " + strconv.Quote(field.Name)) } @@ -767,6 +946,156 @@ func (ec *executionContext) _AlertEdge(ctx context.Context, sel ast.SelectionSet return out } +var alertFacetsImplementors = []string{"AlertFacets"} + +func (ec *executionContext) _AlertFacets(ctx context.Context, sel ast.SelectionSet, obj *alerts.AlertFacets) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, alertFacetsImplementors) + + out := graphql.NewFieldSet(fields) + deferred := make(map[string]*graphql.FieldSet) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("AlertFacets") + case "environments": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._AlertFacets_environments(ctx, field, obj) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + if field.Deferrable != nil { + dfs, ok := deferred[field.Deferrable.Label] + di := 0 + if ok { + dfs.AddField(field) + di = len(dfs.Values) - 1 + } else { + dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) + deferred[field.Deferrable.Label] = dfs + } + dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { + return innerFunc(ctx, dfs) + }) + + // don't run the out.Concurrently() call below + out.Values[i] = graphql.Null + continue + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + case "states": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._AlertFacets_states(ctx, field, obj) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + if field.Deferrable != nil { + dfs, ok := deferred[field.Deferrable.Label] + di := 0 + if ok { + dfs.AddField(field) + di = len(dfs.Values) - 1 + } else { + dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) + deferred[field.Deferrable.Label] = dfs + } + dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { + return innerFunc(ctx, dfs) + }) + + // don't run the out.Concurrently() call below + out.Values[i] = graphql.Null + continue + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.Deferred, int32(min(len(deferred), math.MaxInt32))) + + for label, dfs := range deferred { + ec.ProcessDeferredGroup(graphql.DeferredGroup{ + Label: label, + Path: graphql.GetPath(ctx), + FieldSet: dfs, + Context: ctx, + }) + } + + return out +} + +var alertStateFacetItemImplementors = []string{"AlertStateFacetItem"} + +func (ec *executionContext) _AlertStateFacetItem(ctx context.Context, sel ast.SelectionSet, obj *alerts.AlertStateFacetItem) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, alertStateFacetItemImplementors) + + out := graphql.NewFieldSet(fields) + deferred := make(map[string]*graphql.FieldSet) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("AlertStateFacetItem") + case "state": + out.Values[i] = ec._AlertStateFacetItem_state(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "count": + out.Values[i] = ec._AlertStateFacetItem_count(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.Deferred, int32(min(len(deferred), math.MaxInt32))) + + for label, dfs := range deferred { + ec.ProcessDeferredGroup(graphql.DeferredGroup{ + Label: label, + Path: graphql.GetPath(ctx), + FieldSet: dfs, + Context: ctx, + }) + } + + return out +} + var prometheusAlarmImplementors = []string{"PrometheusAlarm"} func (ec *executionContext) _PrometheusAlarm(ctx context.Context, sel ast.SelectionSet, obj *alerts.PrometheusAlarm) graphql.Marshaler { @@ -1002,11 +1331,11 @@ func (ec *executionContext) marshalNAlert2ᚕgithubᚗcomᚋnaisᚋapiᚋinterna return ret } -func (ec *executionContext) marshalNAlertConnection2githubᚗcomᚋnaisᚋapiᚋinternalᚋgraphᚋpaginationᚐConnection(ctx context.Context, sel ast.SelectionSet, v pagination.Connection[alerts.Alert]) graphql.Marshaler { +func (ec *executionContext) marshalNAlertConnection2githubᚗcomᚋnaisᚋapiᚋinternalᚋgraphᚋpaginationᚐFacetableConnection(ctx context.Context, sel ast.SelectionSet, v pagination.FacetableConnection[alerts.Alert, *alerts.TeamAlertsFilter]) graphql.Marshaler { return ec._AlertConnection(ctx, sel, &v) } -func (ec *executionContext) marshalNAlertConnection2ᚖgithubᚗcomᚋnaisᚋapiᚋinternalᚋgraphᚋpaginationᚐConnection(ctx context.Context, sel ast.SelectionSet, v *pagination.Connection[alerts.Alert]) graphql.Marshaler { +func (ec *executionContext) marshalNAlertConnection2ᚖgithubᚗcomᚋnaisᚋapiᚋinternalᚋgraphᚋpaginationᚐFacetableConnection(ctx context.Context, sel ast.SelectionSet, v *pagination.FacetableConnection[alerts.Alert, *alerts.TeamAlertsFilter]) graphql.Marshaler { if v == nil { if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") @@ -1056,6 +1385,26 @@ func (ec *executionContext) marshalNAlertState2githubᚗcomᚋnaisᚋapiᚋinter return v } +func (ec *executionContext) marshalNAlertStateFacetItem2githubᚗcomᚋnaisᚋapiᚋinternalᚋalertsᚐAlertStateFacetItem(ctx context.Context, sel ast.SelectionSet, v alerts.AlertStateFacetItem) graphql.Marshaler { + return ec._AlertStateFacetItem(ctx, sel, &v) +} + +func (ec *executionContext) marshalNAlertStateFacetItem2ᚕgithubᚗcomᚋnaisᚋapiᚋinternalᚋalertsᚐAlertStateFacetItemᚄ(ctx context.Context, sel ast.SelectionSet, v []alerts.AlertStateFacetItem) graphql.Marshaler { + ret := graphql.MarshalSliceConcurrently(ctx, len(v), 0, false, func(ctx context.Context, i int) graphql.Marshaler { + fc := graphql.GetFieldContext(ctx) + fc.Result = &v[i] + return ec.marshalNAlertStateFacetItem2githubᚗcomᚋnaisᚋapiᚋinternalᚋalertsᚐAlertStateFacetItem(ctx, sel, v[i]) + }) + + for _, e := range ret { + if e == graphql.Null { + return graphql.Null + } + } + + return ret +} + func (ec *executionContext) marshalNPrometheusAlarm2ᚕᚖgithubᚗcomᚋnaisᚋapiᚋinternalᚋalertsᚐPrometheusAlarmᚄ(ctx context.Context, sel ast.SelectionSet, v []*alerts.PrometheusAlarm) graphql.Marshaler { ret := graphql.MarshalSliceConcurrently(ctx, len(v), 0, false, func(ctx context.Context, i int) graphql.Marshaler { fc := graphql.GetFieldContext(ctx) @@ -1082,6 +1431,13 @@ func (ec *executionContext) marshalNPrometheusAlarm2ᚖgithubᚗcomᚋnaisᚋapi return ec._PrometheusAlarm(ctx, sel, v) } +func (ec *executionContext) marshalOAlertFacets2ᚖgithubᚗcomᚋnaisᚋapiᚋinternalᚋalertsᚐAlertFacets(ctx context.Context, sel ast.SelectionSet, v *alerts.AlertFacets) graphql.Marshaler { + if v == nil { + return graphql.Null + } + return ec._AlertFacets(ctx, sel, v) +} + func (ec *executionContext) unmarshalOAlertOrder2ᚖgithubᚗcomᚋnaisᚋapiᚋinternalᚋalertsᚐAlertOrder(ctx context.Context, v any) (*alerts.AlertOrder, error) { if v == nil { return nil, nil diff --git a/internal/graph/gengql/applications.generated.go b/internal/graph/gengql/applications.generated.go index 8decf3265..d6441ecaa 100644 --- a/internal/graph/gengql/applications.generated.go +++ b/internal/graph/gengql/applications.generated.go @@ -55,7 +55,7 @@ type ApplicationResolver interface { ActivityLog(ctx context.Context, obj *application.Application, first *int, after *pagination.Cursor, last *int, before *pagination.Cursor, filter *activitylog.ActivityLogFilter) (*activitylog.ActivityLogEntryConnection, error) State(ctx context.Context, obj *application.Application) (application.ApplicationState, error) - Issues(ctx context.Context, obj *application.Application, first *int, after *pagination.Cursor, last *int, before *pagination.Cursor, orderBy *issue.IssueOrder, filter *issue.ResourceIssueFilter) (*pagination.Connection[issue.Issue], error) + Issues(ctx context.Context, obj *application.Application, first *int, after *pagination.Cursor, last *int, before *pagination.Cursor, orderBy *issue.IssueOrder, filter *issue.ResourceIssueFilter) (*issue.IssueConnection, error) History(ctx context.Context, obj *application.Application) ([]*instancegroup.ApplicationHistory, error) BigQueryDatasets(ctx context.Context, obj *application.Application, orderBy *bigquery.BigQueryDatasetOrder) (*pagination.FacetableConnection[*bigquery.BigQueryDataset, *bigquery.BigQueryDatasetFilter], error) Buckets(ctx context.Context, obj *application.Application, orderBy *bucket.BucketOrder) (*pagination.FacetableConnection[*bucket.Bucket, *bucket.BucketFilter], error) @@ -945,8 +945,8 @@ func (ec *executionContext) _Application_issues(ctx context.Context, field graph return ec.Resolvers.Application().Issues(ctx, obj, fc.Args["first"].(*int), fc.Args["after"].(*pagination.Cursor), fc.Args["last"].(*int), fc.Args["before"].(*pagination.Cursor), fc.Args["orderBy"].(*issue.IssueOrder), fc.Args["filter"].(*issue.ResourceIssueFilter)) }, nil, - func(ctx context.Context, selections ast.SelectionSet, v *pagination.Connection[issue.Issue]) graphql.Marshaler { - return ec.marshalNIssueConnection2ᚖgithubᚗcomᚋnaisᚋapiᚋinternalᚋgraphᚋpaginationᚐConnection(ctx, selections, v) + func(ctx context.Context, selections ast.SelectionSet, v *issue.IssueConnection) graphql.Marshaler { + return ec.marshalNIssueConnection2ᚖgithubᚗcomᚋnaisᚋapiᚋinternalᚋissueᚐIssueConnection(ctx, selections, v) }, true, true, diff --git a/internal/graph/gengql/issues.generated.go b/internal/graph/gengql/issues.generated.go index 2613870d4..fb56b878a 100644 --- a/internal/graph/gengql/issues.generated.go +++ b/internal/graph/gengql/issues.generated.go @@ -13,6 +13,7 @@ import ( "github.com/99designs/gqlgen/graphql" "github.com/nais/api/internal/graph/ident" + "github.com/nais/api/internal/graph/model" "github.com/nais/api/internal/graph/pagination" "github.com/nais/api/internal/graph/scalar" "github.com/nais/api/internal/issue" @@ -59,6 +60,9 @@ type InvalidSpecIssueResolver interface { Workload(ctx context.Context, obj *issue.InvalidSpecIssue) (workload.Workload, error) } +type IssueConnectionResolver interface { + Facets(ctx context.Context, obj *issue.IssueConnection) (*issue.IssueFacets, error) +} type LastRunFailedIssueResolver interface { TeamEnvironment(ctx context.Context, obj *issue.LastRunFailedIssue) (*team.TeamEnvironment, error) @@ -1058,7 +1062,7 @@ func (ec *executionContext) fieldContext_InvalidSpecIssue_workload(_ context.Con return fc, nil } -func (ec *executionContext) _IssueConnection_pageInfo(ctx context.Context, field graphql.CollectedField, obj *pagination.Connection[issue.Issue]) (ret graphql.Marshaler) { +func (ec *executionContext) _IssueConnection_pageInfo(ctx context.Context, field graphql.CollectedField, obj *issue.IssueConnection) (ret graphql.Marshaler) { return graphql.ResolveField( ctx, ec.OperationContext, @@ -1090,7 +1094,7 @@ func (ec *executionContext) fieldContext_IssueConnection_pageInfo(_ context.Cont return fc, nil } -func (ec *executionContext) _IssueConnection_nodes(ctx context.Context, field graphql.CollectedField, obj *pagination.Connection[issue.Issue]) (ret graphql.Marshaler) { +func (ec *executionContext) _IssueConnection_nodes(ctx context.Context, field graphql.CollectedField, obj *issue.IssueConnection) (ret graphql.Marshaler) { return graphql.ResolveField( ctx, ec.OperationContext, @@ -1122,7 +1126,7 @@ func (ec *executionContext) fieldContext_IssueConnection_nodes(_ context.Context return fc, nil } -func (ec *executionContext) _IssueConnection_edges(ctx context.Context, field graphql.CollectedField, obj *pagination.Connection[issue.Issue]) (ret graphql.Marshaler) { +func (ec *executionContext) _IssueConnection_edges(ctx context.Context, field graphql.CollectedField, obj *issue.IssueConnection) (ret graphql.Marshaler) { return graphql.ResolveField( ctx, ec.OperationContext, @@ -1154,6 +1158,38 @@ func (ec *executionContext) fieldContext_IssueConnection_edges(_ context.Context return fc, nil } +func (ec *executionContext) _IssueConnection_facets(ctx context.Context, field graphql.CollectedField, obj *issue.IssueConnection) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_IssueConnection_facets(ctx, field) + }, + func(ctx context.Context) (any, error) { + return ec.Resolvers.IssueConnection().Facets(ctx, obj) + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *issue.IssueFacets) graphql.Marshaler { + return ec.marshalOIssueFacets2ᚖgithubᚗcomᚋnaisᚋapiᚋinternalᚋissueᚐIssueFacets(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext_IssueConnection_facets(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "IssueConnection", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields_IssueFacets(ctx, field) + }, + } + return fc, nil +} + func (ec *executionContext) _IssueEdge_cursor(ctx context.Context, field graphql.CollectedField, obj *pagination.Edge[issue.Issue]) (ret graphql.Marshaler) { return graphql.ResolveField( ctx, @@ -1209,6 +1245,272 @@ func (ec *executionContext) fieldContext_IssueEdge_node(_ context.Context, field return fc, nil } +func (ec *executionContext) _IssueFacets_environments(ctx context.Context, field graphql.CollectedField, obj *issue.IssueFacets) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_IssueFacets_environments(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Environments, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v []model.StringFacetItem) graphql.Marshaler { + return ec.marshalNStringFacetItem2ᚕgithubᚗcomᚋnaisᚋapiᚋinternalᚋgraphᚋmodelᚐStringFacetItemᚄ(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_IssueFacets_environments(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "IssueFacets", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields_StringFacetItem(ctx, field) + }, + } + return fc, nil +} + +func (ec *executionContext) _IssueFacets_severities(ctx context.Context, field graphql.CollectedField, obj *issue.IssueFacets) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_IssueFacets_severities(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Severities, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v []issue.IssueSeverityFacetItem) graphql.Marshaler { + return ec.marshalNIssueSeverityFacetItem2ᚕgithubᚗcomᚋnaisᚋapiᚋinternalᚋissueᚐIssueSeverityFacetItemᚄ(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_IssueFacets_severities(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "IssueFacets", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields_IssueSeverityFacetItem(ctx, field) + }, + } + return fc, nil +} + +func (ec *executionContext) _IssueFacets_resourceTypes(ctx context.Context, field graphql.CollectedField, obj *issue.IssueFacets) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_IssueFacets_resourceTypes(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.ResourceTypes, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v []issue.IssueResourceTypeFacetItem) graphql.Marshaler { + return ec.marshalNIssueResourceTypeFacetItem2ᚕgithubᚗcomᚋnaisᚋapiᚋinternalᚋissueᚐIssueResourceTypeFacetItemᚄ(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_IssueFacets_resourceTypes(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "IssueFacets", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields_IssueResourceTypeFacetItem(ctx, field) + }, + } + return fc, nil +} + +func (ec *executionContext) _IssueFacets_issueTypes(ctx context.Context, field graphql.CollectedField, obj *issue.IssueFacets) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_IssueFacets_issueTypes(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.IssueTypes, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v []issue.IssueTypeFacetItem) graphql.Marshaler { + return ec.marshalNIssueTypeFacetItem2ᚕgithubᚗcomᚋnaisᚋapiᚋinternalᚋissueᚐIssueTypeFacetItemᚄ(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_IssueFacets_issueTypes(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "IssueFacets", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.childFields_IssueTypeFacetItem(ctx, field) + }, + } + return fc, nil +} + +func (ec *executionContext) _IssueResourceTypeFacetItem_resourceType(ctx context.Context, field graphql.CollectedField, obj *issue.IssueResourceTypeFacetItem) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_IssueResourceTypeFacetItem_resourceType(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.ResourceType, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v issue.ResourceType) graphql.Marshaler { + return ec.marshalNResourceType2githubᚗcomᚋnaisᚋapiᚋinternalᚋissueᚐResourceType(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_IssueResourceTypeFacetItem_resourceType(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("IssueResourceTypeFacetItem", field, false, false, errors.New("field of type ResourceType does not have child fields")) +} + +func (ec *executionContext) _IssueResourceTypeFacetItem_count(ctx context.Context, field graphql.CollectedField, obj *issue.IssueResourceTypeFacetItem) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_IssueResourceTypeFacetItem_count(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Count, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v int) graphql.Marshaler { + return ec.marshalNInt2int(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_IssueResourceTypeFacetItem_count(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("IssueResourceTypeFacetItem", field, false, false, errors.New("field of type Int does not have child fields")) +} + +func (ec *executionContext) _IssueSeverityFacetItem_severity(ctx context.Context, field graphql.CollectedField, obj *issue.IssueSeverityFacetItem) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_IssueSeverityFacetItem_severity(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Severity, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v issue.Severity) graphql.Marshaler { + return ec.marshalNSeverity2githubᚗcomᚋnaisᚋapiᚋinternalᚋissueᚐSeverity(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_IssueSeverityFacetItem_severity(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("IssueSeverityFacetItem", field, false, false, errors.New("field of type Severity does not have child fields")) +} + +func (ec *executionContext) _IssueSeverityFacetItem_count(ctx context.Context, field graphql.CollectedField, obj *issue.IssueSeverityFacetItem) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_IssueSeverityFacetItem_count(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Count, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v int) graphql.Marshaler { + return ec.marshalNInt2int(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_IssueSeverityFacetItem_count(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("IssueSeverityFacetItem", field, false, false, errors.New("field of type Int does not have child fields")) +} + +func (ec *executionContext) _IssueTypeFacetItem_issueType(ctx context.Context, field graphql.CollectedField, obj *issue.IssueTypeFacetItem) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_IssueTypeFacetItem_issueType(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.IssueType, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v issue.IssueType) graphql.Marshaler { + return ec.marshalNIssueType2githubᚗcomᚋnaisᚋapiᚋinternalᚋissueᚐIssueType(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_IssueTypeFacetItem_issueType(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("IssueTypeFacetItem", field, false, false, errors.New("field of type IssueType does not have child fields")) +} + +func (ec *executionContext) _IssueTypeFacetItem_count(ctx context.Context, field graphql.CollectedField, obj *issue.IssueTypeFacetItem) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_IssueTypeFacetItem_count(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.Count, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v int) graphql.Marshaler { + return ec.marshalNInt2int(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_IssueTypeFacetItem_count(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("IssueTypeFacetItem", field, false, false, errors.New("field of type Int does not have child fields")) +} + func (ec *executionContext) _LastRunFailedIssue_id(ctx context.Context, field graphql.CollectedField, obj *issue.LastRunFailedIssue) (ret graphql.Marshaler) { return graphql.ResolveField( ctx, @@ -3819,7 +4121,7 @@ func (ec *executionContext) _InvalidSpecIssue(ctx context.Context, sel ast.Selec var issueConnectionImplementors = []string{"IssueConnection"} -func (ec *executionContext) _IssueConnection(ctx context.Context, sel ast.SelectionSet, obj *pagination.Connection[issue.Issue]) graphql.Marshaler { +func (ec *executionContext) _IssueConnection(ctx context.Context, sel ast.SelectionSet, obj *issue.IssueConnection) graphql.Marshaler { fields := graphql.CollectFields(ec.OperationContext, sel, issueConnectionImplementors) out := graphql.NewFieldSet(fields) @@ -3831,18 +4133,51 @@ func (ec *executionContext) _IssueConnection(ctx context.Context, sel ast.Select case "pageInfo": out.Values[i] = ec._IssueConnection_pageInfo(ctx, field, obj) if out.Values[i] == graphql.Null { - out.Invalids++ + atomic.AddUint32(&out.Invalids, 1) } case "nodes": out.Values[i] = ec._IssueConnection_nodes(ctx, field, obj) if out.Values[i] == graphql.Null { - out.Invalids++ + atomic.AddUint32(&out.Invalids, 1) } case "edges": out.Values[i] = ec._IssueConnection_edges(ctx, field, obj) if out.Values[i] == graphql.Null { - out.Invalids++ + atomic.AddUint32(&out.Invalids, 1) + } + case "facets": + field := field + + innerFunc := func(ctx context.Context, _ *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._IssueConnection_facets(ctx, field, obj) + return res } + + if field.Deferrable != nil { + dfs, ok := deferred[field.Deferrable.Label] + di := 0 + if ok { + dfs.AddField(field) + di = len(dfs.Values) - 1 + } else { + dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) + deferred[field.Deferrable.Label] = dfs + } + dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { + return innerFunc(ctx, dfs) + }) + + // don't run the out.Concurrently() call below + out.Values[i] = graphql.Null + continue + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) default: panic("unknown field " + strconv.Quote(field.Name)) } @@ -3910,6 +4245,192 @@ func (ec *executionContext) _IssueEdge(ctx context.Context, sel ast.SelectionSet return out } +var issueFacetsImplementors = []string{"IssueFacets"} + +func (ec *executionContext) _IssueFacets(ctx context.Context, sel ast.SelectionSet, obj *issue.IssueFacets) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, issueFacetsImplementors) + + out := graphql.NewFieldSet(fields) + deferred := make(map[string]*graphql.FieldSet) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("IssueFacets") + case "environments": + out.Values[i] = ec._IssueFacets_environments(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "severities": + out.Values[i] = ec._IssueFacets_severities(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "resourceTypes": + out.Values[i] = ec._IssueFacets_resourceTypes(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "issueTypes": + out.Values[i] = ec._IssueFacets_issueTypes(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.Deferred, int32(min(len(deferred), math.MaxInt32))) + + for label, dfs := range deferred { + ec.ProcessDeferredGroup(graphql.DeferredGroup{ + Label: label, + Path: graphql.GetPath(ctx), + FieldSet: dfs, + Context: ctx, + }) + } + + return out +} + +var issueResourceTypeFacetItemImplementors = []string{"IssueResourceTypeFacetItem"} + +func (ec *executionContext) _IssueResourceTypeFacetItem(ctx context.Context, sel ast.SelectionSet, obj *issue.IssueResourceTypeFacetItem) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, issueResourceTypeFacetItemImplementors) + + out := graphql.NewFieldSet(fields) + deferred := make(map[string]*graphql.FieldSet) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("IssueResourceTypeFacetItem") + case "resourceType": + out.Values[i] = ec._IssueResourceTypeFacetItem_resourceType(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "count": + out.Values[i] = ec._IssueResourceTypeFacetItem_count(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.Deferred, int32(min(len(deferred), math.MaxInt32))) + + for label, dfs := range deferred { + ec.ProcessDeferredGroup(graphql.DeferredGroup{ + Label: label, + Path: graphql.GetPath(ctx), + FieldSet: dfs, + Context: ctx, + }) + } + + return out +} + +var issueSeverityFacetItemImplementors = []string{"IssueSeverityFacetItem"} + +func (ec *executionContext) _IssueSeverityFacetItem(ctx context.Context, sel ast.SelectionSet, obj *issue.IssueSeverityFacetItem) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, issueSeverityFacetItemImplementors) + + out := graphql.NewFieldSet(fields) + deferred := make(map[string]*graphql.FieldSet) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("IssueSeverityFacetItem") + case "severity": + out.Values[i] = ec._IssueSeverityFacetItem_severity(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "count": + out.Values[i] = ec._IssueSeverityFacetItem_count(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.Deferred, int32(min(len(deferred), math.MaxInt32))) + + for label, dfs := range deferred { + ec.ProcessDeferredGroup(graphql.DeferredGroup{ + Label: label, + Path: graphql.GetPath(ctx), + FieldSet: dfs, + Context: ctx, + }) + } + + return out +} + +var issueTypeFacetItemImplementors = []string{"IssueTypeFacetItem"} + +func (ec *executionContext) _IssueTypeFacetItem(ctx context.Context, sel ast.SelectionSet, obj *issue.IssueTypeFacetItem) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, issueTypeFacetItemImplementors) + + out := graphql.NewFieldSet(fields) + deferred := make(map[string]*graphql.FieldSet) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("IssueTypeFacetItem") + case "issueType": + out.Values[i] = ec._IssueTypeFacetItem_issueType(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "count": + out.Values[i] = ec._IssueTypeFacetItem_count(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.Deferred, int32(min(len(deferred), math.MaxInt32))) + + for label, dfs := range deferred { + ec.ProcessDeferredGroup(graphql.DeferredGroup{ + Label: label, + Path: graphql.GetPath(ctx), + FieldSet: dfs, + Context: ctx, + }) + } + + return out +} + var lastRunFailedIssueImplementors = []string{"LastRunFailedIssue", "Issue", "Node"} func (ec *executionContext) _LastRunFailedIssue(ctx context.Context, sel ast.SelectionSet, obj *issue.LastRunFailedIssue) graphql.Marshaler { @@ -5199,11 +5720,11 @@ func (ec *executionContext) marshalNIssue2ᚕgithubᚗcomᚋnaisᚋapiᚋinterna return ret } -func (ec *executionContext) marshalNIssueConnection2githubᚗcomᚋnaisᚋapiᚋinternalᚋgraphᚋpaginationᚐConnection(ctx context.Context, sel ast.SelectionSet, v pagination.Connection[issue.Issue]) graphql.Marshaler { +func (ec *executionContext) marshalNIssueConnection2githubᚗcomᚋnaisᚋapiᚋinternalᚋissueᚐIssueConnection(ctx context.Context, sel ast.SelectionSet, v issue.IssueConnection) graphql.Marshaler { return ec._IssueConnection(ctx, sel, &v) } -func (ec *executionContext) marshalNIssueConnection2ᚖgithubᚗcomᚋnaisᚋapiᚋinternalᚋgraphᚋpaginationᚐConnection(ctx context.Context, sel ast.SelectionSet, v *pagination.Connection[issue.Issue]) graphql.Marshaler { +func (ec *executionContext) marshalNIssueConnection2ᚖgithubᚗcomᚋnaisᚋapiᚋinternalᚋissueᚐIssueConnection(ctx context.Context, sel ast.SelectionSet, v *issue.IssueConnection) graphql.Marshaler { if v == nil { if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") @@ -5243,6 +5764,86 @@ func (ec *executionContext) marshalNIssueOrderField2githubᚗcomᚋnaisᚋapiᚋ return v } +func (ec *executionContext) marshalNIssueResourceTypeFacetItem2githubᚗcomᚋnaisᚋapiᚋinternalᚋissueᚐIssueResourceTypeFacetItem(ctx context.Context, sel ast.SelectionSet, v issue.IssueResourceTypeFacetItem) graphql.Marshaler { + return ec._IssueResourceTypeFacetItem(ctx, sel, &v) +} + +func (ec *executionContext) marshalNIssueResourceTypeFacetItem2ᚕgithubᚗcomᚋnaisᚋapiᚋinternalᚋissueᚐIssueResourceTypeFacetItemᚄ(ctx context.Context, sel ast.SelectionSet, v []issue.IssueResourceTypeFacetItem) graphql.Marshaler { + ret := graphql.MarshalSliceConcurrently(ctx, len(v), 0, false, func(ctx context.Context, i int) graphql.Marshaler { + fc := graphql.GetFieldContext(ctx) + fc.Result = &v[i] + return ec.marshalNIssueResourceTypeFacetItem2githubᚗcomᚋnaisᚋapiᚋinternalᚋissueᚐIssueResourceTypeFacetItem(ctx, sel, v[i]) + }) + + for _, e := range ret { + if e == graphql.Null { + return graphql.Null + } + } + + return ret +} + +func (ec *executionContext) marshalNIssueSeverityFacetItem2githubᚗcomᚋnaisᚋapiᚋinternalᚋissueᚐIssueSeverityFacetItem(ctx context.Context, sel ast.SelectionSet, v issue.IssueSeverityFacetItem) graphql.Marshaler { + return ec._IssueSeverityFacetItem(ctx, sel, &v) +} + +func (ec *executionContext) marshalNIssueSeverityFacetItem2ᚕgithubᚗcomᚋnaisᚋapiᚋinternalᚋissueᚐIssueSeverityFacetItemᚄ(ctx context.Context, sel ast.SelectionSet, v []issue.IssueSeverityFacetItem) graphql.Marshaler { + ret := graphql.MarshalSliceConcurrently(ctx, len(v), 0, false, func(ctx context.Context, i int) graphql.Marshaler { + fc := graphql.GetFieldContext(ctx) + fc.Result = &v[i] + return ec.marshalNIssueSeverityFacetItem2githubᚗcomᚋnaisᚋapiᚋinternalᚋissueᚐIssueSeverityFacetItem(ctx, sel, v[i]) + }) + + for _, e := range ret { + if e == graphql.Null { + return graphql.Null + } + } + + return ret +} + +func (ec *executionContext) unmarshalNIssueType2githubᚗcomᚋnaisᚋapiᚋinternalᚋissueᚐIssueType(ctx context.Context, v any) (issue.IssueType, error) { + var res issue.IssueType + err := res.UnmarshalGQL(v) + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalNIssueType2githubᚗcomᚋnaisᚋapiᚋinternalᚋissueᚐIssueType(ctx context.Context, sel ast.SelectionSet, v issue.IssueType) graphql.Marshaler { + return v +} + +func (ec *executionContext) marshalNIssueTypeFacetItem2githubᚗcomᚋnaisᚋapiᚋinternalᚋissueᚐIssueTypeFacetItem(ctx context.Context, sel ast.SelectionSet, v issue.IssueTypeFacetItem) graphql.Marshaler { + return ec._IssueTypeFacetItem(ctx, sel, &v) +} + +func (ec *executionContext) marshalNIssueTypeFacetItem2ᚕgithubᚗcomᚋnaisᚋapiᚋinternalᚋissueᚐIssueTypeFacetItemᚄ(ctx context.Context, sel ast.SelectionSet, v []issue.IssueTypeFacetItem) graphql.Marshaler { + ret := graphql.MarshalSliceConcurrently(ctx, len(v), 0, false, func(ctx context.Context, i int) graphql.Marshaler { + fc := graphql.GetFieldContext(ctx) + fc.Result = &v[i] + return ec.marshalNIssueTypeFacetItem2githubᚗcomᚋnaisᚋapiᚋinternalᚋissueᚐIssueTypeFacetItem(ctx, sel, v[i]) + }) + + for _, e := range ret { + if e == graphql.Null { + return graphql.Null + } + } + + return ret +} + +func (ec *executionContext) unmarshalNResourceType2githubᚗcomᚋnaisᚋapiᚋinternalᚋissueᚐResourceType(ctx context.Context, v any) (issue.ResourceType, error) { + var res issue.ResourceType + err := res.UnmarshalGQL(v) + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalNResourceType2githubᚗcomᚋnaisᚋapiᚋinternalᚋissueᚐResourceType(ctx context.Context, sel ast.SelectionSet, v issue.ResourceType) graphql.Marshaler { + return v +} + func (ec *executionContext) unmarshalNSeverity2githubᚗcomᚋnaisᚋapiᚋinternalᚋissueᚐSeverity(ctx context.Context, v any) (issue.Severity, error) { var res issue.Severity err := res.UnmarshalGQL(v) @@ -5263,6 +5864,13 @@ func (ec *executionContext) marshalNWorkloadProblemType2githubᚗcomᚋnaisᚋap return v } +func (ec *executionContext) marshalOIssueFacets2ᚖgithubᚗcomᚋnaisᚋapiᚋinternalᚋissueᚐIssueFacets(ctx context.Context, sel ast.SelectionSet, v *issue.IssueFacets) graphql.Marshaler { + if v == nil { + return graphql.Null + } + return ec._IssueFacets(ctx, sel, v) +} + func (ec *executionContext) unmarshalOIssueFilter2ᚖgithubᚗcomᚋnaisᚋapiᚋinternalᚋissueᚐIssueFilter(ctx context.Context, v any) (*issue.IssueFilter, error) { if v == nil { return nil, nil diff --git a/internal/graph/gengql/jobs.generated.go b/internal/graph/gengql/jobs.generated.go index a472ae8c0..f7774d0c3 100644 --- a/internal/graph/gengql/jobs.generated.go +++ b/internal/graph/gengql/jobs.generated.go @@ -59,7 +59,7 @@ type JobResolver interface { ActivityLog(ctx context.Context, obj *job.Job, first *int, after *pagination.Cursor, last *int, before *pagination.Cursor, filter *activitylog.ActivityLogFilter) (*activitylog.ActivityLogEntryConnection, error) State(ctx context.Context, obj *job.Job) (job.JobState, error) - Issues(ctx context.Context, obj *job.Job, first *int, after *pagination.Cursor, last *int, before *pagination.Cursor, orderBy *issue.IssueOrder, filter *issue.ResourceIssueFilter) (*pagination.Connection[issue.Issue], error) + Issues(ctx context.Context, obj *job.Job, first *int, after *pagination.Cursor, last *int, before *pagination.Cursor, orderBy *issue.IssueOrder, filter *issue.ResourceIssueFilter) (*issue.IssueConnection, error) BigQueryDatasets(ctx context.Context, obj *job.Job, orderBy *bigquery.BigQueryDatasetOrder) (*pagination.FacetableConnection[*bigquery.BigQueryDataset, *bigquery.BigQueryDatasetFilter], error) Buckets(ctx context.Context, obj *job.Job, orderBy *bucket.BucketOrder) (*pagination.FacetableConnection[*bucket.Bucket, *bucket.BucketFilter], error) Configs(ctx context.Context, obj *job.Job, first *int, after *pagination.Cursor, last *int, before *pagination.Cursor) (*pagination.FacetableConnection[*config.Config, *config.ConfigFilter], error) @@ -1055,8 +1055,8 @@ func (ec *executionContext) _Job_issues(ctx context.Context, field graphql.Colle return ec.Resolvers.Job().Issues(ctx, obj, fc.Args["first"].(*int), fc.Args["after"].(*pagination.Cursor), fc.Args["last"].(*int), fc.Args["before"].(*pagination.Cursor), fc.Args["orderBy"].(*issue.IssueOrder), fc.Args["filter"].(*issue.ResourceIssueFilter)) }, nil, - func(ctx context.Context, selections ast.SelectionSet, v *pagination.Connection[issue.Issue]) graphql.Marshaler { - return ec.marshalNIssueConnection2ᚖgithubᚗcomᚋnaisᚋapiᚋinternalᚋgraphᚋpaginationᚐConnection(ctx, selections, v) + func(ctx context.Context, selections ast.SelectionSet, v *issue.IssueConnection) graphql.Marshaler { + return ec.marshalNIssueConnection2ᚖgithubᚗcomᚋnaisᚋapiᚋinternalᚋissueᚐIssueConnection(ctx, selections, v) }, true, true, diff --git a/internal/graph/gengql/opensearch.generated.go b/internal/graph/gengql/opensearch.generated.go index 96f7b168c..7e543eec0 100644 --- a/internal/graph/gengql/opensearch.generated.go +++ b/internal/graph/gengql/opensearch.generated.go @@ -36,7 +36,7 @@ type OpenSearchResolver interface { Access(ctx context.Context, obj *opensearch.OpenSearch, first *int, after *pagination.Cursor, last *int, before *pagination.Cursor, orderBy *opensearch.OpenSearchAccessOrder) (*pagination.Connection[*opensearch.OpenSearchAccess], error) Version(ctx context.Context, obj *opensearch.OpenSearch) (*opensearch.OpenSearchVersion, error) - Issues(ctx context.Context, obj *opensearch.OpenSearch, first *int, after *pagination.Cursor, last *int, before *pagination.Cursor, orderBy *issue.IssueOrder, filter *issue.ResourceIssueFilter) (*pagination.Connection[issue.Issue], error) + Issues(ctx context.Context, obj *opensearch.OpenSearch, first *int, after *pagination.Cursor, last *int, before *pagination.Cursor, orderBy *issue.IssueOrder, filter *issue.ResourceIssueFilter) (*issue.IssueConnection, error) ActivityLog(ctx context.Context, obj *opensearch.OpenSearch, first *int, after *pagination.Cursor, last *int, before *pagination.Cursor, filter *activitylog.ActivityLogFilter) (*activitylog.ActivityLogEntryConnection, error) Cost(ctx context.Context, obj *opensearch.OpenSearch) (*cost.OpenSearchCost, error) Maintenance(ctx context.Context, obj *opensearch.OpenSearch) (*servicemaintenance.OpenSearchMaintenance, error) @@ -671,8 +671,8 @@ func (ec *executionContext) _OpenSearch_issues(ctx context.Context, field graphq return ec.Resolvers.OpenSearch().Issues(ctx, obj, fc.Args["first"].(*int), fc.Args["after"].(*pagination.Cursor), fc.Args["last"].(*int), fc.Args["before"].(*pagination.Cursor), fc.Args["orderBy"].(*issue.IssueOrder), fc.Args["filter"].(*issue.ResourceIssueFilter)) }, nil, - func(ctx context.Context, selections ast.SelectionSet, v *pagination.Connection[issue.Issue]) graphql.Marshaler { - return ec.marshalNIssueConnection2ᚖgithubᚗcomᚋnaisᚋapiᚋinternalᚋgraphᚋpaginationᚐConnection(ctx, selections, v) + func(ctx context.Context, selections ast.SelectionSet, v *issue.IssueConnection) graphql.Marshaler { + return ec.marshalNIssueConnection2ᚖgithubᚗcomᚋnaisᚋapiᚋinternalᚋissueᚐIssueConnection(ctx, selections, v) }, true, true, diff --git a/internal/graph/gengql/root_.generated.go b/internal/graph/gengql/root_.generated.go index 31eb121a4..dc7837338 100644 --- a/internal/graph/gengql/root_.generated.go +++ b/internal/graph/gengql/root_.generated.go @@ -59,6 +59,7 @@ type Config = graphql.Config[ResolverRoot, DirectiveRoot, ComplexityRoot] type ResolverRoot interface { ActivityLogEntryConnection() ActivityLogEntryConnectionResolver + AlertConnection() AlertConnectionResolver Application() ApplicationResolver ApplicationConnection() ApplicationConnectionResolver ApplicationInstance() ApplicationInstanceResolver @@ -87,6 +88,7 @@ type ResolverRoot interface { IngressMetrics() IngressMetricsResolver InstanceGroup() InstanceGroupResolver InvalidSpecIssue() InvalidSpecIssueResolver + IssueConnection() IssueConnectionResolver Job() JobResolver JobConnection() JobConnectionResolver JobRun() JobRunResolver @@ -211,6 +213,7 @@ type ComplexityRoot struct { AlertConnection struct { Edges func(childComplexity int) int + Facets func(childComplexity int) int Nodes func(childComplexity int) int PageInfo func(childComplexity int) int } @@ -220,6 +223,16 @@ type ComplexityRoot struct { Node func(childComplexity int) int } + AlertFacets struct { + Environments func(childComplexity int) int + States func(childComplexity int) int + } + + AlertStateFacetItem struct { + Count func(childComplexity int) int + State func(childComplexity int) int + } + AllowTeamAccessToUnleashPayload struct { Unleash func(childComplexity int) int } @@ -1141,6 +1154,7 @@ type ComplexityRoot struct { IssueConnection struct { Edges func(childComplexity int) int + Facets func(childComplexity int) int Nodes func(childComplexity int) int PageInfo func(childComplexity int) int } @@ -1150,6 +1164,28 @@ type ComplexityRoot struct { Node func(childComplexity int) int } + IssueFacets struct { + Environments func(childComplexity int) int + IssueTypes func(childComplexity int) int + ResourceTypes func(childComplexity int) int + Severities func(childComplexity int) int + } + + IssueResourceTypeFacetItem struct { + Count func(childComplexity int) int + ResourceType func(childComplexity int) int + } + + IssueSeverityFacetItem struct { + Count func(childComplexity int) int + Severity func(childComplexity int) int + } + + IssueTypeFacetItem struct { + Count func(childComplexity int) int + IssueType func(childComplexity int) int + } + Job struct { ActivityLog func(childComplexity int, first *int, after *pagination.Cursor, last *int, before *pagination.Cursor, filter *activitylog.ActivityLogFilter) int AuthIntegrations func(childComplexity int) int @@ -3830,6 +3866,13 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return e.ComplexityRoot.AlertConnection.Edges(childComplexity), true + case "AlertConnection.facets": + if e.ComplexityRoot.AlertConnection.Facets == nil { + break + } + + return e.ComplexityRoot.AlertConnection.Facets(childComplexity), true + case "AlertConnection.nodes": if e.ComplexityRoot.AlertConnection.Nodes == nil { break @@ -3858,6 +3901,34 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return e.ComplexityRoot.AlertEdge.Node(childComplexity), true + case "AlertFacets.environments": + if e.ComplexityRoot.AlertFacets.Environments == nil { + break + } + + return e.ComplexityRoot.AlertFacets.Environments(childComplexity), true + + case "AlertFacets.states": + if e.ComplexityRoot.AlertFacets.States == nil { + break + } + + return e.ComplexityRoot.AlertFacets.States(childComplexity), true + + case "AlertStateFacetItem.count": + if e.ComplexityRoot.AlertStateFacetItem.Count == nil { + break + } + + return e.ComplexityRoot.AlertStateFacetItem.Count(childComplexity), true + + case "AlertStateFacetItem.state": + if e.ComplexityRoot.AlertStateFacetItem.State == nil { + break + } + + return e.ComplexityRoot.AlertStateFacetItem.State(childComplexity), true + case "AllowTeamAccessToUnleashPayload.unleash": if e.ComplexityRoot.AllowTeamAccessToUnleashPayload.Unleash == nil { break @@ -7545,6 +7616,13 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return e.ComplexityRoot.IssueConnection.Edges(childComplexity), true + case "IssueConnection.facets": + if e.ComplexityRoot.IssueConnection.Facets == nil { + break + } + + return e.ComplexityRoot.IssueConnection.Facets(childComplexity), true + case "IssueConnection.nodes": if e.ComplexityRoot.IssueConnection.Nodes == nil { break @@ -7573,6 +7651,76 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return e.ComplexityRoot.IssueEdge.Node(childComplexity), true + case "IssueFacets.environments": + if e.ComplexityRoot.IssueFacets.Environments == nil { + break + } + + return e.ComplexityRoot.IssueFacets.Environments(childComplexity), true + + case "IssueFacets.issueTypes": + if e.ComplexityRoot.IssueFacets.IssueTypes == nil { + break + } + + return e.ComplexityRoot.IssueFacets.IssueTypes(childComplexity), true + + case "IssueFacets.resourceTypes": + if e.ComplexityRoot.IssueFacets.ResourceTypes == nil { + break + } + + return e.ComplexityRoot.IssueFacets.ResourceTypes(childComplexity), true + + case "IssueFacets.severities": + if e.ComplexityRoot.IssueFacets.Severities == nil { + break + } + + return e.ComplexityRoot.IssueFacets.Severities(childComplexity), true + + case "IssueResourceTypeFacetItem.count": + if e.ComplexityRoot.IssueResourceTypeFacetItem.Count == nil { + break + } + + return e.ComplexityRoot.IssueResourceTypeFacetItem.Count(childComplexity), true + + case "IssueResourceTypeFacetItem.resourceType": + if e.ComplexityRoot.IssueResourceTypeFacetItem.ResourceType == nil { + break + } + + return e.ComplexityRoot.IssueResourceTypeFacetItem.ResourceType(childComplexity), true + + case "IssueSeverityFacetItem.count": + if e.ComplexityRoot.IssueSeverityFacetItem.Count == nil { + break + } + + return e.ComplexityRoot.IssueSeverityFacetItem.Count(childComplexity), true + + case "IssueSeverityFacetItem.severity": + if e.ComplexityRoot.IssueSeverityFacetItem.Severity == nil { + break + } + + return e.ComplexityRoot.IssueSeverityFacetItem.Severity(childComplexity), true + + case "IssueTypeFacetItem.count": + if e.ComplexityRoot.IssueTypeFacetItem.Count == nil { + break + } + + return e.ComplexityRoot.IssueTypeFacetItem.Count(childComplexity), true + + case "IssueTypeFacetItem.issueType": + if e.ComplexityRoot.IssueTypeFacetItem.IssueType == nil { + break + } + + return e.ComplexityRoot.IssueTypeFacetItem.IssueType(childComplexity), true + case "Job.activityLog": if e.ComplexityRoot.Job.ActivityLog == nil { break @@ -19909,6 +20057,34 @@ type AlertConnection { List of edges. """ edges: [AlertEdge!]! + + """ + Facets for alerts. Provides distribution counts to help narrow down results. + Facet counts are computed over the full result set (ignoring pagination) but respect the current filter. + """ + facets: AlertFacets +} + +""" +Facets for alerts, providing distribution counts across different dimensions. +""" +type AlertFacets { + "Distribution of alerts by environment." + environments: [StringFacetItem!]! + + "Distribution of alerts by state." + states: [AlertStateFacetItem!]! +} + +""" +A single facet item for alert states. +""" +type AlertStateFacetItem { + "The alert state." + state: AlertState! + + "Number of matching alerts." + count: Int! } """ @@ -23170,6 +23346,70 @@ type IssueConnection { "List of edges." edges: [IssueEdge!]! + + """ + Facets for the issues. Provides distribution counts to help narrow down results. + Facet counts are computed over the full result set (ignoring pagination) but respect the current filter. + """ + facets: IssueFacets +} + +""" +Facets for issues, providing distribution counts across different dimensions. +""" +type IssueFacets { + """ + Distribution of issues by environment. + """ + environments: [StringFacetItem!]! + + """ + Distribution of issues by severity. + """ + severities: [IssueSeverityFacetItem!]! + + """ + Distribution of issues by resource type. + """ + resourceTypes: [IssueResourceTypeFacetItem!]! + + """ + Distribution of issues by issue type. + """ + issueTypes: [IssueTypeFacetItem!]! +} + +""" +A single facet item for issue severities. +""" +type IssueSeverityFacetItem { + "The severity." + severity: Severity! + + "Number of matching issues." + count: Int! +} + +""" +A single facet item for resource types. +""" +type IssueResourceTypeFacetItem { + "The resource type." + resourceType: ResourceType! + + "Number of matching issues." + count: Int! +} + +""" +A single facet item for issue types. +""" +type IssueTypeFacetItem { + "The issue type." + issueType: IssueType! + + "Number of matching issues." + count: Int! } type IssueEdge { @@ -32597,6 +32837,8 @@ func (ec *executionContext) childFields_AlertConnection(ctx context.Context, fie return ec.fieldContext_AlertConnection_nodes(ctx, field) case "edges": return ec.fieldContext_AlertConnection_edges(ctx, field) + case "facets": + return ec.fieldContext_AlertConnection_facets(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type AlertConnection", field.Name) } @@ -32611,6 +32853,26 @@ func (ec *executionContext) childFields_AlertEdge(ctx context.Context, field gra return nil, fmt.Errorf("no field named %q was found under type AlertEdge", field.Name) } +func (ec *executionContext) childFields_AlertFacets(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "environments": + return ec.fieldContext_AlertFacets_environments(ctx, field) + case "states": + return ec.fieldContext_AlertFacets_states(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type AlertFacets", field.Name) +} + +func (ec *executionContext) childFields_AlertStateFacetItem(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "state": + return ec.fieldContext_AlertStateFacetItem_state(ctx, field) + case "count": + return ec.fieldContext_AlertStateFacetItem_count(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type AlertStateFacetItem", field.Name) +} + func (ec *executionContext) childFields_AllowTeamAccessToUnleashPayload(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { switch field.Name { case "unleash": @@ -34025,6 +34287,8 @@ func (ec *executionContext) childFields_IssueConnection(ctx context.Context, fie return ec.fieldContext_IssueConnection_nodes(ctx, field) case "edges": return ec.fieldContext_IssueConnection_edges(ctx, field) + case "facets": + return ec.fieldContext_IssueConnection_facets(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type IssueConnection", field.Name) } @@ -34039,6 +34303,50 @@ func (ec *executionContext) childFields_IssueEdge(ctx context.Context, field gra return nil, fmt.Errorf("no field named %q was found under type IssueEdge", field.Name) } +func (ec *executionContext) childFields_IssueFacets(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "environments": + return ec.fieldContext_IssueFacets_environments(ctx, field) + case "severities": + return ec.fieldContext_IssueFacets_severities(ctx, field) + case "resourceTypes": + return ec.fieldContext_IssueFacets_resourceTypes(ctx, field) + case "issueTypes": + return ec.fieldContext_IssueFacets_issueTypes(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type IssueFacets", field.Name) +} + +func (ec *executionContext) childFields_IssueResourceTypeFacetItem(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "resourceType": + return ec.fieldContext_IssueResourceTypeFacetItem_resourceType(ctx, field) + case "count": + return ec.fieldContext_IssueResourceTypeFacetItem_count(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type IssueResourceTypeFacetItem", field.Name) +} + +func (ec *executionContext) childFields_IssueSeverityFacetItem(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "severity": + return ec.fieldContext_IssueSeverityFacetItem_severity(ctx, field) + case "count": + return ec.fieldContext_IssueSeverityFacetItem_count(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type IssueSeverityFacetItem", field.Name) +} + +func (ec *executionContext) childFields_IssueTypeFacetItem(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "issueType": + return ec.fieldContext_IssueTypeFacetItem_issueType(ctx, field) + case "count": + return ec.fieldContext_IssueTypeFacetItem_count(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type IssueTypeFacetItem", field.Name) +} + func (ec *executionContext) childFields_Job(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { switch field.Name { case "id": diff --git a/internal/graph/gengql/sqlinstance.generated.go b/internal/graph/gengql/sqlinstance.generated.go index e1aedb0f5..6dd122ceb 100644 --- a/internal/graph/gengql/sqlinstance.generated.go +++ b/internal/graph/gengql/sqlinstance.generated.go @@ -37,7 +37,7 @@ type SqlInstanceResolver interface { Users(ctx context.Context, obj *sqlinstance.SQLInstance, first *int, after *pagination.Cursor, last *int, before *pagination.Cursor, orderBy *sqlinstance.SQLInstanceUserOrder) (*pagination.Connection[*sqlinstance.SQLInstanceUser], error) Metrics(ctx context.Context, obj *sqlinstance.SQLInstance) (*sqlinstance.SQLInstanceMetrics, error) State(ctx context.Context, obj *sqlinstance.SQLInstance) (sqlinstance.SQLInstanceState, error) - Issues(ctx context.Context, obj *sqlinstance.SQLInstance, first *int, after *pagination.Cursor, last *int, before *pagination.Cursor, orderBy *issue.IssueOrder, filter *issue.ResourceIssueFilter) (*pagination.Connection[issue.Issue], error) + Issues(ctx context.Context, obj *sqlinstance.SQLInstance, first *int, after *pagination.Cursor, last *int, before *pagination.Cursor, orderBy *issue.IssueOrder, filter *issue.ResourceIssueFilter) (*issue.IssueConnection, error) AuditLog(ctx context.Context, obj *sqlinstance.SQLInstance) (*sqlinstance.AuditLog, error) Cost(ctx context.Context, obj *sqlinstance.SQLInstance) (*cost.SQLInstanceCost, error) } @@ -1147,8 +1147,8 @@ func (ec *executionContext) _SqlInstance_issues(ctx context.Context, field graph return ec.Resolvers.SqlInstance().Issues(ctx, obj, fc.Args["first"].(*int), fc.Args["after"].(*pagination.Cursor), fc.Args["last"].(*int), fc.Args["before"].(*pagination.Cursor), fc.Args["orderBy"].(*issue.IssueOrder), fc.Args["filter"].(*issue.ResourceIssueFilter)) }, nil, - func(ctx context.Context, selections ast.SelectionSet, v *pagination.Connection[issue.Issue]) graphql.Marshaler { - return ec.marshalNIssueConnection2ᚖgithubᚗcomᚋnaisᚋapiᚋinternalᚋgraphᚋpaginationᚐConnection(ctx, selections, v) + func(ctx context.Context, selections ast.SelectionSet, v *issue.IssueConnection) graphql.Marshaler { + return ec.marshalNIssueConnection2ᚖgithubᚗcomᚋnaisᚋapiᚋinternalᚋissueᚐIssueConnection(ctx, selections, v) }, true, true, diff --git a/internal/graph/gengql/teams.generated.go b/internal/graph/gengql/teams.generated.go index 09e52e9e1..0480e03c0 100644 --- a/internal/graph/gengql/teams.generated.go +++ b/internal/graph/gengql/teams.generated.go @@ -61,7 +61,7 @@ type TeamResolver interface { DeleteKey(ctx context.Context, obj *team.Team, key string) (*team.TeamDeleteKey, error) InventoryCounts(ctx context.Context, obj *team.Team) (*team.TeamInventoryCounts, error) ActivityLog(ctx context.Context, obj *team.Team, first *int, after *pagination.Cursor, last *int, before *pagination.Cursor, filter *activitylog.ActivityLogFilter) (*activitylog.ActivityLogEntryConnection, error) - Alerts(ctx context.Context, obj *team.Team, first *int, after *pagination.Cursor, last *int, before *pagination.Cursor, orderBy *alerts.AlertOrder, filter *alerts.TeamAlertsFilter) (*pagination.Connection[alerts.Alert], error) + Alerts(ctx context.Context, obj *team.Team, first *int, after *pagination.Cursor, last *int, before *pagination.Cursor, orderBy *alerts.AlertOrder, filter *alerts.TeamAlertsFilter) (*pagination.FacetableConnection[alerts.Alert, *alerts.TeamAlertsFilter], error) Applications(ctx context.Context, obj *team.Team, first *int, after *pagination.Cursor, last *int, before *pagination.Cursor, orderBy *application.ApplicationOrder, filter *application.TeamApplicationsFilter) (*pagination.FacetableConnection[*application.Application, *application.TeamApplicationsFilter], error) BigQueryDatasets(ctx context.Context, obj *team.Team, first *int, after *pagination.Cursor, last *int, before *pagination.Cursor, orderBy *bigquery.BigQueryDatasetOrder, filter *bigquery.BigQueryDatasetFilter) (*pagination.FacetableConnection[*bigquery.BigQueryDataset, *bigquery.BigQueryDatasetFilter], error) Buckets(ctx context.Context, obj *team.Team, first *int, after *pagination.Cursor, last *int, before *pagination.Cursor, orderBy *bucket.BucketOrder, filter *bucket.BucketFilter) (*pagination.FacetableConnection[*bucket.Bucket, *bucket.BucketFilter], error) @@ -69,7 +69,7 @@ type TeamResolver interface { Cost(ctx context.Context, obj *team.Team) (*cost.TeamCost, error) DeploymentKey(ctx context.Context, obj *team.Team) (*deployment.DeploymentKey, error) Deployments(ctx context.Context, obj *team.Team, first *int, after *pagination.Cursor, last *int, before *pagination.Cursor) (*pagination.Connection[*deployment.Deployment], error) - Issues(ctx context.Context, obj *team.Team, first *int, after *pagination.Cursor, last *int, before *pagination.Cursor, orderBy *issue.IssueOrder, filter *issue.IssueFilter) (*pagination.Connection[issue.Issue], error) + Issues(ctx context.Context, obj *team.Team, first *int, after *pagination.Cursor, last *int, before *pagination.Cursor, orderBy *issue.IssueOrder, filter *issue.IssueFilter) (*issue.IssueConnection, error) Jobs(ctx context.Context, obj *team.Team, first *int, after *pagination.Cursor, last *int, before *pagination.Cursor, orderBy *job.JobOrder, filter *job.TeamJobsFilter) (*pagination.FacetableConnection[*job.Job, *job.TeamJobsFilter], error) KafkaTopics(ctx context.Context, obj *team.Team, first *int, after *pagination.Cursor, last *int, before *pagination.Cursor, orderBy *kafkatopic.KafkaTopicOrder, filter *kafkatopic.KafkaTopicFilter) (*pagination.FacetableConnection[*kafkatopic.KafkaTopic, *kafkatopic.KafkaTopicFilter], error) OpenSearches(ctx context.Context, obj *team.Team, first *int, after *pagination.Cursor, last *int, before *pagination.Cursor, orderBy *opensearch.OpenSearchOrder, filter *opensearch.OpenSearchFilter) (*pagination.FacetableConnection[*opensearch.OpenSearch, *opensearch.OpenSearchFilter], error) @@ -94,7 +94,7 @@ type TeamDeleteKeyResolver interface { } type TeamEnvironmentResolver interface { Team(ctx context.Context, obj *team.TeamEnvironment) (*team.Team, error) - Alerts(ctx context.Context, obj *team.TeamEnvironment, first *int, after *pagination.Cursor, last *int, before *pagination.Cursor, orderBy *alerts.AlertOrder, filter *alerts.TeamAlertsFilter) (*pagination.Connection[alerts.Alert], error) + Alerts(ctx context.Context, obj *team.TeamEnvironment, first *int, after *pagination.Cursor, last *int, before *pagination.Cursor, orderBy *alerts.AlertOrder, filter *alerts.TeamAlertsFilter) (*pagination.FacetableConnection[alerts.Alert, *alerts.TeamAlertsFilter], error) Application(ctx context.Context, obj *team.TeamEnvironment, name string) (*application.Application, error) BigQueryDataset(ctx context.Context, obj *team.TeamEnvironment, name string) (*bigquery.BigQueryDataset, error) Bucket(ctx context.Context, obj *team.TeamEnvironment, name string) (*bucket.Bucket, error) @@ -2238,8 +2238,8 @@ func (ec *executionContext) _Team_alerts(ctx context.Context, field graphql.Coll return ec.Resolvers.Team().Alerts(ctx, obj, fc.Args["first"].(*int), fc.Args["after"].(*pagination.Cursor), fc.Args["last"].(*int), fc.Args["before"].(*pagination.Cursor), fc.Args["orderBy"].(*alerts.AlertOrder), fc.Args["filter"].(*alerts.TeamAlertsFilter)) }, nil, - func(ctx context.Context, selections ast.SelectionSet, v *pagination.Connection[alerts.Alert]) graphql.Marshaler { - return ec.marshalNAlertConnection2ᚖgithubᚗcomᚋnaisᚋapiᚋinternalᚋgraphᚋpaginationᚐConnection(ctx, selections, v) + func(ctx context.Context, selections ast.SelectionSet, v *pagination.FacetableConnection[alerts.Alert, *alerts.TeamAlertsFilter]) graphql.Marshaler { + return ec.marshalNAlertConnection2ᚖgithubᚗcomᚋnaisᚋapiᚋinternalᚋgraphᚋpaginationᚐFacetableConnection(ctx, selections, v) }, true, true, @@ -2566,8 +2566,8 @@ func (ec *executionContext) _Team_issues(ctx context.Context, field graphql.Coll return ec.Resolvers.Team().Issues(ctx, obj, fc.Args["first"].(*int), fc.Args["after"].(*pagination.Cursor), fc.Args["last"].(*int), fc.Args["before"].(*pagination.Cursor), fc.Args["orderBy"].(*issue.IssueOrder), fc.Args["filter"].(*issue.IssueFilter)) }, nil, - func(ctx context.Context, selections ast.SelectionSet, v *pagination.Connection[issue.Issue]) graphql.Marshaler { - return ec.marshalNIssueConnection2ᚖgithubᚗcomᚋnaisᚋapiᚋinternalᚋgraphᚋpaginationᚐConnection(ctx, selections, v) + func(ctx context.Context, selections ast.SelectionSet, v *issue.IssueConnection) graphql.Marshaler { + return ec.marshalNIssueConnection2ᚖgithubᚗcomᚋnaisᚋapiᚋinternalᚋissueᚐIssueConnection(ctx, selections, v) }, true, true, @@ -4340,8 +4340,8 @@ func (ec *executionContext) _TeamEnvironment_alerts(ctx context.Context, field g return ec.Resolvers.TeamEnvironment().Alerts(ctx, obj, fc.Args["first"].(*int), fc.Args["after"].(*pagination.Cursor), fc.Args["last"].(*int), fc.Args["before"].(*pagination.Cursor), fc.Args["orderBy"].(*alerts.AlertOrder), fc.Args["filter"].(*alerts.TeamAlertsFilter)) }, nil, - func(ctx context.Context, selections ast.SelectionSet, v *pagination.Connection[alerts.Alert]) graphql.Marshaler { - return ec.marshalNAlertConnection2ᚖgithubᚗcomᚋnaisᚋapiᚋinternalᚋgraphᚋpaginationᚐConnection(ctx, selections, v) + func(ctx context.Context, selections ast.SelectionSet, v *pagination.FacetableConnection[alerts.Alert, *alerts.TeamAlertsFilter]) graphql.Marshaler { + return ec.marshalNAlertConnection2ᚖgithubᚗcomᚋnaisᚋapiᚋinternalᚋgraphᚋpaginationᚐFacetableConnection(ctx, selections, v) }, true, true, diff --git a/internal/graph/gengql/valkey.generated.go b/internal/graph/gengql/valkey.generated.go index ed0a51b7e..748c092e1 100644 --- a/internal/graph/gengql/valkey.generated.go +++ b/internal/graph/gengql/valkey.generated.go @@ -35,7 +35,7 @@ type ValkeyResolver interface { Workload(ctx context.Context, obj *valkey.Valkey) (workload.Workload, error) State(ctx context.Context, obj *valkey.Valkey) (valkey.ValkeyState, error) - Issues(ctx context.Context, obj *valkey.Valkey, first *int, after *pagination.Cursor, last *int, before *pagination.Cursor, orderBy *issue.IssueOrder, filter *issue.ResourceIssueFilter) (*pagination.Connection[issue.Issue], error) + Issues(ctx context.Context, obj *valkey.Valkey, first *int, after *pagination.Cursor, last *int, before *pagination.Cursor, orderBy *issue.IssueOrder, filter *issue.ResourceIssueFilter) (*issue.IssueConnection, error) ActivityLog(ctx context.Context, obj *valkey.Valkey, first *int, after *pagination.Cursor, last *int, before *pagination.Cursor, filter *activitylog.ActivityLogFilter) (*activitylog.ActivityLogEntryConnection, error) Cost(ctx context.Context, obj *valkey.Valkey) (*cost.ValkeyCost, error) Maintenance(ctx context.Context, obj *valkey.Valkey) (*servicemaintenance.ValkeyMaintenance, error) @@ -739,8 +739,8 @@ func (ec *executionContext) _Valkey_issues(ctx context.Context, field graphql.Co return ec.Resolvers.Valkey().Issues(ctx, obj, fc.Args["first"].(*int), fc.Args["after"].(*pagination.Cursor), fc.Args["last"].(*int), fc.Args["before"].(*pagination.Cursor), fc.Args["orderBy"].(*issue.IssueOrder), fc.Args["filter"].(*issue.ResourceIssueFilter)) }, nil, - func(ctx context.Context, selections ast.SelectionSet, v *pagination.Connection[issue.Issue]) graphql.Marshaler { - return ec.marshalNIssueConnection2ᚖgithubᚗcomᚋnaisᚋapiᚋinternalᚋgraphᚋpaginationᚐConnection(ctx, selections, v) + func(ctx context.Context, selections ast.SelectionSet, v *issue.IssueConnection) graphql.Marshaler { + return ec.marshalNIssueConnection2ᚖgithubᚗcomᚋnaisᚋapiᚋinternalᚋissueᚐIssueConnection(ctx, selections, v) }, true, true, diff --git a/internal/graph/issues.resolvers.go b/internal/graph/issues.resolvers.go index 58e6143e3..558894d1f 100644 --- a/internal/graph/issues.resolvers.go +++ b/internal/graph/issues.resolvers.go @@ -68,6 +68,10 @@ func (r *invalidSpecIssueResolver) Workload(ctx context.Context, obj *issue.Inva return getWorkloadByResourceType(ctx, obj.TeamSlug, obj.EnvironmentName, obj.ResourceName, obj.ResourceType) } +func (r *issueConnectionResolver) Facets(ctx context.Context, obj *issue.IssueConnection) (*issue.IssueFacets, error) { + return issue.ComputeFacets(ctx, obj.GetTeamSlug(), obj.GetScope(), obj.GetFilter()) +} + func (r *lastRunFailedIssueResolver) TeamEnvironment(ctx context.Context, obj *issue.LastRunFailedIssue) (*team.TeamEnvironment, error) { return team.GetTeamEnvironment(ctx, obj.TeamSlug, obj.EnvironmentName) } @@ -116,13 +120,13 @@ func (r *sqlInstanceVersionIssueResolver) SQLInstance(ctx context.Context, obj * return sqlinstance.Get(ctx, obj.TeamSlug, obj.EnvironmentName, obj.ResourceName) } -func (r *teamResolver) Issues(ctx context.Context, obj *team.Team, first *int, after *pagination.Cursor, last *int, before *pagination.Cursor, orderBy *issue.IssueOrder, filter *issue.IssueFilter) (*pagination.Connection[issue.Issue], error) { +func (r *teamResolver) Issues(ctx context.Context, obj *team.Team, first *int, after *pagination.Cursor, last *int, before *pagination.Cursor, orderBy *issue.IssueOrder, filter *issue.IssueFilter) (*issue.IssueConnection, error) { page, err := pagination.ParsePage(first, after, last, before) if err != nil { return nil, err } - return issue.ListIssues(ctx, obj.Slug, page, orderBy, filter) + return issue.ListIssues(ctx, obj.Slug, page, orderBy, nil, filter) } func (r *unleashReleaseChannelIssueResolver) TeamEnvironment(ctx context.Context, obj *issue.UnleashReleaseChannelIssue) (*team.TeamEnvironment, error) { @@ -181,6 +185,10 @@ func (r *Resolver) InvalidSpecIssue() gengql.InvalidSpecIssueResolver { return &invalidSpecIssueResolver{r} } +func (r *Resolver) IssueConnection() gengql.IssueConnectionResolver { + return &issueConnectionResolver{r} +} + func (r *Resolver) LastRunFailedIssue() gengql.LastRunFailedIssueResolver { return &lastRunFailedIssueResolver{r} } @@ -226,6 +234,7 @@ type ( externalIngressCriticalVulnerabilityIssueResolver struct{ *Resolver } failedSynchronizationIssueResolver struct{ *Resolver } invalidSpecIssueResolver struct{ *Resolver } + issueConnectionResolver struct{ *Resolver } lastRunFailedIssueResolver struct{ *Resolver } missingSbomIssueResolver struct{ *Resolver } noRunningInstancesIssueResolver struct{ *Resolver } diff --git a/internal/graph/jobs.resolvers.go b/internal/graph/jobs.resolvers.go index 6cc3eda67..ed17c7def 100644 --- a/internal/graph/jobs.resolvers.go +++ b/internal/graph/jobs.resolvers.go @@ -73,24 +73,23 @@ func (r *jobResolver) State(ctx context.Context, obj *job.Job) (job.JobState, er return job.GetState(ctx, obj) } -func (r *jobResolver) Issues(ctx context.Context, obj *job.Job, first *int, after *pagination.Cursor, last *int, before *pagination.Cursor, orderBy *issue.IssueOrder, filter *issue.ResourceIssueFilter) (*pagination.Connection[issue.Issue], error) { +func (r *jobResolver) Issues(ctx context.Context, obj *job.Job, first *int, after *pagination.Cursor, last *int, before *pagination.Cursor, orderBy *issue.IssueOrder, filter *issue.ResourceIssueFilter) (*issue.IssueConnection, error) { page, err := pagination.ParsePage(first, after, last, before) if err != nil { return nil, err } - jobType := issue.ResourceTypeJob - f := &issue.IssueFilter{ - ResourceName: &obj.Name, - ResourceType: &jobType, - Environments: []string{obj.EnvironmentName}, + scope := &issue.IssueScope{ + ResourceName: obj.Name, + ResourceType: issue.ResourceTypeJob, + Env: obj.EnvironmentName, } + var f *issue.IssueFilter if filter != nil { - f.Severity = filter.Severity - f.IssueType = filter.IssueType + f = &issue.IssueFilter{ResourceIssueFilter: issue.ResourceIssueFilter{Severity: filter.Severity, IssueType: filter.IssueType}} } - return issue.ListIssues(ctx, obj.TeamSlug, page, orderBy, f) + return issue.ListIssues(ctx, obj.TeamSlug, page, orderBy, scope, f) } func (r *jobConnectionResolver) Facets(ctx context.Context, obj *pagination.FacetableConnection[*job.Job, *job.TeamJobsFilter]) (*job.JobFacets, error) { diff --git a/internal/graph/opensearch.resolvers.go b/internal/graph/opensearch.resolvers.go index 9de3ac5ab..06e8dd9ac 100644 --- a/internal/graph/opensearch.resolvers.go +++ b/internal/graph/opensearch.resolvers.go @@ -79,24 +79,23 @@ func (r *openSearchResolver) Version(ctx context.Context, obj *opensearch.OpenSe return opensearch.GetOpenSearchVersion(ctx, obj) } -func (r *openSearchResolver) Issues(ctx context.Context, obj *opensearch.OpenSearch, first *int, after *pagination.Cursor, last *int, before *pagination.Cursor, orderBy *issue.IssueOrder, filter *issue.ResourceIssueFilter) (*pagination.Connection[issue.Issue], error) { +func (r *openSearchResolver) Issues(ctx context.Context, obj *opensearch.OpenSearch, first *int, after *pagination.Cursor, last *int, before *pagination.Cursor, orderBy *issue.IssueOrder, filter *issue.ResourceIssueFilter) (*issue.IssueConnection, error) { page, err := pagination.ParsePage(first, after, last, before) if err != nil { return nil, err } - t := issue.ResourceTypeOpensearch - f := &issue.IssueFilter{ - ResourceName: &obj.Name, - ResourceType: &t, - Environments: []string{obj.EnvironmentName}, + scope := &issue.IssueScope{ + ResourceName: obj.Name, + ResourceType: issue.ResourceTypeOpensearch, + Env: obj.EnvironmentName, } + var f *issue.IssueFilter if filter != nil { - f.Severity = filter.Severity - f.IssueType = filter.IssueType + f = &issue.IssueFilter{ResourceIssueFilter: issue.ResourceIssueFilter{Severity: filter.Severity, IssueType: filter.IssueType}} } - return issue.ListIssues(ctx, obj.TeamSlug, page, orderBy, f) + return issue.ListIssues(ctx, obj.TeamSlug, page, orderBy, scope, f) } func (r *openSearchAccessResolver) Workload(ctx context.Context, obj *opensearch.OpenSearchAccess) (workload.Workload, error) { diff --git a/internal/graph/schema/alerts.graphqls b/internal/graph/schema/alerts.graphqls index 640503bf3..62b35da69 100644 --- a/internal/graph/schema/alerts.graphqls +++ b/internal/graph/schema/alerts.graphqls @@ -197,6 +197,34 @@ type AlertConnection { List of edges. """ edges: [AlertEdge!]! + + """ + Facets for alerts. Provides distribution counts to help narrow down results. + Facet counts are computed over the full result set (ignoring pagination) but respect the current filter. + """ + facets: AlertFacets +} + +""" +Facets for alerts, providing distribution counts across different dimensions. +""" +type AlertFacets { + "Distribution of alerts by environment." + environments: [StringFacetItem!]! + + "Distribution of alerts by state." + states: [AlertStateFacetItem!]! +} + +""" +A single facet item for alert states. +""" +type AlertStateFacetItem { + "The alert state." + state: AlertState! + + "Number of matching alerts." + count: Int! } """ diff --git a/internal/graph/schema/issues.graphqls b/internal/graph/schema/issues.graphqls index 6558ea2b1..81d65dbd6 100644 --- a/internal/graph/schema/issues.graphqls +++ b/internal/graph/schema/issues.graphqls @@ -76,6 +76,70 @@ type IssueConnection { "List of edges." edges: [IssueEdge!]! + + """ + Facets for the issues. Provides distribution counts to help narrow down results. + Facet counts are computed over the full result set (ignoring pagination) but respect the current filter. + """ + facets: IssueFacets +} + +""" +Facets for issues, providing distribution counts across different dimensions. +""" +type IssueFacets { + """ + Distribution of issues by environment. + """ + environments: [StringFacetItem!]! + + """ + Distribution of issues by severity. + """ + severities: [IssueSeverityFacetItem!]! + + """ + Distribution of issues by resource type. + """ + resourceTypes: [IssueResourceTypeFacetItem!]! + + """ + Distribution of issues by issue type. + """ + issueTypes: [IssueTypeFacetItem!]! +} + +""" +A single facet item for issue severities. +""" +type IssueSeverityFacetItem { + "The severity." + severity: Severity! + + "Number of matching issues." + count: Int! +} + +""" +A single facet item for resource types. +""" +type IssueResourceTypeFacetItem { + "The resource type." + resourceType: ResourceType! + + "Number of matching issues." + count: Int! +} + +""" +A single facet item for issue types. +""" +type IssueTypeFacetItem { + "The issue type." + issueType: IssueType! + + "Number of matching issues." + count: Int! } type IssueEdge { diff --git a/internal/graph/sqlinstance.resolvers.go b/internal/graph/sqlinstance.resolvers.go index baf764e10..71883ff46 100644 --- a/internal/graph/sqlinstance.resolvers.go +++ b/internal/graph/sqlinstance.resolvers.go @@ -96,24 +96,23 @@ func (r *sqlInstanceResolver) State(ctx context.Context, obj *sqlinstance.SQLIns return sqlinstance.GetState(ctx, obj.ProjectID, obj.Name) } -func (r *sqlInstanceResolver) Issues(ctx context.Context, obj *sqlinstance.SQLInstance, first *int, after *pagination.Cursor, last *int, before *pagination.Cursor, orderBy *issue.IssueOrder, filter *issue.ResourceIssueFilter) (*pagination.Connection[issue.Issue], error) { +func (r *sqlInstanceResolver) Issues(ctx context.Context, obj *sqlinstance.SQLInstance, first *int, after *pagination.Cursor, last *int, before *pagination.Cursor, orderBy *issue.IssueOrder, filter *issue.ResourceIssueFilter) (*issue.IssueConnection, error) { page, err := pagination.ParsePage(first, after, last, before) if err != nil { return nil, err } - t := issue.ResourceTypeSQLInstance - f := &issue.IssueFilter{ - ResourceName: &obj.Name, - ResourceType: &t, - Environments: []string{obj.EnvironmentName}, + scope := &issue.IssueScope{ + ResourceName: obj.Name, + ResourceType: issue.ResourceTypeSQLInstance, + Env: obj.EnvironmentName, } + var f *issue.IssueFilter if filter != nil { - f.Severity = filter.Severity - f.IssueType = filter.IssueType + f = &issue.IssueFilter{ResourceIssueFilter: issue.ResourceIssueFilter{Severity: filter.Severity, IssueType: filter.IssueType}} } - return issue.ListIssues(ctx, obj.TeamSlug, page, orderBy, f) + return issue.ListIssues(ctx, obj.TeamSlug, page, orderBy, scope, f) } func (r *sqlInstanceResolver) AuditLog(ctx context.Context, obj *sqlinstance.SQLInstance) (*sqlinstance.AuditLog, error) { diff --git a/internal/graph/valkey.resolvers.go b/internal/graph/valkey.resolvers.go index fbacd1c4e..a276b6973 100644 --- a/internal/graph/valkey.resolvers.go +++ b/internal/graph/valkey.resolvers.go @@ -94,24 +94,23 @@ func (r *valkeyResolver) State(ctx context.Context, obj *valkey.Valkey) (valkey. return valkey.State(ctx, obj) } -func (r *valkeyResolver) Issues(ctx context.Context, obj *valkey.Valkey, first *int, after *pagination.Cursor, last *int, before *pagination.Cursor, orderBy *issue.IssueOrder, filter *issue.ResourceIssueFilter) (*pagination.Connection[issue.Issue], error) { +func (r *valkeyResolver) Issues(ctx context.Context, obj *valkey.Valkey, first *int, after *pagination.Cursor, last *int, before *pagination.Cursor, orderBy *issue.IssueOrder, filter *issue.ResourceIssueFilter) (*issue.IssueConnection, error) { page, err := pagination.ParsePage(first, after, last, before) if err != nil { return nil, err } - t := issue.ResourceTypeValkey - f := &issue.IssueFilter{ - ResourceName: &obj.Name, - ResourceType: &t, - Environments: []string{obj.EnvironmentName}, + scope := &issue.IssueScope{ + ResourceName: obj.Name, + ResourceType: issue.ResourceTypeValkey, + Env: obj.EnvironmentName, } + var f *issue.IssueFilter if filter != nil { - f.Severity = filter.Severity - f.IssueType = filter.IssueType + f = &issue.IssueFilter{ResourceIssueFilter: issue.ResourceIssueFilter{Severity: filter.Severity, IssueType: filter.IssueType}} } - return issue.ListIssues(ctx, obj.TeamSlug, page, orderBy, f) + return issue.ListIssues(ctx, obj.TeamSlug, page, orderBy, scope, f) } func (r *valkeyAccessResolver) Workload(ctx context.Context, obj *valkey.ValkeyAccess) (workload.Workload, error) { diff --git a/internal/issue/facets.go b/internal/issue/facets.go new file mode 100644 index 000000000..4815ae968 --- /dev/null +++ b/internal/issue/facets.go @@ -0,0 +1,113 @@ +package issue + +import ( + "context" + "slices" + "strings" + + "github.com/nais/api/internal/graph/model" + "github.com/nais/api/internal/issue/issuesql" + "github.com/nais/api/internal/slug" +) + +func ComputeFacets(ctx context.Context, teamSlug slug.Slug, scope *IssueScope, filter *IssueFilter) (*IssueFacets, error) { + params := issuesql.FacetsForIssuesParams{ + Team: teamSlug.String(), + } + + if scope != nil { + params.ScopeResourceName = &scope.ResourceName + params.ScopeResourceType = (*string)(&scope.ResourceType) + params.ScopeEnv = &scope.Env + } + + if filter != nil { + if scope == nil { + if len(filter.Environments) > 0 { + params.Env = filter.Environments + } + params.FilterResourceType = (*string)(filter.ResourceType) + params.FilterResourceName = filter.ResourceName + } + params.IssueType = (*string)(filter.IssueType) + if filter.Severity != nil { + params.Severity = new(issuesql.SeverityLevel(*filter.Severity)) + } + } + + rows, err := db(ctx).FacetsForIssues(ctx, params) + if err != nil { + return nil, err + } + + return buildFacets(rows), nil +} + +func buildFacets(rows []*issuesql.FacetsForIssuesRow) *IssueFacets { + severityCounts := map[Severity]int{} + resourceTypeCounts := map[ResourceType]int{} + environmentCounts := map[string]int{} + issueTypeCounts := map[IssueType]int{} + + for _, row := range rows { + sev := Severity(row.Severity) + rt := ResourceType(row.ResourceType) + it := IssueType(row.IssueType) + + if _, ok := severityCounts[sev]; !ok { + severityCounts[sev] = 0 + } + if _, ok := resourceTypeCounts[rt]; !ok { + resourceTypeCounts[rt] = 0 + } + if _, ok := environmentCounts[row.Env]; !ok { + environmentCounts[row.Env] = 0 + } + if _, ok := issueTypeCounts[it]; !ok { + issueTypeCounts[it] = 0 + } + + filtered := int(row.FilteredCount) + severityCounts[sev] += filtered + resourceTypeCounts[rt] += filtered + environmentCounts[row.Env] += filtered + issueTypeCounts[it] += filtered + } + + severities := make([]IssueSeverityFacetItem, 0, len(severityCounts)) + for sev, count := range severityCounts { + severities = append(severities, IssueSeverityFacetItem{Severity: sev, Count: count}) + } + slices.SortFunc(severities, func(a, b IssueSeverityFacetItem) int { + return strings.Compare(a.Severity.String(), b.Severity.String()) + }) + + resourceTypes := make([]IssueResourceTypeFacetItem, 0, len(resourceTypeCounts)) + for rt, count := range resourceTypeCounts { + resourceTypes = append(resourceTypes, IssueResourceTypeFacetItem{ResourceType: rt, Count: count}) + } + slices.SortFunc(resourceTypes, func(a, b IssueResourceTypeFacetItem) int { + return strings.Compare(a.ResourceType.String(), b.ResourceType.String()) + }) + + environments := make([]model.StringFacetItem, 0, len(environmentCounts)) + for env, count := range environmentCounts { + environments = append(environments, model.StringFacetItem{Value: env, Count: count}) + } + model.SortStringFacetItems(environments) + + issueTypes := make([]IssueTypeFacetItem, 0, len(issueTypeCounts)) + for it, count := range issueTypeCounts { + issueTypes = append(issueTypes, IssueTypeFacetItem{IssueType: it, Count: count}) + } + slices.SortFunc(issueTypes, func(a, b IssueTypeFacetItem) int { + return strings.Compare(a.IssueType.String(), b.IssueType.String()) + }) + + return &IssueFacets{ + Environments: environments, + Severities: severities, + ResourceTypes: resourceTypes, + IssueTypes: issueTypes, + } +} diff --git a/internal/issue/facets_test.go b/internal/issue/facets_test.go new file mode 100644 index 000000000..f2eb4430f --- /dev/null +++ b/internal/issue/facets_test.go @@ -0,0 +1,101 @@ +package issue + +import ( + "reflect" + "testing" + + "github.com/nais/api/internal/graph/model" + "github.com/nais/api/internal/issue/issuesql" +) + +func TestBuildFacets(t *testing.T) { + rows := []*issuesql.FacetsForIssuesRow{ + {Severity: "CRITICAL", ResourceType: "APPLICATION", Env: "prod", IssueType: "DEPRECATED_INGRESS", FilteredCount: 3}, + {Severity: "CRITICAL", ResourceType: "APPLICATION", Env: "dev", IssueType: "DEPRECATED_INGRESS", FilteredCount: 1}, + {Severity: "WARNING", ResourceType: "JOB", Env: "prod", IssueType: "LAST_RUN_FAILED", FilteredCount: 2}, + {Severity: "TODO", ResourceType: "SQLINSTANCE", Env: "prod", IssueType: "SQLINSTANCE_VERSION", FilteredCount: 0}, + } + + tests := []struct { + name string + rows []*issuesql.FacetsForIssuesRow + wantSeverities []IssueSeverityFacetItem + wantResourceTypes []IssueResourceTypeFacetItem + wantEnvironments []model.StringFacetItem + wantIssueTypes []IssueTypeFacetItem + }{ + { + name: "mixed rows: seeded values with zero filtered_count are included", + rows: rows, + wantSeverities: []IssueSeverityFacetItem{ + {Severity: SeverityCritical, Count: 4}, + {Severity: SeverityTodo, Count: 0}, + {Severity: SeverityWarning, Count: 2}, + }, + wantResourceTypes: []IssueResourceTypeFacetItem{ + {ResourceType: ResourceTypeApplication, Count: 4}, + {ResourceType: ResourceTypeJob, Count: 2}, + {ResourceType: ResourceTypeSQLInstance, Count: 0}, + }, + wantEnvironments: []model.StringFacetItem{ + {Value: "dev", Count: 1}, + {Value: "prod", Count: 5}, + }, + wantIssueTypes: []IssueTypeFacetItem{ + {IssueType: IssueTypeDeprecatedIngress, Count: 4}, + {IssueType: IssueTypeLastRunFailed, Count: 2}, + {IssueType: IssueTypeSqlInstanceVersion, Count: 0}, + }, + }, + { + name: "with filter: TODO row has filtered_count=0 but is still seeded", + rows: []*issuesql.FacetsForIssuesRow{ + {Severity: "CRITICAL", ResourceType: "APPLICATION", Env: "prod", IssueType: "DEPRECATED_INGRESS", FilteredCount: 3}, + {Severity: "WARNING", ResourceType: "JOB", Env: "dev", IssueType: "LAST_RUN_FAILED", FilteredCount: 0}, + }, + wantSeverities: []IssueSeverityFacetItem{ + {Severity: SeverityCritical, Count: 3}, + {Severity: SeverityWarning, Count: 0}, + }, + wantResourceTypes: []IssueResourceTypeFacetItem{ + {ResourceType: ResourceTypeApplication, Count: 3}, + {ResourceType: ResourceTypeJob, Count: 0}, + }, + wantEnvironments: []model.StringFacetItem{ + {Value: "dev", Count: 0}, + {Value: "prod", Count: 3}, + }, + wantIssueTypes: []IssueTypeFacetItem{ + {IssueType: IssueTypeDeprecatedIngress, Count: 3}, + {IssueType: IssueTypeLastRunFailed, Count: 0}, + }, + }, + { + name: "empty rows returns empty facets", + rows: nil, + wantSeverities: []IssueSeverityFacetItem{}, + wantResourceTypes: []IssueResourceTypeFacetItem{}, + wantEnvironments: []model.StringFacetItem{}, + wantIssueTypes: []IssueTypeFacetItem{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := buildFacets(tt.rows) + + if !reflect.DeepEqual(got.Severities, tt.wantSeverities) { + t.Errorf("Severities =\n %v\nwant\n %v", got.Severities, tt.wantSeverities) + } + if !reflect.DeepEqual(got.ResourceTypes, tt.wantResourceTypes) { + t.Errorf("ResourceTypes =\n %v\nwant\n %v", got.ResourceTypes, tt.wantResourceTypes) + } + if !reflect.DeepEqual(got.Environments, tt.wantEnvironments) { + t.Errorf("Environments =\n %v\nwant\n %v", got.Environments, tt.wantEnvironments) + } + if !reflect.DeepEqual(got.IssueTypes, tt.wantIssueTypes) { + t.Errorf("IssueTypes =\n %v\nwant\n %v", got.IssueTypes, tt.wantIssueTypes) + } + }) + } +} diff --git a/internal/issue/issuesql/issue.sql.go b/internal/issue/issuesql/issue.sql.go index 8cf588b2d..3aae4a915 100644 --- a/internal/issue/issuesql/issue.sql.go +++ b/internal/issue/issuesql/issue.sql.go @@ -10,6 +10,119 @@ import ( "github.com/jackc/pgx/v5/pgtype" ) +const facetsForIssues = `-- name: FacetsForIssues :many +SELECT + severity, + resource_type, + env, + issue_type, + COUNT(*) FILTER ( + WHERE + ( + $1::TEXT[] IS NULL + OR env = ANY ($1::TEXT[]) + ) + AND ( + $2::TEXT IS NULL + OR issue_type = $2::TEXT + ) + AND ( + $3::severity_level IS NULL + OR severity = $3::severity_level + ) + AND ( + $4::TEXT IS NULL + OR resource_type = $4::TEXT + ) + AND ( + $5::TEXT IS NULL + OR resource_name = $5::TEXT + ) + ) AS filtered_count +FROM + issues +WHERE + team = $6 + AND ( + $7::TEXT IS NULL + OR resource_type = $7::TEXT + ) + AND ( + $8::TEXT IS NULL + OR resource_name = $8::TEXT + ) + AND ( + $9::TEXT IS NULL + OR env = $9::TEXT + ) +GROUP BY + severity, + resource_type, + env, + issue_type +ORDER BY + severity, + resource_type, + env, + issue_type +` + +type FacetsForIssuesParams struct { + Env []string + IssueType *string + Severity *SeverityLevel + FilterResourceType *string + FilterResourceName *string + Team string + ScopeResourceType *string + ScopeResourceName *string + ScopeEnv *string +} + +type FacetsForIssuesRow struct { + Severity SeverityLevel + ResourceType string + Env string + IssueType string + FilteredCount int64 +} + +func (q *Queries) FacetsForIssues(ctx context.Context, arg FacetsForIssuesParams) ([]*FacetsForIssuesRow, error) { + rows, err := q.db.Query(ctx, facetsForIssues, + arg.Env, + arg.IssueType, + arg.Severity, + arg.FilterResourceType, + arg.FilterResourceName, + arg.Team, + arg.ScopeResourceType, + arg.ScopeResourceName, + arg.ScopeEnv, + ) + if err != nil { + return nil, err + } + defer rows.Close() + items := []*FacetsForIssuesRow{} + for rows.Next() { + var i FacetsForIssuesRow + if err := rows.Scan( + &i.Severity, + &i.ResourceType, + &i.Env, + &i.IssueType, + &i.FilteredCount, + ); err != nil { + return nil, err + } + items = append(items, &i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const getIssueByID = `-- name: GetIssueByID :one SELECT id, issue_type, resource_name, resource_type, team, env, severity, message, issue_details, created_at diff --git a/internal/issue/issuesql/querier.go b/internal/issue/issuesql/querier.go index a9e20b561..96a86d9bb 100644 --- a/internal/issue/issuesql/querier.go +++ b/internal/issue/issuesql/querier.go @@ -9,6 +9,7 @@ import ( ) type Querier interface { + FacetsForIssues(ctx context.Context, arg FacetsForIssuesParams) ([]*FacetsForIssuesRow, error) GetIssueByID(ctx context.Context, id uuid.UUID) (*Issue, error) GetSeverityScoreForWorkload(ctx context.Context, arg GetSeverityScoreForWorkloadParams) (int64, error) ListIssues(ctx context.Context, arg ListIssuesParams) ([]*ListIssuesRow, error) diff --git a/internal/issue/model.go b/internal/issue/model.go index b6ebf41b1..9a6e9b10a 100644 --- a/internal/issue/model.go +++ b/internal/issue/model.go @@ -266,10 +266,49 @@ func (e IssueType) MarshalGQL(w io.Writer) { } type ( - IssueConnection = pagination.Connection[Issue] - IssueEdge = pagination.Edge[Issue] + IssueEdge = pagination.Edge[Issue] ) +type IssueScope struct { + ResourceName string + ResourceType ResourceType + Env string +} + +type IssueConnection struct { + pagination.Connection[Issue] + + teamSlug slug.Slug + scope *IssueScope + filter *IssueFilter +} + +func (c *IssueConnection) GetTeamSlug() slug.Slug { return c.teamSlug } +func (c *IssueConnection) GetScope() *IssueScope { return c.scope } +func (c *IssueConnection) GetFilter() *IssueFilter { return c.filter } + +type IssueFacets struct { + Environments []model.StringFacetItem `json:"environments"` + Severities []IssueSeverityFacetItem `json:"severities"` + ResourceTypes []IssueResourceTypeFacetItem `json:"resourceTypes"` + IssueTypes []IssueTypeFacetItem `json:"issueTypes"` +} + +type IssueSeverityFacetItem struct { + Severity Severity `json:"severity"` + Count int `json:"count"` +} + +type IssueResourceTypeFacetItem struct { + ResourceType ResourceType `json:"resourceType"` + Count int `json:"count"` +} + +type IssueTypeFacetItem struct { + IssueType IssueType `json:"issueType"` + Count int `json:"count"` +} + type IssueOrder struct { // Order by this field. Field IssueOrderField `json:"field"` diff --git a/internal/issue/queries.go b/internal/issue/queries.go index 70bb4307b..ad1ab7401 100644 --- a/internal/issue/queries.go +++ b/internal/issue/queries.go @@ -43,7 +43,7 @@ func GetByIdent(ctx context.Context, id ident.Ident) (Issue, error) { return convert(issue) } -func ListIssues(ctx context.Context, teamSlug slug.Slug, page *pagination.Pagination, orderBy *IssueOrder, filter *IssueFilter) (*pagination.Connection[Issue], error) { +func ListIssues(ctx context.Context, teamSlug slug.Slug, page *pagination.Pagination, orderBy *IssueOrder, scope *IssueScope, filter *IssueFilter) (*IssueConnection, error) { params := issuesql.ListIssuesParams{ Team: teamSlug.String(), Offset: page.Offset(), @@ -51,11 +51,21 @@ func ListIssues(ctx context.Context, teamSlug slug.Slug, page *pagination.Pagina OrderBy: orderBy.String(), } + if scope != nil { + params.ResourceName = &scope.ResourceName + params.ResourceType = (*string)(&scope.ResourceType) + params.Env = []string{scope.Env} + } + if filter != nil { - params.Env = filter.Environments - params.ResourceType = (*string)(filter.ResourceType) + if scope == nil { + if len(filter.Environments) > 0 { + params.Env = filter.Environments + } + params.ResourceType = (*string)(filter.ResourceType) + params.ResourceName = filter.ResourceName + } params.IssueType = (*string)(filter.IssueType) - params.ResourceName = filter.ResourceName if filter.Severity != nil { params.Severity = new(issuesql.SeverityLevel(*filter.Severity)) } @@ -71,7 +81,7 @@ func ListIssues(ctx context.Context, teamSlug slug.Slug, page *pagination.Pagina total = ret[0].TotalCount } - return pagination.NewConvertConnectionWithError(ret, page, total, func(from *issuesql.ListIssuesRow) (Issue, error) { + conn, err := pagination.NewConvertConnectionWithError(ret, page, total, func(from *issuesql.ListIssuesRow) (Issue, error) { iss := &issuesql.Issue{ ID: from.ID, IssueType: from.IssueType, @@ -86,6 +96,16 @@ func ListIssues(ctx context.Context, teamSlug slug.Slug, page *pagination.Pagina } return convert(iss) }) + if err != nil { + return nil, err + } + + return &IssueConnection{ + Connection: *conn, + teamSlug: teamSlug, + scope: scope, + filter: filter, + }, nil } func convert(issue *issuesql.Issue) (Issue, error) { diff --git a/internal/issue/queries/issue.sql b/internal/issue/queries/issue.sql index 036cecfd0..68e68f9f4 100644 --- a/internal/issue/queries/issue.sql +++ b/internal/issue/queries/issue.sql @@ -92,3 +92,60 @@ OFFSET LIMIT sqlc.arg('limit') ; + +-- name: FacetsForIssues :many +SELECT + severity, + resource_type, + env, + issue_type, + COUNT(*) FILTER ( + WHERE + ( + sqlc.narg('env')::TEXT[] IS NULL + OR env = ANY (sqlc.narg('env')::TEXT[]) + ) + AND ( + sqlc.narg('issue_type')::TEXT IS NULL + OR issue_type = sqlc.narg('issue_type')::TEXT + ) + AND ( + sqlc.narg('severity')::severity_level IS NULL + OR severity = sqlc.narg('severity')::severity_level + ) + AND ( + sqlc.narg('filter_resource_type')::TEXT IS NULL + OR resource_type = sqlc.narg('filter_resource_type')::TEXT + ) + AND ( + sqlc.narg('filter_resource_name')::TEXT IS NULL + OR resource_name = sqlc.narg('filter_resource_name')::TEXT + ) + ) AS filtered_count +FROM + issues +WHERE + team = @team + AND ( + sqlc.narg('scope_resource_type')::TEXT IS NULL + OR resource_type = sqlc.narg('scope_resource_type')::TEXT + ) + AND ( + sqlc.narg('scope_resource_name')::TEXT IS NULL + OR resource_name = sqlc.narg('scope_resource_name')::TEXT + ) + AND ( + sqlc.narg('scope_env')::TEXT IS NULL + OR env = sqlc.narg('scope_env')::TEXT + ) +GROUP BY + severity, + resource_type, + env, + issue_type +ORDER BY + severity, + resource_type, + env, + issue_type +;