Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions internal/alerts/facets.go
Original file line number Diff line number Diff line change
@@ -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
}
137 changes: 137 additions & 0 deletions internal/alerts/facets_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
})
}
}
15 changes: 14 additions & 1 deletion internal/alerts/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"fmt"
"io"
"strconv"
"sync"
"time"

"github.com/nais/api/internal/graph/ident"
Expand All @@ -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
Expand Down
40 changes: 20 additions & 20 deletions internal/graph/alerts.resolvers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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 }
)
17 changes: 8 additions & 9 deletions internal/graph/applications.resolvers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Loading
Loading