diff --git a/backend/apid/graphql/environment.go b/backend/apid/graphql/environment.go index edf5c0ef22..ceb8b47c14 100644 --- a/backend/apid/graphql/environment.go +++ b/backend/apid/graphql/environment.go @@ -3,6 +3,7 @@ package graphql import ( "errors" "sort" + "strings" "github.com/sensu/sensu-go/backend/apid/actions" "github.com/sensu/sensu-go/backend/apid/graphql/globalid" @@ -11,6 +12,7 @@ import ( "github.com/sensu/sensu-go/graphql" "github.com/sensu/sensu-go/types" "github.com/sensu/sensu-go/util/eval" + string_utils "github.com/sensu/sensu-go/util/strings" ) var _ schema.EnvironmentFieldResolvers = (*envImpl)(nil) @@ -283,3 +285,49 @@ func (r *envImpl) CheckHistory(p schema.EnvironmentCheckHistoryFieldResolverPara limit := clampInt(p.Args.Limit, 0, len(history)) return history[0:limit], nil } + +// Subscriptions implements response to request for 'subscriptions' field. +func (r *envImpl) Subscriptions(p schema.EnvironmentSubscriptionsFieldResolverParams) (interface{}, error) { + set := string_utils.OccurrenceSet{} + env := p.Source.(*types.Environment) + ctx := types.SetContextFromResource(p.Context, env) + + entities, err := r.entityCtrl.Query(ctx) + if err != nil { + return set, err + } + for _, entity := range entities { + newSet := occurrencesOfSubscriptions(entity) + set.Merge(newSet) + } + + checks, err := r.checksCtrl.Query(ctx) + if err != nil { + return set, err + } + for _, check := range checks { + newSet := occurrencesOfSubscriptions(check) + set.Merge(newSet) + } + + // If specified omit subscriptions prefix'd with 'entity:' + if p.Args.OmitEntity { + for _, subscription := range set.Values() { + if strings.HasPrefix(subscription, "entity:") { + set.Remove(subscription) + } + } + } + + // Sort entries + subscriptionSet := newSubscriptionSet(set) + if p.Args.OrderBy == schema.SubscriptionSetOrders.ALPHA_DESC { + subscriptionSet.sortByAlpha(false) + } else if p.Args.OrderBy == schema.SubscriptionSetOrders.ALPHA_ASC { + subscriptionSet.sortByAlpha(true) + } else if p.Args.OrderBy == schema.SubscriptionSetOrders.OCCURRENCES { + subscriptionSet.sortByOccurrence() + } + + return subscriptionSet, nil +} diff --git a/backend/apid/graphql/schema/environment.gql.go b/backend/apid/graphql/schema/environment.gql.go index d865bd4b9a..bb81dd5616 100644 --- a/backend/apid/graphql/schema/environment.gql.go +++ b/backend/apid/graphql/schema/environment.gql.go @@ -42,8 +42,8 @@ type EnvironmentOrganizationFieldResolver interface { // EnvironmentChecksFieldResolverArgs contains arguments provided to checks when selected type EnvironmentChecksFieldResolverArgs struct { Offset int // Offset - self descriptive - Limit int // Limit - self descriptive - OrderBy CheckListOrder // OrderBy - self descriptive + Limit int // Limit adds optional limit to the number of entries returned. + OrderBy CheckListOrder // OrderBy adds optional order to the records retrieved. Filter string // Filter reduces the set using the given Sensu Query Expression predicate. } @@ -62,8 +62,8 @@ type EnvironmentChecksFieldResolver interface { // EnvironmentEntitiesFieldResolverArgs contains arguments provided to entities when selected type EnvironmentEntitiesFieldResolverArgs struct { Offset int // Offset - self descriptive - Limit int // Limit - self descriptive - OrderBy EntityListOrder // OrderBy - self descriptive + Limit int // Limit adds optional limit to the number of entries returned. + OrderBy EntityListOrder // OrderBy adds optional order to the records retrieved. Filter string // Filter reduces the set using the given Sensu Query Expression predicate. } @@ -82,8 +82,8 @@ type EnvironmentEntitiesFieldResolver interface { // EnvironmentEventsFieldResolverArgs contains arguments provided to events when selected type EnvironmentEventsFieldResolverArgs struct { Offset int // Offset - self descriptive - Limit int // Limit - self descriptive - OrderBy EventsListOrder // OrderBy - self descriptive + Limit int // Limit adds optional limit to the number of entries returned. + OrderBy EventsListOrder // OrderBy adds optional order to the records retrieved. Filter string // Filter reduces the set using the given Sensu Query Expression predicate. } @@ -102,7 +102,7 @@ type EnvironmentEventsFieldResolver interface { // EnvironmentSilencesFieldResolverArgs contains arguments provided to silences when selected type EnvironmentSilencesFieldResolverArgs struct { Offset int // Offset - self descriptive - Limit int // Limit - self descriptive + Limit int // Limit adds optional limit to the number of entries returned. } // EnvironmentSilencesFieldResolverParams contains contextual info to resolve silences field @@ -117,6 +117,24 @@ type EnvironmentSilencesFieldResolver interface { Silences(p EnvironmentSilencesFieldResolverParams) (interface{}, error) } +// EnvironmentSubscriptionsFieldResolverArgs contains arguments provided to subscriptions when selected +type EnvironmentSubscriptionsFieldResolverArgs struct { + OmitEntity bool // OmitEntity - Omit entity subscriptions from set. + OrderBy SubscriptionSetOrder // OrderBy adds optional order to the records retrieved. +} + +// EnvironmentSubscriptionsFieldResolverParams contains contextual info to resolve subscriptions field +type EnvironmentSubscriptionsFieldResolverParams struct { + graphql.ResolveParams + Args EnvironmentSubscriptionsFieldResolverArgs +} + +// EnvironmentSubscriptionsFieldResolver implement to resolve requests for the Environment's subscriptions field. +type EnvironmentSubscriptionsFieldResolver interface { + // Subscriptions implements response to request for subscriptions field. + Subscriptions(p EnvironmentSubscriptionsFieldResolverParams) (interface{}, error) +} + // EnvironmentCheckHistoryFieldResolverArgs contains arguments provided to checkHistory when selected type EnvironmentCheckHistoryFieldResolverArgs struct { Filter string // Filter reduces the set using the given Sensu Query Expression predicate. @@ -206,6 +224,7 @@ type EnvironmentFieldResolvers interface { EnvironmentEntitiesFieldResolver EnvironmentEventsFieldResolver EnvironmentSilencesFieldResolver + EnvironmentSubscriptionsFieldResolver EnvironmentCheckHistoryFieldResolver } @@ -314,6 +333,12 @@ func (_ EnvironmentAliases) Silences(p EnvironmentSilencesFieldResolverParams) ( return val, err } +// Subscriptions implements response to request for 'subscriptions' field. +func (_ EnvironmentAliases) Subscriptions(p EnvironmentSubscriptionsFieldResolverParams) (interface{}, error) { + val, err := graphql.DefaultResolver(p.Source, p.Info.FieldName) + return val, err +} + // CheckHistory implements response to request for 'checkHistory' field. func (_ EnvironmentAliases) CheckHistory(p EnvironmentCheckHistoryFieldResolverParams) (interface{}, error) { val, err := graphql.DefaultResolver(p.Source, p.Info.FieldName) @@ -416,6 +441,19 @@ func _ObjTypeEnvironmentSilencesHandler(impl interface{}) graphql1.FieldResolveF } } +func _ObjTypeEnvironmentSubscriptionsHandler(impl interface{}) graphql1.FieldResolveFn { + resolver := impl.(EnvironmentSubscriptionsFieldResolver) + return func(p graphql1.ResolveParams) (interface{}, error) { + frp := EnvironmentSubscriptionsFieldResolverParams{ResolveParams: p} + err := mapstructure.Decode(p.Args, &frp.Args) + if err != nil { + return nil, err + } + + return resolver.Subscriptions(frp) + } +} + func _ObjTypeEnvironmentCheckHistoryHandler(impl interface{}) graphql1.FieldResolveFn { resolver := impl.(EnvironmentCheckHistoryFieldResolver) return func(p graphql1.ResolveParams) (interface{}, error) { @@ -460,7 +498,7 @@ func _ObjectTypeEnvironmentConfigFn() graphql1.ObjectConfig { }, "limit": &graphql1.ArgumentConfig{ DefaultValue: 10, - Description: "self descriptive", + Description: "Limit adds optional limit to the number of entries returned.", Type: graphql1.Int, }, "offset": &graphql1.ArgumentConfig{ @@ -470,7 +508,7 @@ func _ObjectTypeEnvironmentConfigFn() graphql1.ObjectConfig { }, "orderBy": &graphql1.ArgumentConfig{ DefaultValue: "NAME_DESC", - Description: "self descriptive", + Description: "OrderBy adds optional order to the records retrieved.", Type: graphql.InputType("CheckListOrder"), }, }, @@ -502,7 +540,7 @@ func _ObjectTypeEnvironmentConfigFn() graphql1.ObjectConfig { }, "limit": &graphql1.ArgumentConfig{ DefaultValue: 10, - Description: "self descriptive", + Description: "Limit adds optional limit to the number of entries returned.", Type: graphql1.Int, }, "offset": &graphql1.ArgumentConfig{ @@ -512,7 +550,7 @@ func _ObjectTypeEnvironmentConfigFn() graphql1.ObjectConfig { }, "orderBy": &graphql1.ArgumentConfig{ DefaultValue: "ID_DESC", - Description: "self descriptive", + Description: "OrderBy adds optional order to the records retrieved.", Type: graphql.InputType("EntityListOrder"), }, }, @@ -530,7 +568,7 @@ func _ObjectTypeEnvironmentConfigFn() graphql1.ObjectConfig { }, "limit": &graphql1.ArgumentConfig{ DefaultValue: 10, - Description: "self descriptive", + Description: "Limit adds optional limit to the number of entries returned.", Type: graphql1.Int, }, "offset": &graphql1.ArgumentConfig{ @@ -540,7 +578,7 @@ func _ObjectTypeEnvironmentConfigFn() graphql1.ObjectConfig { }, "orderBy": &graphql1.ArgumentConfig{ DefaultValue: "SEVERITY", - Description: "self descriptive", + Description: "OrderBy adds optional order to the records retrieved.", Type: graphql.InputType("EventsListOrder"), }, }, @@ -574,7 +612,7 @@ func _ObjectTypeEnvironmentConfigFn() graphql1.ObjectConfig { Args: graphql1.FieldConfigArgument{ "limit": &graphql1.ArgumentConfig{ DefaultValue: 10, - Description: "self descriptive", + Description: "Limit adds optional limit to the number of entries returned.", Type: graphql1.Int, }, "offset": &graphql1.ArgumentConfig{ @@ -588,6 +626,24 @@ func _ObjectTypeEnvironmentConfigFn() graphql1.ObjectConfig { Name: "silences", Type: graphql1.NewNonNull(graphql.OutputType("SilencedConnection")), }, + "subscriptions": &graphql1.Field{ + Args: graphql1.FieldConfigArgument{ + "omitEntity": &graphql1.ArgumentConfig{ + DefaultValue: false, + Description: "Omit entity subscriptions from set.", + Type: graphql1.Boolean, + }, + "orderBy": &graphql1.ArgumentConfig{ + DefaultValue: "OCCURRENCES", + Description: "OrderBy adds optional order to the records retrieved.", + Type: graphql.InputType("SubscriptionSetOrder"), + }, + }, + DeprecationReason: "", + Description: "All subscriptions in use in the environment.", + Name: "subscriptions", + Type: graphql1.NewNonNull(graphql.OutputType("SubscriptionSet")), + }, }, Interfaces: []*graphql1.Interface{ graphql.Interface("Node")}, @@ -607,19 +663,73 @@ func _ObjectTypeEnvironmentConfigFn() graphql1.ObjectConfig { var _ObjectTypeEnvironmentDesc = graphql.ObjectDesc{ Config: _ObjectTypeEnvironmentConfigFn, FieldHandlers: map[string]graphql.FieldHandler{ - "checkHistory": _ObjTypeEnvironmentCheckHistoryHandler, - "checks": _ObjTypeEnvironmentChecksHandler, - "colourId": _ObjTypeEnvironmentColourIDHandler, - "description": _ObjTypeEnvironmentDescriptionHandler, - "entities": _ObjTypeEnvironmentEntitiesHandler, - "events": _ObjTypeEnvironmentEventsHandler, - "id": _ObjTypeEnvironmentIDHandler, - "name": _ObjTypeEnvironmentNameHandler, - "organization": _ObjTypeEnvironmentOrganizationHandler, - "silences": _ObjTypeEnvironmentSilencesHandler, + "checkHistory": _ObjTypeEnvironmentCheckHistoryHandler, + "checks": _ObjTypeEnvironmentChecksHandler, + "colourId": _ObjTypeEnvironmentColourIDHandler, + "description": _ObjTypeEnvironmentDescriptionHandler, + "entities": _ObjTypeEnvironmentEntitiesHandler, + "events": _ObjTypeEnvironmentEventsHandler, + "id": _ObjTypeEnvironmentIDHandler, + "name": _ObjTypeEnvironmentNameHandler, + "organization": _ObjTypeEnvironmentOrganizationHandler, + "silences": _ObjTypeEnvironmentSilencesHandler, + "subscriptions": _ObjTypeEnvironmentSubscriptionsHandler, }, } +// SubscriptionSetOrder Describes ways in which a set of subscriptions can be ordered. +type SubscriptionSetOrder string + +// SubscriptionSetOrders holds enum values +var SubscriptionSetOrders = _EnumTypeSubscriptionSetOrderValues{ + ALPHA_ASC: "ALPHA_ASC", + ALPHA_DESC: "ALPHA_DESC", + OCCURRENCES: "OCCURRENCES", +} + +// SubscriptionSetOrderType Describes ways in which a set of subscriptions can be ordered. +var SubscriptionSetOrderType = graphql.NewType("SubscriptionSetOrder", graphql.EnumKind) + +// RegisterSubscriptionSetOrder registers SubscriptionSetOrder object type with given service. +func RegisterSubscriptionSetOrder(svc *graphql.Service) { + svc.RegisterEnum(_EnumTypeSubscriptionSetOrderDesc) +} +func _EnumTypeSubscriptionSetOrderConfigFn() graphql1.EnumConfig { + return graphql1.EnumConfig{ + Description: "Describes ways in which a set of subscriptions can be ordered.", + Name: "SubscriptionSetOrder", + Values: graphql1.EnumValueConfigMap{ + "ALPHA_ASC": &graphql1.EnumValueConfig{ + DeprecationReason: "", + Description: "self descriptive", + Value: "ALPHA_ASC", + }, + "ALPHA_DESC": &graphql1.EnumValueConfig{ + DeprecationReason: "", + Description: "self descriptive", + Value: "ALPHA_DESC", + }, + "OCCURRENCES": &graphql1.EnumValueConfig{ + DeprecationReason: "", + Description: "self descriptive", + Value: "OCCURRENCES", + }, + }, + } +} + +// describe SubscriptionSetOrder's configuration; kept private to avoid unintentional tampering of configuration at runtime. +var _EnumTypeSubscriptionSetOrderDesc = graphql.EnumDesc{Config: _EnumTypeSubscriptionSetOrderConfigFn} + +type _EnumTypeSubscriptionSetOrderValues struct { + // ALPHA_ASC - self descriptive + ALPHA_ASC SubscriptionSetOrder + // ALPHA_DESC - self descriptive + ALPHA_DESC SubscriptionSetOrder + // OCCURRENCES - self descriptive + OCCURRENCES SubscriptionSetOrder +} + // CheckListOrder self descriptive type CheckListOrder string diff --git a/backend/apid/graphql/schema/environment.graphql b/backend/apid/graphql/schema/environment.graphql index a0ae69b83d..e6e3a55109 100644 --- a/backend/apid/graphql/schema/environment.graphql +++ b/backend/apid/graphql/schema/environment.graphql @@ -20,7 +20,9 @@ type Environment implements Node { "All check configurations associated with the environment." checks( offset: Int = 0, + "Limit adds optional limit to the number of entries returned." limit: Int = 10, + "OrderBy adds optional order to the records retrieved." orderBy: CheckListOrder = NAME_DESC "Filter reduces the set using the given Sensu Query Expression predicate." filter: String = "", @@ -29,7 +31,9 @@ type Environment implements Node { "All entities associated with the environment." entities( offset: Int = 0, + "Limit adds optional limit to the number of entries returned." limit: Int = 10, + "OrderBy adds optional order to the records retrieved." orderBy: EntityListOrder = ID_DESC "Filter reduces the set using the given Sensu Query Expression predicate." filter: String = "", @@ -38,14 +42,28 @@ type Environment implements Node { "All events associated with the environment." events( offset: Int = 0, + "Limit adds optional limit to the number of entries returned." limit: Int = 10, + "OrderBy adds optional order to the records retrieved." orderBy: EventsListOrder = SEVERITY "Filter reduces the set using the given Sensu Query Expression predicate." filter: String = "", ): EventConnection! "All silences associated with the environment." - silences(offset: Int = 0, limit: Int = 10): SilencedConnection! + silences( + offset: Int = 0 + "Limit adds optional limit to the number of entries returned." + limit: Int = 10 + ): SilencedConnection! + + "All subscriptions in use in the environment." + subscriptions( + "Omit entity subscriptions from set." + omitEntity: Boolean = false + "OrderBy adds optional order to the records retrieved." + orderBy: SubscriptionSetOrder = OCCURRENCES + ): SubscriptionSet! """ checkHistory includes all persisted check execution results associated with @@ -60,6 +78,13 @@ type Environment implements Node { ): [CheckHistory]! } +"Describes ways in which a set of subscriptions can be ordered." +enum SubscriptionSetOrder { + ALPHA_ASC + ALPHA_DESC + OCCURRENCES +} + enum CheckListOrder { NAME NAME_DESC diff --git a/backend/apid/graphql/schema/subscriptions.gql.go b/backend/apid/graphql/schema/subscriptions.gql.go new file mode 100644 index 0000000000..9512dcc19c --- /dev/null +++ b/backend/apid/graphql/schema/subscriptions.gql.go @@ -0,0 +1,498 @@ +// Code generated by scripts/gengraphql.go. DO NOT EDIT. + +package schema + +import ( + fmt "fmt" + graphql1 "github.com/graphql-go/graphql" + mapstructure "github.com/mitchellh/mapstructure" + graphql "github.com/sensu/sensu-go/graphql" +) + +// SubscriptionSetEntriesFieldResolverArgs contains arguments provided to entries when selected +type SubscriptionSetEntriesFieldResolverArgs struct { + Limit int // Limit - self descriptive + Offset int // Offset - self descriptive +} + +// SubscriptionSetEntriesFieldResolverParams contains contextual info to resolve entries field +type SubscriptionSetEntriesFieldResolverParams struct { + graphql.ResolveParams + Args SubscriptionSetEntriesFieldResolverArgs +} + +// SubscriptionSetEntriesFieldResolver implement to resolve requests for the SubscriptionSet's entries field. +type SubscriptionSetEntriesFieldResolver interface { + // Entries implements response to request for entries field. + Entries(p SubscriptionSetEntriesFieldResolverParams) (interface{}, error) +} + +// SubscriptionSetValuesFieldResolverArgs contains arguments provided to values when selected +type SubscriptionSetValuesFieldResolverArgs struct { + Limit int // Limit - self descriptive + Offset int // Offset - self descriptive +} + +// SubscriptionSetValuesFieldResolverParams contains contextual info to resolve values field +type SubscriptionSetValuesFieldResolverParams struct { + graphql.ResolveParams + Args SubscriptionSetValuesFieldResolverArgs +} + +// SubscriptionSetValuesFieldResolver implement to resolve requests for the SubscriptionSet's values field. +type SubscriptionSetValuesFieldResolver interface { + // Values implements response to request for values field. + Values(p SubscriptionSetValuesFieldResolverParams) ([]string, error) +} + +// SubscriptionSetSizeFieldResolver implement to resolve requests for the SubscriptionSet's size field. +type SubscriptionSetSizeFieldResolver interface { + // Size implements response to request for size field. + Size(p graphql.ResolveParams) (int, error) +} + +// +// SubscriptionSetFieldResolvers represents a collection of methods whose products represent the +// response values of the 'SubscriptionSet' type. +// +// == Example SDL +// +// """ +// Dog's are not hooman. +// """ +// type Dog implements Pet { +// "name of this fine beast." +// name: String! +// +// "breed of this silly animal; probably shibe." +// breed: [Breed] +// } +// +// == Example generated interface +// +// // DogResolver ... +// type DogFieldResolvers interface { +// DogNameFieldResolver +// DogBreedFieldResolver +// +// // IsTypeOf is used to determine if a given value is associated with the Dog type +// IsTypeOf(interface{}, graphql.IsTypeOfParams) bool +// } +// +// == Example implementation ... +// +// // DogResolver implements DogFieldResolvers interface +// type DogResolver struct { +// logger logrus.LogEntry +// store interface{ +// store.BreedStore +// store.DogStore +// } +// } +// +// // Name implements response to request for name field. +// func (r *DogResolver) Name(p graphql.ResolveParams) (interface{}, error) { +// // ... implementation details ... +// dog := p.Source.(DogGetter) +// return dog.GetName() +// } +// +// // Breed implements response to request for breed field. +// func (r *DogResolver) Breed(p graphql.ResolveParams) (interface{}, error) { +// // ... implementation details ... +// dog := p.Source.(DogGetter) +// breed := r.store.GetBreed(dog.GetBreedName()) +// return breed +// } +// +// // IsTypeOf is used to determine if a given value is associated with the Dog type +// func (r *DogResolver) IsTypeOf(p graphql.IsTypeOfParams) bool { +// // ... implementation details ... +// _, ok := p.Value.(DogGetter) +// return ok +// } +// +type SubscriptionSetFieldResolvers interface { + SubscriptionSetEntriesFieldResolver + SubscriptionSetValuesFieldResolver + SubscriptionSetSizeFieldResolver +} + +// SubscriptionSetAliases implements all methods on SubscriptionSetFieldResolvers interface by using reflection to +// match name of field to a field on the given value. Intent is reduce friction +// of writing new resolvers by removing all the instances where you would simply +// have the resolvers method return a field. +// +// == Example SDL +// +// type Dog { +// name: String! +// weight: Float! +// dob: DateTime +// breed: [Breed] +// } +// +// == Example generated aliases +// +// type DogAliases struct {} +// func (_ DogAliases) Name(p graphql.ResolveParams) (interface{}, error) { +// // reflect... +// } +// func (_ DogAliases) Weight(p graphql.ResolveParams) (interface{}, error) { +// // reflect... +// } +// func (_ DogAliases) Dob(p graphql.ResolveParams) (interface{}, error) { +// // reflect... +// } +// func (_ DogAliases) Breed(p graphql.ResolveParams) (interface{}, error) { +// // reflect... +// } +// +// == Example Implementation +// +// type DogResolver struct { // Implements DogResolver +// DogAliases +// store store.BreedStore +// } +// +// // NOTE: +// // All other fields are satisified by DogAliases but since this one +// // requires hitting the store we implement it in our resolver. +// func (r *DogResolver) Breed(p graphql.ResolveParams) interface{} { +// dog := v.(*Dog) +// return r.BreedsById(dog.BreedIDs) +// } +// +type SubscriptionSetAliases struct{} + +// Entries implements response to request for 'entries' field. +func (_ SubscriptionSetAliases) Entries(p SubscriptionSetEntriesFieldResolverParams) (interface{}, error) { + val, err := graphql.DefaultResolver(p.Source, p.Info.FieldName) + return val, err +} + +// Values implements response to request for 'values' field. +func (_ SubscriptionSetAliases) Values(p SubscriptionSetValuesFieldResolverParams) ([]string, error) { + val, err := graphql.DefaultResolver(p.Source, p.Info.FieldName) + ret := val.([]string) + return ret, err +} + +// Size implements response to request for 'size' field. +func (_ SubscriptionSetAliases) Size(p graphql.ResolveParams) (int, error) { + val, err := graphql.DefaultResolver(p.Source, p.Info.FieldName) + ret := graphql1.Int.ParseValue(val).(int) + return ret, err +} + +// SubscriptionSetType SubscriptionSet describes a set of subscriptions. +var SubscriptionSetType = graphql.NewType("SubscriptionSet", graphql.ObjectKind) + +// RegisterSubscriptionSet registers SubscriptionSet object type with given service. +func RegisterSubscriptionSet(svc *graphql.Service, impl SubscriptionSetFieldResolvers) { + svc.RegisterObject(_ObjectTypeSubscriptionSetDesc, impl) +} +func _ObjTypeSubscriptionSetEntriesHandler(impl interface{}) graphql1.FieldResolveFn { + resolver := impl.(SubscriptionSetEntriesFieldResolver) + return func(p graphql1.ResolveParams) (interface{}, error) { + frp := SubscriptionSetEntriesFieldResolverParams{ResolveParams: p} + err := mapstructure.Decode(p.Args, &frp.Args) + if err != nil { + return nil, err + } + + return resolver.Entries(frp) + } +} + +func _ObjTypeSubscriptionSetValuesHandler(impl interface{}) graphql1.FieldResolveFn { + resolver := impl.(SubscriptionSetValuesFieldResolver) + return func(p graphql1.ResolveParams) (interface{}, error) { + frp := SubscriptionSetValuesFieldResolverParams{ResolveParams: p} + err := mapstructure.Decode(p.Args, &frp.Args) + if err != nil { + return nil, err + } + + return resolver.Values(frp) + } +} + +func _ObjTypeSubscriptionSetSizeHandler(impl interface{}) graphql1.FieldResolveFn { + resolver := impl.(SubscriptionSetSizeFieldResolver) + return func(frp graphql1.ResolveParams) (interface{}, error) { + return resolver.Size(frp) + } +} + +func _ObjectTypeSubscriptionSetConfigFn() graphql1.ObjectConfig { + return graphql1.ObjectConfig{ + Description: "SubscriptionSet describes a set of subscriptions.", + Fields: graphql1.Fields{ + "entries": &graphql1.Field{ + Args: graphql1.FieldConfigArgument{ + "limit": &graphql1.ArgumentConfig{ + DefaultValue: 50, + Description: "self descriptive", + Type: graphql1.Int, + }, + "offset": &graphql1.ArgumentConfig{ + DefaultValue: 0, + Description: "self descriptive", + Type: graphql1.Int, + }, + }, + DeprecationReason: "", + Description: "Returns all subscriptions in the set. Optionally constrained", + Name: "entries", + Type: graphql1.NewNonNull(graphql1.NewList(graphql1.NewNonNull(graphql.OutputType("SubscriptionOccurences")))), + }, + "size": &graphql1.Field{ + Args: graphql1.FieldConfigArgument{}, + DeprecationReason: "", + Description: "Returns the number of values in the set.", + Name: "size", + Type: graphql1.Int, + }, + "values": &graphql1.Field{ + Args: graphql1.FieldConfigArgument{ + "limit": &graphql1.ArgumentConfig{ + DefaultValue: 50, + Description: "self descriptive", + Type: graphql1.Int, + }, + "offset": &graphql1.ArgumentConfig{ + DefaultValue: 0, + Description: "self descriptive", + Type: graphql1.Int, + }, + }, + DeprecationReason: "", + Description: "Returns all subscriptions in the set. Optinally constrained.", + Name: "values", + Type: graphql1.NewNonNull(graphql1.NewList(graphql1.NewNonNull(graphql1.String))), + }, + }, + Interfaces: []*graphql1.Interface{}, + IsTypeOf: func(_ graphql1.IsTypeOfParams) bool { + // NOTE: + // Panic by default. Intent is that when Service is invoked, values of + // these fields are updated with instantiated resolvers. If these + // defaults are called it is most certainly programmer err. + // If you're see this comment then: 'Whoops! Sorry, my bad.' + panic("Unimplemented; see SubscriptionSetFieldResolvers.") + }, + Name: "SubscriptionSet", + } +} + +// describe SubscriptionSet's configuration; kept private to avoid unintentional tampering of configuration at runtime. +var _ObjectTypeSubscriptionSetDesc = graphql.ObjectDesc{ + Config: _ObjectTypeSubscriptionSetConfigFn, + FieldHandlers: map[string]graphql.FieldHandler{ + "entries": _ObjTypeSubscriptionSetEntriesHandler, + "size": _ObjTypeSubscriptionSetSizeHandler, + "values": _ObjTypeSubscriptionSetValuesHandler, + }, +} + +// SubscriptionOccurencesSubscriptionFieldResolver implement to resolve requests for the SubscriptionOccurences's subscription field. +type SubscriptionOccurencesSubscriptionFieldResolver interface { + // Subscription implements response to request for subscription field. + Subscription(p graphql.ResolveParams) (string, error) +} + +// SubscriptionOccurencesOccurrencesFieldResolver implement to resolve requests for the SubscriptionOccurences's occurrences field. +type SubscriptionOccurencesOccurrencesFieldResolver interface { + // Occurrences implements response to request for occurrences field. + Occurrences(p graphql.ResolveParams) (int, error) +} + +// +// SubscriptionOccurencesFieldResolvers represents a collection of methods whose products represent the +// response values of the 'SubscriptionOccurences' type. +// +// == Example SDL +// +// """ +// Dog's are not hooman. +// """ +// type Dog implements Pet { +// "name of this fine beast." +// name: String! +// +// "breed of this silly animal; probably shibe." +// breed: [Breed] +// } +// +// == Example generated interface +// +// // DogResolver ... +// type DogFieldResolvers interface { +// DogNameFieldResolver +// DogBreedFieldResolver +// +// // IsTypeOf is used to determine if a given value is associated with the Dog type +// IsTypeOf(interface{}, graphql.IsTypeOfParams) bool +// } +// +// == Example implementation ... +// +// // DogResolver implements DogFieldResolvers interface +// type DogResolver struct { +// logger logrus.LogEntry +// store interface{ +// store.BreedStore +// store.DogStore +// } +// } +// +// // Name implements response to request for name field. +// func (r *DogResolver) Name(p graphql.ResolveParams) (interface{}, error) { +// // ... implementation details ... +// dog := p.Source.(DogGetter) +// return dog.GetName() +// } +// +// // Breed implements response to request for breed field. +// func (r *DogResolver) Breed(p graphql.ResolveParams) (interface{}, error) { +// // ... implementation details ... +// dog := p.Source.(DogGetter) +// breed := r.store.GetBreed(dog.GetBreedName()) +// return breed +// } +// +// // IsTypeOf is used to determine if a given value is associated with the Dog type +// func (r *DogResolver) IsTypeOf(p graphql.IsTypeOfParams) bool { +// // ... implementation details ... +// _, ok := p.Value.(DogGetter) +// return ok +// } +// +type SubscriptionOccurencesFieldResolvers interface { + SubscriptionOccurencesSubscriptionFieldResolver + SubscriptionOccurencesOccurrencesFieldResolver +} + +// SubscriptionOccurencesAliases implements all methods on SubscriptionOccurencesFieldResolvers interface by using reflection to +// match name of field to a field on the given value. Intent is reduce friction +// of writing new resolvers by removing all the instances where you would simply +// have the resolvers method return a field. +// +// == Example SDL +// +// type Dog { +// name: String! +// weight: Float! +// dob: DateTime +// breed: [Breed] +// } +// +// == Example generated aliases +// +// type DogAliases struct {} +// func (_ DogAliases) Name(p graphql.ResolveParams) (interface{}, error) { +// // reflect... +// } +// func (_ DogAliases) Weight(p graphql.ResolveParams) (interface{}, error) { +// // reflect... +// } +// func (_ DogAliases) Dob(p graphql.ResolveParams) (interface{}, error) { +// // reflect... +// } +// func (_ DogAliases) Breed(p graphql.ResolveParams) (interface{}, error) { +// // reflect... +// } +// +// == Example Implementation +// +// type DogResolver struct { // Implements DogResolver +// DogAliases +// store store.BreedStore +// } +// +// // NOTE: +// // All other fields are satisified by DogAliases but since this one +// // requires hitting the store we implement it in our resolver. +// func (r *DogResolver) Breed(p graphql.ResolveParams) interface{} { +// dog := v.(*Dog) +// return r.BreedsById(dog.BreedIDs) +// } +// +type SubscriptionOccurencesAliases struct{} + +// Subscription implements response to request for 'subscription' field. +func (_ SubscriptionOccurencesAliases) Subscription(p graphql.ResolveParams) (string, error) { + val, err := graphql.DefaultResolver(p.Source, p.Info.FieldName) + ret := fmt.Sprint(val) + return ret, err +} + +// Occurrences implements response to request for 'occurrences' field. +func (_ SubscriptionOccurencesAliases) Occurrences(p graphql.ResolveParams) (int, error) { + val, err := graphql.DefaultResolver(p.Source, p.Info.FieldName) + ret := graphql1.Int.ParseValue(val).(int) + return ret, err +} + +// SubscriptionOccurencesType SubscriptionOccurences describes the number of occurrences of a subscription. +var SubscriptionOccurencesType = graphql.NewType("SubscriptionOccurences", graphql.ObjectKind) + +// RegisterSubscriptionOccurences registers SubscriptionOccurences object type with given service. +func RegisterSubscriptionOccurences(svc *graphql.Service, impl SubscriptionOccurencesFieldResolvers) { + svc.RegisterObject(_ObjectTypeSubscriptionOccurencesDesc, impl) +} +func _ObjTypeSubscriptionOccurencesSubscriptionHandler(impl interface{}) graphql1.FieldResolveFn { + resolver := impl.(SubscriptionOccurencesSubscriptionFieldResolver) + return func(frp graphql1.ResolveParams) (interface{}, error) { + return resolver.Subscription(frp) + } +} + +func _ObjTypeSubscriptionOccurencesOccurrencesHandler(impl interface{}) graphql1.FieldResolveFn { + resolver := impl.(SubscriptionOccurencesOccurrencesFieldResolver) + return func(frp graphql1.ResolveParams) (interface{}, error) { + return resolver.Occurrences(frp) + } +} + +func _ObjectTypeSubscriptionOccurencesConfigFn() graphql1.ObjectConfig { + return graphql1.ObjectConfig{ + Description: "SubscriptionOccurences describes the number of occurrences of a subscription.", + Fields: graphql1.Fields{ + "occurrences": &graphql1.Field{ + Args: graphql1.FieldConfigArgument{}, + DeprecationReason: "", + Description: "self descriptive", + Name: "occurrences", + Type: graphql1.NewNonNull(graphql1.Int), + }, + "subscription": &graphql1.Field{ + Args: graphql1.FieldConfigArgument{}, + DeprecationReason: "", + Description: "self descriptive", + Name: "subscription", + Type: graphql1.NewNonNull(graphql1.String), + }, + }, + Interfaces: []*graphql1.Interface{}, + IsTypeOf: func(_ graphql1.IsTypeOfParams) bool { + // NOTE: + // Panic by default. Intent is that when Service is invoked, values of + // these fields are updated with instantiated resolvers. If these + // defaults are called it is most certainly programmer err. + // If you're see this comment then: 'Whoops! Sorry, my bad.' + panic("Unimplemented; see SubscriptionOccurencesFieldResolvers.") + }, + Name: "SubscriptionOccurences", + } +} + +// describe SubscriptionOccurences's configuration; kept private to avoid unintentional tampering of configuration at runtime. +var _ObjectTypeSubscriptionOccurencesDesc = graphql.ObjectDesc{ + Config: _ObjectTypeSubscriptionOccurencesConfigFn, + FieldHandlers: map[string]graphql.FieldHandler{ + "occurrences": _ObjTypeSubscriptionOccurencesOccurrencesHandler, + "subscription": _ObjTypeSubscriptionOccurencesSubscriptionHandler, + }, +} diff --git a/backend/apid/graphql/schema/subscriptions.graphql b/backend/apid/graphql/schema/subscriptions.graphql new file mode 100644 index 0000000000..e5c7564170 --- /dev/null +++ b/backend/apid/graphql/schema/subscriptions.graphql @@ -0,0 +1,19 @@ +""" +SubscriptionSet describes a set of subscriptions. +""" +type SubscriptionSet { + "Returns all subscriptions in the set. Optionally constrained" + entries(limit: Int = 50, offset: Int = 0): [SubscriptionOccurences!]! + "Returns all subscriptions in the set. Optinally constrained." + values(limit: Int = 50, offset: Int = 0): [String!]! + "Returns the number of values in the set." + size: Int +} + +""" +SubscriptionOccurences describes the number of occurrences of a subscription. +""" +type SubscriptionOccurences { + subscription: String! + occurrences: Int! +} diff --git a/backend/apid/graphql/service.go b/backend/apid/graphql/service.go index 2d90113c35..ab566d660a 100644 --- a/backend/apid/graphql/service.go +++ b/backend/apid/graphql/service.go @@ -43,6 +43,9 @@ func NewService(cfg ServiceConfig) (*graphql.Service, error) { schema.RegisterSchema(svc) schema.RegisterSilenced(svc, newSilencedImpl(store, cfg.QueueGetter)) schema.RegisterSilencedConnection(svc, &schema.SilencedConnectionAliases{}) + schema.RegisterSubscriptionSet(svc, subscriptionSetImpl{}) + schema.RegisterSubscriptionSetOrder(svc) + schema.RegisterSubscriptionOccurences(svc, &schema.SubscriptionOccurencesAliases{}) schema.RegisterViewer(svc, newViewerImpl(store, cfg.QueueGetter, cfg.Bus)) // Register check types diff --git a/backend/apid/graphql/subscription.go b/backend/apid/graphql/subscription.go new file mode 100644 index 0000000000..40b84c753f --- /dev/null +++ b/backend/apid/graphql/subscription.go @@ -0,0 +1,101 @@ +package graphql + +import ( + "sort" + + "github.com/sensu/sensu-go/backend/apid/graphql/schema" + "github.com/sensu/sensu-go/graphql" + "github.com/sensu/sensu-go/util/strings" +) + +var _ schema.SubscriptionSetFieldResolvers = (*subscriptionSetImpl)(nil) + +// +// Implement SubscriptionSetFieldResolvers +// + +type subscriptionSetImpl struct{} + +// Entries implements response to request for 'entries' field. +func (subscriptionSetImpl) Entries(p schema.SubscriptionSetEntriesFieldResolverParams) (interface{}, error) { + set := p.Source.(subscriptionSet) + entries := set.entries() + + l, h := clampSlice(p.Args.Offset, p.Args.Offset+p.Args.Limit, len(entries)) + return entries[l:h], nil +} + +// Values implements response to request for 'values' field. +func (subscriptionSetImpl) Values(p schema.SubscriptionSetValuesFieldResolverParams) ([]string, error) { + set := p.Source.(subscriptionSet) + values := set.values() + + l, h := clampSlice(p.Args.Offset, p.Args.Offset+p.Args.Limit, len(values)) + return values[l:h], nil +} + +// Size implements response to request for 'size' field. +func (subscriptionSetImpl) Size(p graphql.ResolveParams) (int, error) { + set := p.Source.(subscriptionSet) + return set.size(), nil +} + +// SubscriptionSet + +type subscribable interface { + GetSubscriptions() []string +} + +func occurrencesOfSubscriptions(record subscribable) strings.OccurrenceSet { + vls := record.GetSubscriptions() + return strings.NewOccurrenceSet(vls...) +} + +type subscriptionSet struct { + occurrences strings.OccurrenceSet + vls []string +} + +type subscriptionOccurrences struct { + Subscription string + Occurrences int +} + +func newSubscriptionSet(set strings.OccurrenceSet) subscriptionSet { + return subscriptionSet{ + occurrences: set, + vls: set.Values(), + } +} + +func (set subscriptionSet) size() int { + return len(set.vls) +} + +func (set subscriptionSet) entries() []subscriptionOccurrences { + occurrences := make([]subscriptionOccurrences, len(set.vls)) + for i, v := range set.vls { + occurrences[i] = subscriptionOccurrences{v, set.occurrences.Get(v)} + } + return occurrences +} + +func (set subscriptionSet) values() []string { + return set.vls +} + +func (set subscriptionSet) sortByAlpha(asc bool) { + var vls sort.Interface = sort.StringSlice(set.vls) + if asc { + vls = sort.Reverse(vls) + } + sort.Sort(vls) +} + +func (set subscriptionSet) sortByOccurrence() { + sort.Slice(set.vls, func(i, j int) bool { + a := set.occurrences.Get(set.vls[i]) + b := set.occurrences.Get(set.vls[j]) + return a > b + }) +} diff --git a/backend/apid/graphql/subscription_test.go b/backend/apid/graphql/subscription_test.go new file mode 100644 index 0000000000..33c50c2e09 --- /dev/null +++ b/backend/apid/graphql/subscription_test.go @@ -0,0 +1,50 @@ +package graphql + +import ( + "testing" + + "github.com/sensu/sensu-go/types" + "github.com/sensu/sensu-go/util/strings" + "github.com/stretchr/testify/assert" +) + +func TestOccurrencesOfSubscription(t *testing.T) { + assert := assert.New(t) + + entity := types.FixtureEntity("test") + entity.Subscriptions = []string{"one", "two"} + + set := occurrencesOfSubscriptions(entity) + assert.Equal(set.Size(), 2) + assert.Equal(set.Get("one"), 1) + assert.Equal(set.Get("two"), 1) + assert.Equal(set.Get("three"), 0) +} + +func TestSubscriptionSet(t *testing.T) { + assert := assert.New(t) + + // new + set := newSubscriptionSet(strings.NewOccurrenceSet("one", "one", "two")) + assert.Equal(set.size(), 2) + + // sort alpha desc + set.sortByAlpha(false) + assert.EqualValues(set.values(), []string{"one", "two"}) + + // sort alpha asc + set.sortByAlpha(true) + assert.EqualValues(set.values(), []string{"two", "one"}) + + // sort by occurrence + set.sortByOccurrence() + assert.EqualValues(set.values(), []string{"one", "two"}) + + // entries + entries := set.entries() + assert.Len(entries, 2) + assert.EqualValues(entries, []subscriptionOccurrences{ + subscriptionOccurrences{"one", 2}, + subscriptionOccurrences{"two", 1}, + }) +} diff --git a/dashboard/src/apollo/schema/combined.graphql b/dashboard/src/apollo/schema/combined.graphql index 5524855076..f727570d13 100644 --- a/dashboard/src/apollo/schema/combined.graphql +++ b/dashboard/src/apollo/schema/combined.graphql @@ -476,7 +476,11 @@ type Environment implements Node { """All check configurations associated with the environment.""" checks( offset: Int = 0 + + """Limit adds optional limit to the number of entries returned.""" limit: Int = 10 + + """OrderBy adds optional order to the records retrieved.""" orderBy: CheckListOrder = NAME_DESC """ @@ -488,7 +492,11 @@ type Environment implements Node { """All entities associated with the environment.""" entities( offset: Int = 0 + + """Limit adds optional limit to the number of entries returned.""" limit: Int = 10 + + """OrderBy adds optional order to the records retrieved.""" orderBy: EntityListOrder = ID_DESC """ @@ -500,7 +508,11 @@ type Environment implements Node { """All events associated with the environment.""" events( offset: Int = 0 + + """Limit adds optional limit to the number of entries returned.""" limit: Int = 10 + + """OrderBy adds optional order to the records retrieved.""" orderBy: EventsListOrder = SEVERITY """ @@ -510,7 +522,21 @@ type Environment implements Node { ): EventConnection! """All silences associated with the environment.""" - silences(offset: Int = 0, limit: Int = 10): SilencedConnection! + silences( + offset: Int = 0 + + """Limit adds optional limit to the number of entries returned.""" + limit: Int = 10 + ): SilencedConnection! + + """All subscriptions in use in the environment.""" + subscriptions( + """Omit entity subscriptions from set.""" + omitEntity: Boolean = false + + """OrderBy adds optional order to the records retrieved.""" + orderBy: SubscriptionSetOrder = OCCURRENCES + ): SubscriptionSet! """ checkHistory includes all persisted check execution results associated with @@ -1023,6 +1049,33 @@ input SilenceInputs { expire: Int = -1 } +""" +SubscriptionOccurences describes the number of occurrences of a subscription. +""" +type SubscriptionOccurences { + subscription: String! + occurrences: Int! +} + +"""SubscriptionSet describes a set of subscriptions.""" +type SubscriptionSet { + """Returns all subscriptions in the set. Optionally constrained""" + entries(limit: Int = 50, offset: Int = 0): [SubscriptionOccurences!]! + + """Returns all subscriptions in the set. Optinally constrained.""" + values(limit: Int = 50, offset: Int = 0): [String!]! + + """Returns the number of values in the set.""" + size: Int +} + +"""Describes ways in which a set of subscriptions can be ordered.""" +enum SubscriptionSetOrder { + ALPHA_ASC + ALPHA_DESC + OCCURRENCES +} + """ System contains information about the system that the Agent process is running on, used for additional Entity context. diff --git a/dashboard/src/components/EventsListItem.js b/dashboard/src/components/EventsListItem.js index 4fc83e9e4b..c412e9e46d 100644 --- a/dashboard/src/components/EventsListItem.js +++ b/dashboard/src/components/EventsListItem.js @@ -48,22 +48,20 @@ class EventListItem extends React.Component { static fragments = { event: gql` fragment EventsListItem_event on Event { - ... on Event { - id - timestamp - deleted @client - check { - status - name - output - } - entity { - name - } - namespace { - organization - environment - } + id + timestamp + deleted @client + check { + status + name + output + } + entity { + name + } + namespace { + organization + environment } } `, diff --git a/dashboard/src/components/partials/EntitiesList/EntitiesList.js b/dashboard/src/components/partials/EntitiesList/EntitiesList.js index e8992bc4c8..a5fe39f719 100644 --- a/dashboard/src/components/partials/EntitiesList/EntitiesList.js +++ b/dashboard/src/components/partials/EntitiesList/EntitiesList.js @@ -2,22 +2,13 @@ import React from "react"; import PropTypes from "prop-types"; import gql from "graphql-tag"; import { withApollo } from "react-apollo"; - +import deleteEntity from "/mutations/deleteEntity"; +import Loader from "/components/util/Loader"; import TableList, { TableListBody, TableListEmptyState, - TableListSelect as Select, } from "/components/TableList"; -import Button from "@material-ui/core/Button"; -import ButtonSet from "/components/ButtonSet"; -import MenuItem from "@material-ui/core/MenuItem"; -import ListItemText from "@material-ui/core/ListItemText"; - -import ConfirmDelete from "/components/partials/ConfirmDelete"; -import deleteEntity from "/mutations/deleteEntity"; - -import Loader from "/components/util/Loader"; import Pagination from "/components/partials/Pagination"; import EntitiesListHeader from "./EntitiesListHeader"; @@ -78,9 +69,13 @@ class EntitiesList extends React.PureComponent { ...Pagination_pageInfo } } + subscriptions(orderBy: OCCURRENCES, omitEntity: true) { + ...EntitiesListHeader_subscriptions + } } ${EntitiesListItem.fragments.entity} + ${EntitiesListHeader.fragments.subscriptions} ${Pagination.fragments.pageInfo} `, }; @@ -119,6 +114,16 @@ class EntitiesList extends React.PureComponent { }; }); + _handleChangeFilter = (filter, val) => { + switch (filter) { + case "subscription": + this.props.onChangeParams({ filter: `'${val}' IN Subscriptions` }); + break; + default: + throw new Error(`unexpected filter '${filter}'`); + } + }; + _handleDeleteItems = () => { const { selectedIds } = this.state; const { client } = this.props; @@ -146,42 +151,22 @@ class EntitiesList extends React.PureComponent { }); }; + _getSubscriptions = () => + this.props.environment && this.props.environment.subscriptions; + render() { const { environment, limit, offset, onChangeParams } = this.props; const entities = getEntities(this.props); - const selectLen = this.state.selectedIds.length; return ( - - - } - bulkActions={ - - this._handleDeleteItems()} - > - {confirm => ( - - )} - - - } + subscriptions={this._getSubscriptions()} /> diff --git a/dashboard/src/components/partials/EntitiesList/EntitiesListHeader.js b/dashboard/src/components/partials/EntitiesList/EntitiesListHeader.js index 0190e775bf..da3f61b4e1 100644 --- a/dashboard/src/components/partials/EntitiesList/EntitiesListHeader.js +++ b/dashboard/src/components/partials/EntitiesList/EntitiesListHeader.js @@ -1,16 +1,17 @@ import React from "react"; import PropTypes from "prop-types"; -import Checkbox from "@material-ui/core/Checkbox"; +import gql from "graphql-tag"; + import { withStyles } from "@material-ui/core/styles"; -import { TableListHeader } from "/components/TableList"; +import Button from "@material-ui/core/Button"; +import ButtonSet from "/components/ButtonSet"; +import Checkbox from "@material-ui/core/Checkbox"; +import ConfirmDelete from "/components/partials/ConfirmDelete"; +import ListItemText from "@material-ui/core/ListItemText"; +import MenuItem from "@material-ui/core/MenuItem"; +import { TableListHeader, TableListSelect } from "/components/TableList"; const styles = theme => ({ - filterActions: { - display: "none", - [theme.breakpoints.up("sm")]: { - display: "flex", - }, - }, // Remove padding from button container checkbox: { marginLeft: -11, @@ -23,26 +24,80 @@ const styles = theme => ({ class EntitiesListHeader extends React.PureComponent { static propTypes = { - actions: PropTypes.node.isRequired, - bulkActions: PropTypes.node.isRequired, classes: PropTypes.object.isRequired, onClickSelect: PropTypes.func, + onChangeFilter: PropTypes.func, + onChangeSort: PropTypes.func, + onSubmitDelete: PropTypes.func, selectedCount: PropTypes.number, + subscriptions: PropTypes.shape({ values: PropTypes.array }), }; static defaultProps = { onClickSelect: () => {}, + onChangeFilter: () => {}, + onChangeSort: () => {}, + onSubmitDelete: () => {}, selectedCount: 0, + subscriptions: undefined, }; - render() { + static fragments = { + subscriptions: gql` + fragment EntitiesListHeader_subscriptions on SubscriptionSet { + values(limit: 25) + } + `, + }; + + _renderListActions = () => { const { - actions, - bulkActions, - classes, - selectedCount, - onClickSelect, + onChangeFilter, + onChangeSort, + subscriptions: subscriptionsProp, } = this.props; + const subscriptions = subscriptionsProp ? subscriptionsProp.values : []; + + return ( + + onChangeFilter("subscription", val)} + > + {subscriptions.map(entry => ( + + + + ))} + + + + Name + + + Last Seen + + + + ); + }; + + _renderBulkActions = () => { + const { onSubmitDelete, selectedCount } = this.props; + const resource = selectedCount === 1 ? "entity" : "entities"; + const ref = `${selectedCount} ${resource}`; + + return ( + + + {confirm => } + + + ); + }; + + render() { + const { classes, onClickSelect, selectedCount } = this.props; return ( 0}> @@ -55,7 +110,9 @@ class EntitiesListHeader extends React.PureComponent { /> {selectedCount > 0 &&
{selectedCount} Selected
}
- {selectedCount > 0 ? bulkActions : actions} + {selectedCount === 0 + ? this._renderListActions() + : this._renderBulkActions()} ); } diff --git a/dashboard/src/components/views/EnvironmentView/EntitiesContent.js b/dashboard/src/components/views/EnvironmentView/EntitiesContent.js index c84539bf9e..b4b88fd394 100644 --- a/dashboard/src/components/views/EnvironmentView/EntitiesContent.js +++ b/dashboard/src/components/views/EnvironmentView/EntitiesContent.js @@ -80,8 +80,8 @@ class EntitiesContent extends React.PureComponent { diff --git a/util/strings/occurrences.go b/util/strings/occurrences.go new file mode 100644 index 0000000000..8a2610b5b7 --- /dev/null +++ b/util/strings/occurrences.go @@ -0,0 +1,60 @@ +package strings + +// OccurrencesOf returns the number of times a string appears in the given slice +// of strings. +func OccurrencesOf(s string, in []string) int { + o := NewOccurrenceSet(in...) + return o.Get(s) +} + +// OccurrenceSet captures of occurrences of string values. +type OccurrenceSet map[string]int + +// NewOccurrenceSet returns new instance of OccurrenceSet. +func NewOccurrenceSet(s ...string) OccurrenceSet { + o := OccurrenceSet{} + o.Add(s...) + return o +} + +// Add entry and increment count +func (o OccurrenceSet) Add(ss ...string) { + for _, s := range ss { + num, _ := o[s] + o[s] = num + 1 + } +} + +// Remove items from set +func (o OccurrenceSet) Remove(ss ...string) { + for _, s := range ss { + delete(o, s) + } +} + +// Get returns occurrences of given string +func (o OccurrenceSet) Get(entry string) int { + return o[entry] +} + +// Values returns all occurrences +func (o OccurrenceSet) Values() []string { + vs := []string{} + for v := range o { + vs = append(vs, v) + } + return vs +} + +// Merge given set of occurrences +func (o OccurrenceSet) Merge(b OccurrenceSet) { + for name, bCount := range b { + aCount, _ := o[name] + o[name] = aCount + bCount + } +} + +// Size of values tracked +func (o OccurrenceSet) Size() int { + return len(o) +} diff --git a/util/strings/occurrences_test.go b/util/strings/occurrences_test.go new file mode 100644 index 0000000000..dd28fab95c --- /dev/null +++ b/util/strings/occurrences_test.go @@ -0,0 +1,45 @@ +package strings + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestOccurrences(t *testing.T) { + assert := assert.New(t) + + // new + setA := NewOccurrenceSet("one", "two") + assert.Equal(setA.Size(), 2) + assert.Contains(setA.Values(), "one") + assert.Contains(setA.Values(), "two") + + // add + setB := setA + setB.Add("three") + setB.Add("four") + assert.Equal(setB.Size(), 4) + assert.Contains(setB.Values(), "three") + assert.Contains(setB.Values(), "four") + + // merge + setA.Merge(setB) + assert.Equal(setA.Size(), 4) + assert.Contains(setA.Values(), "three") + assert.Contains(setA.Values(), "four") + + // remove + setA.Remove("four") + assert.Equal(setA.Size(), 3) + assert.NotContains(setA.Values(), "four") +} + +func TestOccurrencesOf(t *testing.T) { + assert := assert.New(t) + + assert.Equal(OccurrencesOf("zero", []string{}), 0) + assert.Equal(OccurrencesOf("zero", []string{"one", "one", "four"}), 0) + assert.Equal(OccurrencesOf("one", []string{"one", "two", "three"}), 1) + assert.Equal(OccurrencesOf("two", []string{"two", "two", "three"}), 2) +}