From 8d686a538c704a1e2264006c23a48b7bafba60c5 Mon Sep 17 00:00:00 2001 From: Nathaniel Cook Date: Fri, 16 Jun 2023 14:40:30 -0700 Subject: [PATCH 01/35] add ability to store alert metadata from ui --- alert/metadata.go | 9 + alert/store.go | 56 +++ graphql2/generated.go | 372 ++++++++++++++++++ graphql2/graphqlapp/alert.go | 35 +- graphql2/models_gen.go | 11 + graphql2/schema.graphql | 14 + .../20230616110941-add-alerts-metadata.sql | 10 + .../app/alerts/components/AlertDetails.tsx | 69 ++-- .../app/alerts/components/AlertMetadata.tsx | 130 ++++++ web/src/app/details/CardActions.tsx | 1 + web/src/schema.d.ts | 13 + 11 files changed, 682 insertions(+), 38 deletions(-) create mode 100644 alert/metadata.go create mode 100644 migrate/migrations/20230616110941-add-alerts-metadata.sql create mode 100644 web/src/app/alerts/components/AlertMetadata.tsx diff --git a/alert/metadata.go b/alert/metadata.go new file mode 100644 index 0000000000..7d8c5c78ba --- /dev/null +++ b/alert/metadata.go @@ -0,0 +1,9 @@ +package alert + +// Metadata represents user provided information about a given alert +type Metadata struct { + // ID is the ID of the alert. + AlertID int + Sentiment int + Note string +} diff --git a/alert/store.go b/alert/store.go index fdd1462625..97d3e80e35 100644 --- a/alert/store.go +++ b/alert/store.go @@ -51,6 +51,9 @@ type Store struct { escalate *sql.Stmt epState *sql.Stmt svcInfo *sql.Stmt + + metadata *sql.Stmt + updateMetadata *sql.Stmt } // A Trigger signals that an alert needs to be processed @@ -222,6 +225,21 @@ func NewStore(ctx context.Context, db *sql.DB, logDB *alertlog.Store) (*Store, e FROM services WHERE id = $1 `), + + metadata: p(` + SELECT + alert_id, sentiment, note + FROM alert_metadata + WHERE alert_id = $1 + `), + + updateMetadata: p(` + INSERT INTO alert_metadata (alert_id, sentiment, note) + VALUES ($1, $2, $3) + ON CONFLICT (alert_id) DO UPDATE + SET sentiment = $2, note = $3 + WHERE alert_metadata.alert_id = $1 + `), }, prep.Err } @@ -857,3 +875,41 @@ func (s *Store) State(ctx context.Context, alertIDs []int) ([]State, error) { return list, nil } + +func (s *Store) Metadata(ctx context.Context, alertID int) (am Metadata, err error) { + row := s.metadata.QueryRowContext(ctx, alertID) + err = row.Scan(&am.AlertID, &am.Sentiment, &am.Note) + if errors.Is(err, sql.ErrNoRows) { + return Metadata{ + AlertID: alertID, + Sentiment: 0, + Note: "", + }, nil + } + return am, err +} + +func (s Store) UpdateMetadata(ctx context.Context, metadata *Metadata) error { + err := permission.LimitCheckAny(ctx, permission.System, permission.User) + if err != nil { + return err + } + + am, err := (s).Metadata(ctx, metadata.AlertID) + if err != nil { + return err + } + if metadata.Sentiment != 0 { + am.Sentiment = metadata.Sentiment + } + if metadata.Note != "" { + am.Note = metadata.Note + } + + _, err = s.updateMetadata.ExecContext(ctx, metadata.AlertID, am.Sentiment, am.Note) + if err != nil { + return err + } + + return nil +} diff --git a/graphql2/generated.go b/graphql2/generated.go index 03d1738ab6..d2fa9f804b 100644 --- a/graphql2/generated.go +++ b/graphql2/generated.go @@ -97,6 +97,7 @@ type ComplexityRoot struct { CreatedAt func(childComplexity int) int Details func(childComplexity int) int ID func(childComplexity int) int + Metadata func(childComplexity int) int Metrics func(childComplexity int) int PendingNotifications func(childComplexity int) int RecentEvents func(childComplexity int, input *AlertRecentEventsOptions) int @@ -129,6 +130,11 @@ type ComplexityRoot struct { PageInfo func(childComplexity int) int } + AlertMetadata struct { + Note func(childComplexity int) int + Sentiment func(childComplexity int) int + } + AlertMetric struct { ClosedAt func(childComplexity int) int Escalated func(childComplexity int) int @@ -312,6 +318,7 @@ type ComplexityRoot struct { SetTemporarySchedule func(childComplexity int, input SetTemporaryScheduleInput) int SwoAction func(childComplexity int, action SWOAction) int TestContactMethod func(childComplexity int, id string) int + UpdateAlertMetadata func(childComplexity int, input UpdateAlertMetadataInput) int UpdateAlerts func(childComplexity int, input UpdateAlertsInput) int UpdateAlertsByService func(childComplexity int, input UpdateAlertsByServiceInput) int UpdateBasicAuth func(childComplexity int, input UpdateBasicAuthInput) int @@ -666,6 +673,7 @@ type AlertResolver interface { RecentEvents(ctx context.Context, obj *alert.Alert, input *AlertRecentEventsOptions) (*AlertLogEntryConnection, error) PendingNotifications(ctx context.Context, obj *alert.Alert) ([]AlertPendingNotification, error) Metrics(ctx context.Context, obj *alert.Alert) (*alertmetrics.Metric, error) + Metadata(ctx context.Context, obj *alert.Alert) (*AlertMetadata, error) } type AlertLogEntryResolver interface { Message(ctx context.Context, obj *alertlog.Entry) (string, error) @@ -720,6 +728,7 @@ type MutationResolver interface { UpdateEscalationPolicyStep(ctx context.Context, input UpdateEscalationPolicyStepInput) (bool, error) DeleteAll(ctx context.Context, input []assignment.RawTarget) (bool, error) CreateAlert(ctx context.Context, input CreateAlertInput) (*alert.Alert, error) + UpdateAlertMetadata(ctx context.Context, input UpdateAlertMetadataInput) (bool, error) CreateService(ctx context.Context, input CreateServiceInput) (*service.Service, error) CreateEscalationPolicy(ctx context.Context, input CreateEscalationPolicyInput) (*escalation.Policy, error) CreateEscalationPolicyStep(ctx context.Context, input CreateEscalationPolicyStepInput) (*escalation.Step, error) @@ -916,6 +925,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Alert.ID(childComplexity), true + case "Alert.metadata": + if e.complexity.Alert.Metadata == nil { + break + } + + return e.complexity.Alert.Metadata(childComplexity), true + case "Alert.metrics": if e.complexity.Alert.Metrics == nil { break @@ -1047,6 +1063,20 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.AlertLogEntryConnection.PageInfo(childComplexity), true + case "AlertMetadata.note": + if e.complexity.AlertMetadata.Note == nil { + break + } + + return e.complexity.AlertMetadata.Note(childComplexity), true + + case "AlertMetadata.sentiment": + if e.complexity.AlertMetadata.Sentiment == nil { + break + } + + return e.complexity.AlertMetadata.Sentiment(childComplexity), true + case "AlertMetric.closedAt": if e.complexity.AlertMetric.ClosedAt == nil { break @@ -2019,6 +2049,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Mutation.TestContactMethod(childComplexity, args["id"].(string)), true + case "Mutation.updateAlertMetadata": + if e.complexity.Mutation.UpdateAlertMetadata == nil { + break + } + + args, err := ec.field_Mutation_updateAlertMetadata_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Mutation.UpdateAlertMetadata(childComplexity, args["input"].(UpdateAlertMetadataInput)), true + case "Mutation.updateAlerts": if e.complexity.Mutation.UpdateAlerts == nil { break @@ -3930,6 +3972,7 @@ func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler { ec.unmarshalInputTargetInput, ec.unmarshalInputTimeSeriesOptions, ec.unmarshalInputTimeZoneSearchOptions, + ec.unmarshalInputUpdateAlertMetadataInput, ec.unmarshalInputUpdateAlertsByServiceInput, ec.unmarshalInputUpdateAlertsInput, ec.unmarshalInputUpdateBasicAuthInput, @@ -4520,6 +4563,21 @@ func (ec *executionContext) field_Mutation_testContactMethod_args(ctx context.Co return args, nil } +func (ec *executionContext) field_Mutation_updateAlertMetadata_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { + var err error + args := map[string]interface{}{} + var arg0 UpdateAlertMetadataInput + if tmp, ok := rawArgs["input"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("input")) + arg0, err = ec.unmarshalNUpdateAlertMetadataInput2githubᚗcomᚋtargetᚋgoalertᚋgraphql2ᚐUpdateAlertMetadataInput(ctx, tmp) + if err != nil { + return nil, err + } + } + args["input"] = arg0 + return args, nil +} + func (ec *executionContext) field_Mutation_updateAlertsByService_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} @@ -6006,6 +6064,53 @@ func (ec *executionContext) fieldContext_Alert_metrics(ctx context.Context, fiel return fc, nil } +func (ec *executionContext) _Alert_metadata(ctx context.Context, field graphql.CollectedField, obj *alert.Alert) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Alert_metadata(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Alert().Metadata(rctx, obj) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*AlertMetadata) + fc.Result = res + return ec.marshalOAlertMetadata2ᚖgithub.comᚋtargetᚋgoalertᚋgraphql2ᚐAlertMetadata(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Alert_metadata(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Alert", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "sentiment": + return ec.fieldContext_AlertMetadata_sentiment(ctx, field) + case "note": + return ec.fieldContext_AlertMetadata_note(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type AlertMetadata", field.Name) + }, + } + return fc, nil +} + func (ec *executionContext) _AlertConnection_nodes(ctx context.Context, field graphql.CollectedField, obj *AlertConnection) (ret graphql.Marshaler) { fc, err := ec.fieldContext_AlertConnection_nodes(ctx, field) if err != nil { @@ -6069,6 +6174,8 @@ func (ec *executionContext) fieldContext_AlertConnection_nodes(ctx context.Conte return ec.fieldContext_Alert_pendingNotifications(ctx, field) case "metrics": return ec.fieldContext_Alert_metrics(ctx, field) + case "metadata": + return ec.fieldContext_Alert_metadata(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type Alert", field.Name) }, @@ -6499,6 +6606,91 @@ func (ec *executionContext) fieldContext_AlertLogEntryConnection_pageInfo(ctx co return fc, nil } +func (ec *executionContext) _AlertMetadata_sentiment(ctx context.Context, field graphql.CollectedField, obj *AlertMetadata) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_AlertMetadata_sentiment(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Sentiment, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(int) + fc.Result = res + return ec.marshalNInt2int(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_AlertMetadata_sentiment(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "AlertMetadata", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type Int does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _AlertMetadata_note(ctx context.Context, field graphql.CollectedField, obj *AlertMetadata) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_AlertMetadata_note(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Note, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*string) + fc.Result = res + return ec.marshalOString2ᚖstring(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_AlertMetadata_note(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "AlertMetadata", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + func (ec *executionContext) _AlertMetric_escalated(ctx context.Context, field graphql.CollectedField, obj *alertmetrics.Metric) (ret graphql.Marshaler) { fc, err := ec.fieldContext_AlertMetric_escalated(ctx, field) if err != nil { @@ -11074,6 +11266,8 @@ func (ec *executionContext) fieldContext_Mutation_updateAlerts(ctx context.Conte return ec.fieldContext_Alert_pendingNotifications(ctx, field) case "metrics": return ec.fieldContext_Alert_metrics(ctx, field) + case "metadata": + return ec.fieldContext_Alert_metadata(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type Alert", field.Name) }, @@ -11207,6 +11401,8 @@ func (ec *executionContext) fieldContext_Mutation_escalateAlerts(ctx context.Con return ec.fieldContext_Alert_pendingNotifications(ctx, field) case "metrics": return ec.fieldContext_Alert_metrics(ctx, field) + case "metadata": + return ec.fieldContext_Alert_metadata(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type Alert", field.Name) }, @@ -11560,6 +11756,8 @@ func (ec *executionContext) fieldContext_Mutation_createAlert(ctx context.Contex return ec.fieldContext_Alert_pendingNotifications(ctx, field) case "metrics": return ec.fieldContext_Alert_metrics(ctx, field) + case "metadata": + return ec.fieldContext_Alert_metadata(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type Alert", field.Name) }, @@ -11578,6 +11776,61 @@ func (ec *executionContext) fieldContext_Mutation_createAlert(ctx context.Contex return fc, nil } +func (ec *executionContext) _Mutation_updateAlertMetadata(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Mutation_updateAlertMetadata(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().UpdateAlertMetadata(rctx, fc.Args["input"].(UpdateAlertMetadataInput)) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(bool) + fc.Result = res + return ec.marshalNBoolean2bool(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Mutation_updateAlertMetadata(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Mutation", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type Boolean does not have child fields") + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Mutation_updateAlertMetadata_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return + } + return fc, nil +} + func (ec *executionContext) _Mutation_createService(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Mutation_createService(ctx, field) if err != nil { @@ -14695,6 +14948,8 @@ func (ec *executionContext) fieldContext_Query_alert(ctx context.Context, field return ec.fieldContext_Alert_pendingNotifications(ctx, field) case "metrics": return ec.fieldContext_Alert_metrics(ctx, field) + case "metadata": + return ec.fieldContext_Alert_metadata(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type Alert", field.Name) }, @@ -28832,6 +29087,53 @@ func (ec *executionContext) unmarshalInputTimeZoneSearchOptions(ctx context.Cont return it, nil } +func (ec *executionContext) unmarshalInputUpdateAlertMetadataInput(ctx context.Context, obj interface{}) (UpdateAlertMetadataInput, error) { + var it UpdateAlertMetadataInput + asMap := map[string]interface{}{} + for k, v := range obj.(map[string]interface{}) { + asMap[k] = v + } + + fieldsInOrder := [...]string{"alertID", "sentiment", "note"} + for _, k := range fieldsInOrder { + v, ok := asMap[k] + if !ok { + continue + } + switch k { + case "alertID": + var err error + + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("alertID")) + data, err := ec.unmarshalNInt2int(ctx, v) + if err != nil { + return it, err + } + it.AlertID = data + case "sentiment": + var err error + + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("sentiment")) + data, err := ec.unmarshalOInt2ᚖint(ctx, v) + if err != nil { + return it, err + } + it.Sentiment = data + case "note": + var err error + + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("note")) + data, err := ec.unmarshalOString2ᚖstring(ctx, v) + if err != nil { + return it, err + } + it.Note = data + } + } + + return it, nil +} + func (ec *executionContext) unmarshalInputUpdateAlertsByServiceInput(ctx context.Context, obj interface{}) (UpdateAlertsByServiceInput, error) { var it UpdateAlertsByServiceInput asMap := map[string]interface{}{} @@ -30028,6 +30330,23 @@ func (ec *executionContext) _Alert(ctx context.Context, sel ast.SelectionSet, ob return res } + out.Concurrently(i, func() graphql.Marshaler { + return innerFunc(ctx) + + }) + case "metadata": + field := field + + innerFunc := func(ctx context.Context) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Alert_metadata(ctx, field, obj) + return res + } + out.Concurrently(i, func() graphql.Marshaler { return innerFunc(ctx) @@ -30220,6 +30539,38 @@ func (ec *executionContext) _AlertLogEntryConnection(ctx context.Context, sel as return out } +var alertMetadataImplementors = []string{"AlertMetadata"} + +func (ec *executionContext) _AlertMetadata(ctx context.Context, sel ast.SelectionSet, obj *AlertMetadata) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, alertMetadataImplementors) + out := graphql.NewFieldSet(fields) + var invalids uint32 + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("AlertMetadata") + case "sentiment": + + out.Values[i] = ec._AlertMetadata_sentiment(ctx, field, obj) + + if out.Values[i] == graphql.Null { + invalids++ + } + case "note": + + out.Values[i] = ec._AlertMetadata_note(ctx, field, obj) + + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch() + if invalids > 0 { + return graphql.Null + } + return out +} + var alertMetricImplementors = []string{"AlertMetric"} func (ec *executionContext) _AlertMetric(ctx context.Context, sel ast.SelectionSet, obj *alertmetrics.Metric) graphql.Marshaler { @@ -31599,6 +31950,15 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) return ec._Mutation_createAlert(ctx, field) }) + case "updateAlertMetadata": + + out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { + return ec._Mutation_updateAlertMetadata(ctx, field) + }) + + if out.Values[i] == graphql.Null { + invalids++ + } case "createService": out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { @@ -37887,6 +38247,11 @@ func (ec *executionContext) marshalNTimeZoneConnection2ᚖgithub.comᚋtarget return ec._TimeZoneConnection(ctx, sel, v) } +func (ec *executionContext) unmarshalNUpdateAlertMetadataInput2githubᚗcomᚋtargetᚋgoalertᚋgraphql2ᚐUpdateAlertMetadataInput(ctx context.Context, v interface{}) (UpdateAlertMetadataInput, error) { + res, err := ec.unmarshalInputUpdateAlertMetadataInput(ctx, v) + return res, graphql.ErrorOnPath(ctx, err) +} + func (ec *executionContext) unmarshalNUpdateAlertsByServiceInput2githubᚗcomᚋtargetᚋgoalertᚋgraphql2ᚐUpdateAlertsByServiceInput(ctx context.Context, v interface{}) (UpdateAlertsByServiceInput, error) { res, err := ec.unmarshalInputUpdateAlertsByServiceInput(ctx, v) return res, graphql.ErrorOnPath(ctx, err) @@ -38610,6 +38975,13 @@ func (ec *executionContext) marshalOAlert2ᚖgithub.comᚋtargetᚋgoalertᚋa return ec._Alert(ctx, sel, v) } +func (ec *executionContext) marshalOAlertMetadata2ᚖgithub.comᚋtargetᚋgoalertᚋgraphql2ᚐAlertMetadata(ctx context.Context, sel ast.SelectionSet, v *AlertMetadata) graphql.Marshaler { + if v == nil { + return graphql.Null + } + return ec._AlertMetadata(ctx, sel, v) +} + func (ec *executionContext) marshalOAlertMetric2ᚖgithub.comᚋtargetᚋgoalertᚋalertᚋalertmetricsᚐMetric(ctx context.Context, sel ast.SelectionSet, v *alertmetrics.Metric) graphql.Marshaler { if v == nil { return graphql.Null diff --git a/graphql2/graphqlapp/alert.go b/graphql2/graphqlapp/alert.go index 233b25df2f..88f6c85c2a 100644 --- a/graphql2/graphqlapp/alert.go +++ b/graphql2/graphqlapp/alert.go @@ -32,9 +32,8 @@ type ( AlertLogEntryState App ) -func (a *App) Alert() graphql2.AlertResolver { return (*Alert)(a) } -func (a *App) AlertMetric() graphql2.AlertMetricResolver { return (*AlertMetric)(a) } - +func (a *App) Alert() graphql2.AlertResolver { return (*Alert)(a) } +func (a *App) AlertMetric() graphql2.AlertMetricResolver { return (*AlertMetric)(a) } func (a *App) AlertLogEntry() graphql2.AlertLogEntryResolver { return (*AlertLogEntry)(a) } func (a *AlertLogEntry) ID(ctx context.Context, obj *alertlog.Entry) (int, error) { @@ -361,6 +360,36 @@ func (m *Mutation) CreateAlert(ctx context.Context, input graphql2.CreateAlertIn return m.AlertStore.Create(ctx, a) } +func (a *Alert) Metadata(ctx context.Context, raw *alert.Alert) (*graphql2.AlertMetadata, error) { + am, err := a.AlertStore.Metadata(ctx, raw.ID) + if err != nil { + return nil, err + } + return &graphql2.AlertMetadata{ + Sentiment: am.Sentiment, + Note: &am.Note, + }, nil +} + +func (m *Mutation) UpdateAlertMetadata(ctx context.Context, input graphql2.UpdateAlertMetadataInput) (bool, error) { + am := &alert.Metadata{ + AlertID: input.AlertID, + } + + if input.Sentiment != nil { + am.Sentiment = *input.Sentiment + } + if input.Note != nil { + am.Note = *input.Note + } + + err := m.AlertStore.UpdateMetadata(ctx, am) + if err != nil { + return false, err + } + return true, nil +} + func (a *Alert) RecentEvents(ctx context.Context, obj *alert.Alert, opts *graphql2.AlertRecentEventsOptions) (*graphql2.AlertLogEntryConnection, error) { if opts == nil { opts = new(graphql2.AlertRecentEventsOptions) diff --git a/graphql2/models_gen.go b/graphql2/models_gen.go index 99d0e3a10a..57d9b6d3c6 100644 --- a/graphql2/models_gen.go +++ b/graphql2/models_gen.go @@ -42,6 +42,11 @@ type AlertLogEntryConnection struct { PageInfo *PageInfo `json:"pageInfo"` } +type AlertMetadata struct { + Sentiment int `json:"sentiment"` + Note *string `json:"note,omitempty"` +} + type AlertMetricsOptions struct { RInterval timeutil.ISORInterval `json:"rInterval"` FilterByServiceID []string `json:"filterByServiceID,omitempty"` @@ -548,6 +553,12 @@ type TimeZoneSearchOptions struct { Omit []string `json:"omit,omitempty"` } +type UpdateAlertMetadataInput struct { + AlertID int `json:"alertID"` + Sentiment *int `json:"sentiment,omitempty"` + Note *string `json:"note,omitempty"` +} + type UpdateAlertsByServiceInput struct { ServiceID string `json:"serviceID"` NewStatus AlertStatus `json:"newStatus"` diff --git a/graphql2/schema.graphql b/graphql2/schema.graphql index c8dc1639c9..83d66d7d02 100644 --- a/graphql2/schema.graphql +++ b/graphql2/schema.graphql @@ -514,6 +514,7 @@ type Mutation { deleteAll(input: [TargetInput!]): Boolean! createAlert(input: CreateAlertInput!): Alert + updateAlertMetadata(input: UpdateAlertMetadataInput!): Boolean! createService(input: CreateServiceInput!): Service createEscalationPolicy(input: CreateEscalationPolicyInput!): EscalationPolicy @@ -591,6 +592,12 @@ input CreateAlertInput { sanitize: Boolean } +input UpdateAlertMetadataInput { + alertID: Int! + sentiment: Int + note: String +} + input CreateUserInput { username: String! password: String! @@ -1067,6 +1074,8 @@ type Alert { # metrics are only available for closed alerts metrics: AlertMetric + + metadata: AlertMetadata } type AlertMetric { @@ -1116,6 +1125,11 @@ type AlertState { repeatCount: Int! } +type AlertMetadata { + sentiment: Int! + note: String +} + type Service { id: ID! name: String! diff --git a/migrate/migrations/20230616110941-add-alerts-metadata.sql b/migrate/migrations/20230616110941-add-alerts-metadata.sql new file mode 100644 index 0000000000..e68a65378b --- /dev/null +++ b/migrate/migrations/20230616110941-add-alerts-metadata.sql @@ -0,0 +1,10 @@ +-- +migrate Up +CREATE TABLE alert_metadata +( + alert_id BIGINT PRIMARY KEY REFERENCES alerts (id) ON DELETE CASCADE, + sentiment INT NOT NULL DEFAULT 0, + note TEXT +); + +-- +migrate Down +DROP TABLE alert_metadata; diff --git a/web/src/app/alerts/components/AlertDetails.tsx b/web/src/app/alerts/components/AlertDetails.tsx index f723c049f5..61718299f3 100644 --- a/web/src/app/alerts/components/AlertDetails.tsx +++ b/web/src/app/alerts/components/AlertDetails.tsx @@ -1,5 +1,7 @@ import React, { ReactElement, useState, ReactNode } from 'react' import p from 'prop-types' +import ButtonGroup from '@mui/material/ButtonGroup' +import Button from '@mui/material/Button' import Card from '@mui/material/Card' import CardContent from '@mui/material/CardContent' import FormControlLabel from '@mui/material/FormControlLabel' @@ -32,7 +34,7 @@ import { styles as globalStyles } from '../../styles/materialStyles' import Markdown from '../../util/Markdown' import AlertDetailLogs from '../AlertDetailLogs' import AppLink from '../../util/AppLink' -import CardActions, { Action } from '../../details/CardActions' +import CardActions from '../../details/CardActions' import { Alert, Target, @@ -41,6 +43,7 @@ import { } from '../../../schema' import ServiceNotices from '../../services/ServiceNotices' import { Time } from '../../util/Time' +import AlertMetadata from './AlertMetadata' interface AlertDetailsProps { data: Alert @@ -317,42 +320,33 @@ export default function AlertDetails(props: AlertDetailsProps): JSX.Element { /* * Options to show for alert details menu */ - function getMenuOptions(): Action[] { + function getMenuOptions(): JSX.Element { const { status } = props.data - let options: Action[] = [] - - if (status === 'StatusClosed') return options - if (status === 'StatusUnacknowledged') { - options = [ - { - icon: , - label: 'Acknowledge', - handleOnClick: () => ack(), - }, - ] - } - + if (status === 'StatusClosed') return const isMaintMode = Boolean(props.data?.service?.maintenanceExpiresAt) - // only remaining status is acknowledged, show remaining buttons - return [ - ...options, - { - icon: , - label: 'Close', - handleOnClick: () => close(), - }, - { - icon: , - label: isMaintMode - ? 'Escalate disabled. In maintenance mode.' - : 'Escalate', - handleOnClick: () => escalate(), - ButtonProps: { - disabled: isMaintMode, - }, - }, - ] + return ( + + {status === 'StatusUnacknowledged' && ( + + )} + + + + ) } const { data: alert } = props @@ -392,7 +386,12 @@ export default function AlertDetails(props: AlertDetailsProps): JSX.Element { - + , + ]} + /> {renderAlertDetails()} diff --git a/web/src/app/alerts/components/AlertMetadata.tsx b/web/src/app/alerts/components/AlertMetadata.tsx new file mode 100644 index 0000000000..71e560aeff --- /dev/null +++ b/web/src/app/alerts/components/AlertMetadata.tsx @@ -0,0 +1,130 @@ +import React, { useState, useEffect } from 'react' +import { useQuery, useMutation, gql } from 'urql' +import { Grid, Grow, IconButton, Tooltip, TextField } from '@mui/material' +import { + ThumbUp, + ThumbDown, + ThumbUpOutlined, + ThumbDownOutlined, + Info, +} from '@mui/icons-material' +import { DEBOUNCE_DELAY } from '../../config' + +const query = gql` + query AlertMetadataQuery($id: Int!) { + alert(id: $id) { + id + metadata { + sentiment + note + } + } + } +` + +const mutation = gql` + mutation UpdateMetadataMutation($input: UpdateAlertMetadataInput!) { + updateAlertMetadata(input: $input) + } +` + +interface AlertMetadataProps { + alertID: number +} + +export default function AlertMetadata(props: AlertMetadataProps): JSX.Element { + const { alertID } = props + const [{ data, fetching, error }] = useQuery({ + query, + variables: { + id: alertID, + }, + requestPolicy: 'cache-first', + }) + const [note, setNote] = useState(data?.alert?.metadata?.note ?? '') + const [mutationStatus, commit] = useMutation(mutation) + + // Debounce setting note + useEffect(() => { + const t = setTimeout(() => { + commit({ + input: { + alertID, + note, + }, + }) + }, DEBOUNCE_DELAY) + + return () => clearTimeout(t) + }, [note]) + + useEffect(() => { + setNote(data?.alert?.metadata?.note ?? '') + }, [data?.alert?.metadata?.note]) + + const thumbUp = + !fetching && !error && data?.alert?.metadata?.sentiment === 1 ? ( + + ) : ( + + ) + + const thumbDown = + !fetching && !error && data?.alert?.metadata?.sentiment === -1 ? ( + + ) : ( + + ) + + return ( + + {mutationStatus.error?.message} + + { + commit({ + input: { + alertID, + sentiment: 1, + }, + }) + }} + size='large' + > + {thumbUp} + + + + { + commit({ + input: { + alertID, + sentiment: -1, + }, + }) + }} + size='large' + > + {thumbDown} + + + + + + + + + + setNote(e.target.value)} /> + + + + ) +} diff --git a/web/src/app/details/CardActions.tsx b/web/src/app/details/CardActions.tsx index 5cdca8058c..2b851afab8 100644 --- a/web/src/app/details/CardActions.tsx +++ b/web/src/app/details/CardActions.tsx @@ -28,6 +28,7 @@ const useStyles = makeStyles({ }, primaryActionsContainer: { padding: 8, + width: '100%', }, autoExpandWidth: { margin: '0 auto', diff --git a/web/src/schema.d.ts b/web/src/schema.d.ts index 8b21e7b3a0..a7e3f532f5 100644 --- a/web/src/schema.d.ts +++ b/web/src/schema.d.ts @@ -389,6 +389,7 @@ export interface Mutation { updateEscalationPolicyStep: boolean deleteAll: boolean createAlert?: null | Alert + updateAlertMetadata: boolean createService?: null | Service createEscalationPolicy?: null | EscalationPolicy createEscalationPolicyStep?: null | EscalationPolicyStep @@ -441,6 +442,12 @@ export interface CreateAlertInput { sanitize?: null | boolean } +export interface UpdateAlertMetadataInput { + alertID: number + sentiment?: null | number + note?: null | string +} + export interface CreateUserInput { username: string password: string @@ -835,6 +842,7 @@ export interface Alert { recentEvents: AlertLogEntryConnection pendingNotifications: AlertPendingNotification[] metrics?: null | AlertMetric + metadata?: null | AlertMetadata } export interface AlertMetric { @@ -879,6 +887,11 @@ export interface AlertState { repeatCount: number } +export interface AlertMetadata { + sentiment: number + note?: null | string +} + export interface Service { id: string name: string From 4642f6037708b6c0259ee12755c2c647fe9fefd8 Mon Sep 17 00:00:00 2001 From: Nathaniel Cook Date: Tue, 20 Jun 2023 07:49:52 -0700 Subject: [PATCH 02/35] simplify bool --- .../app/alerts/components/AlertMetadata.tsx | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/web/src/app/alerts/components/AlertMetadata.tsx b/web/src/app/alerts/components/AlertMetadata.tsx index 71e560aeff..fad2d634e0 100644 --- a/web/src/app/alerts/components/AlertMetadata.tsx +++ b/web/src/app/alerts/components/AlertMetadata.tsx @@ -69,12 +69,9 @@ export default function AlertMetadata(props: AlertMetadataProps): JSX.Element { ) + const isDown = data?.alert?.metadata?.sentiment === -1 const thumbDown = - !fetching && !error && data?.alert?.metadata?.sentiment === -1 ? ( - - ) : ( - - ) + !fetching && !error && isDown ? : return ( - - + + setNote(e.target.value)} /> From be50341065c41f961a0ae20d9cfc0a704597bca0 Mon Sep 17 00:00:00 2001 From: Nathaniel Cook Date: Tue, 20 Jun 2023 07:58:56 -0700 Subject: [PATCH 03/35] update metadata to feedback --- alert/store.go | 6 +- graphql2/generated.go | 376 +++++++++--------- graphql2/graphqlapp/alert.go | 6 +- graphql2/models_gen.go | 12 +- graphql2/schema.graphql | 8 +- ...=> 20230616110941-add-alerts-feedback.sql} | 4 +- .../app/alerts/components/AlertDetails.tsx | 4 +- .../{AlertMetadata.tsx => AlertFeedback.tsx} | 22 +- web/src/schema.d.ts | 8 +- 9 files changed, 223 insertions(+), 223 deletions(-) rename migrate/migrations/{20230616110941-add-alerts-metadata.sql => 20230616110941-add-alerts-feedback.sql} (74%) rename web/src/app/alerts/components/{AlertMetadata.tsx => AlertFeedback.tsx} (82%) diff --git a/alert/store.go b/alert/store.go index 97d3e80e35..d04e52a4d1 100644 --- a/alert/store.go +++ b/alert/store.go @@ -229,16 +229,16 @@ func NewStore(ctx context.Context, db *sql.DB, logDB *alertlog.Store) (*Store, e metadata: p(` SELECT alert_id, sentiment, note - FROM alert_metadata + FROM alert_feedback WHERE alert_id = $1 `), updateMetadata: p(` - INSERT INTO alert_metadata (alert_id, sentiment, note) + INSERT INTO alert_feedback (alert_id, sentiment, note) VALUES ($1, $2, $3) ON CONFLICT (alert_id) DO UPDATE SET sentiment = $2, note = $3 - WHERE alert_metadata.alert_id = $1 + WHERE alert_feedback.alert_id = $1 `), }, prep.Err } diff --git a/graphql2/generated.go b/graphql2/generated.go index d2fa9f804b..6acd16823a 100644 --- a/graphql2/generated.go +++ b/graphql2/generated.go @@ -96,8 +96,8 @@ type ComplexityRoot struct { AlertID func(childComplexity int) int CreatedAt func(childComplexity int) int Details func(childComplexity int) int + Feedback func(childComplexity int) int ID func(childComplexity int) int - Metadata func(childComplexity int) int Metrics func(childComplexity int) int PendingNotifications func(childComplexity int) int RecentEvents func(childComplexity int, input *AlertRecentEventsOptions) int @@ -118,6 +118,11 @@ type ComplexityRoot struct { Timestamp func(childComplexity int) int } + AlertFeedback struct { + Note func(childComplexity int) int + Sentiment func(childComplexity int) int + } + AlertLogEntry struct { ID func(childComplexity int) int Message func(childComplexity int) int @@ -130,11 +135,6 @@ type ComplexityRoot struct { PageInfo func(childComplexity int) int } - AlertMetadata struct { - Note func(childComplexity int) int - Sentiment func(childComplexity int) int - } - AlertMetric struct { ClosedAt func(childComplexity int) int Escalated func(childComplexity int) int @@ -318,7 +318,7 @@ type ComplexityRoot struct { SetTemporarySchedule func(childComplexity int, input SetTemporaryScheduleInput) int SwoAction func(childComplexity int, action SWOAction) int TestContactMethod func(childComplexity int, id string) int - UpdateAlertMetadata func(childComplexity int, input UpdateAlertMetadataInput) int + UpdateAlertFeedback func(childComplexity int, input UpdateAlertFeedbackInput) int UpdateAlerts func(childComplexity int, input UpdateAlertsInput) int UpdateAlertsByService func(childComplexity int, input UpdateAlertsByServiceInput) int UpdateBasicAuth func(childComplexity int, input UpdateBasicAuthInput) int @@ -673,7 +673,7 @@ type AlertResolver interface { RecentEvents(ctx context.Context, obj *alert.Alert, input *AlertRecentEventsOptions) (*AlertLogEntryConnection, error) PendingNotifications(ctx context.Context, obj *alert.Alert) ([]AlertPendingNotification, error) Metrics(ctx context.Context, obj *alert.Alert) (*alertmetrics.Metric, error) - Metadata(ctx context.Context, obj *alert.Alert) (*AlertMetadata, error) + Feedback(ctx context.Context, obj *alert.Alert) (*AlertFeedback, error) } type AlertLogEntryResolver interface { Message(ctx context.Context, obj *alertlog.Entry) (string, error) @@ -728,7 +728,7 @@ type MutationResolver interface { UpdateEscalationPolicyStep(ctx context.Context, input UpdateEscalationPolicyStepInput) (bool, error) DeleteAll(ctx context.Context, input []assignment.RawTarget) (bool, error) CreateAlert(ctx context.Context, input CreateAlertInput) (*alert.Alert, error) - UpdateAlertMetadata(ctx context.Context, input UpdateAlertMetadataInput) (bool, error) + UpdateAlertFeedback(ctx context.Context, input UpdateAlertFeedbackInput) (bool, error) CreateService(ctx context.Context, input CreateServiceInput) (*service.Service, error) CreateEscalationPolicy(ctx context.Context, input CreateEscalationPolicyInput) (*escalation.Policy, error) CreateEscalationPolicyStep(ctx context.Context, input CreateEscalationPolicyStepInput) (*escalation.Step, error) @@ -918,19 +918,19 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Alert.Details(childComplexity), true - case "Alert.id": - if e.complexity.Alert.ID == nil { + case "Alert.feedback": + if e.complexity.Alert.Feedback == nil { break } - return e.complexity.Alert.ID(childComplexity), true + return e.complexity.Alert.Feedback(childComplexity), true - case "Alert.metadata": - if e.complexity.Alert.Metadata == nil { + case "Alert.id": + if e.complexity.Alert.ID == nil { break } - return e.complexity.Alert.Metadata(childComplexity), true + return e.complexity.Alert.ID(childComplexity), true case "Alert.metrics": if e.complexity.Alert.Metrics == nil { @@ -1021,6 +1021,20 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.AlertDataPoint.Timestamp(childComplexity), true + case "AlertFeedback.note": + if e.complexity.AlertFeedback.Note == nil { + break + } + + return e.complexity.AlertFeedback.Note(childComplexity), true + + case "AlertFeedback.sentiment": + if e.complexity.AlertFeedback.Sentiment == nil { + break + } + + return e.complexity.AlertFeedback.Sentiment(childComplexity), true + case "AlertLogEntry.id": if e.complexity.AlertLogEntry.ID == nil { break @@ -1063,20 +1077,6 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.AlertLogEntryConnection.PageInfo(childComplexity), true - case "AlertMetadata.note": - if e.complexity.AlertMetadata.Note == nil { - break - } - - return e.complexity.AlertMetadata.Note(childComplexity), true - - case "AlertMetadata.sentiment": - if e.complexity.AlertMetadata.Sentiment == nil { - break - } - - return e.complexity.AlertMetadata.Sentiment(childComplexity), true - case "AlertMetric.closedAt": if e.complexity.AlertMetric.ClosedAt == nil { break @@ -2049,17 +2049,17 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Mutation.TestContactMethod(childComplexity, args["id"].(string)), true - case "Mutation.updateAlertMetadata": - if e.complexity.Mutation.UpdateAlertMetadata == nil { + case "Mutation.updateAlertFeedback": + if e.complexity.Mutation.UpdateAlertFeedback == nil { break } - args, err := ec.field_Mutation_updateAlertMetadata_args(context.TODO(), rawArgs) + args, err := ec.field_Mutation_updateAlertFeedback_args(context.TODO(), rawArgs) if err != nil { return 0, false } - return e.complexity.Mutation.UpdateAlertMetadata(childComplexity, args["input"].(UpdateAlertMetadataInput)), true + return e.complexity.Mutation.UpdateAlertFeedback(childComplexity, args["input"].(UpdateAlertFeedbackInput)), true case "Mutation.updateAlerts": if e.complexity.Mutation.UpdateAlerts == nil { @@ -3972,7 +3972,7 @@ func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler { ec.unmarshalInputTargetInput, ec.unmarshalInputTimeSeriesOptions, ec.unmarshalInputTimeZoneSearchOptions, - ec.unmarshalInputUpdateAlertMetadataInput, + ec.unmarshalInputUpdateAlertFeedbackInput, ec.unmarshalInputUpdateAlertsByServiceInput, ec.unmarshalInputUpdateAlertsInput, ec.unmarshalInputUpdateBasicAuthInput, @@ -4563,13 +4563,13 @@ func (ec *executionContext) field_Mutation_testContactMethod_args(ctx context.Co return args, nil } -func (ec *executionContext) field_Mutation_updateAlertMetadata_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { +func (ec *executionContext) field_Mutation_updateAlertFeedback_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} - var arg0 UpdateAlertMetadataInput + var arg0 UpdateAlertFeedbackInput if tmp, ok := rawArgs["input"]; ok { ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("input")) - arg0, err = ec.unmarshalNUpdateAlertMetadataInput2githubᚗcomᚋtargetᚋgoalertᚋgraphql2ᚐUpdateAlertMetadataInput(ctx, tmp) + arg0, err = ec.unmarshalNUpdateAlertFeedbackInput2githubᚗcomᚋtargetᚋgoalertᚋgraphql2ᚐUpdateAlertFeedbackInput(ctx, tmp) if err != nil { return nil, err } @@ -6064,8 +6064,8 @@ func (ec *executionContext) fieldContext_Alert_metrics(ctx context.Context, fiel return fc, nil } -func (ec *executionContext) _Alert_metadata(ctx context.Context, field graphql.CollectedField, obj *alert.Alert) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_Alert_metadata(ctx, field) +func (ec *executionContext) _Alert_feedback(ctx context.Context, field graphql.CollectedField, obj *alert.Alert) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Alert_feedback(ctx, field) if err != nil { return graphql.Null } @@ -6078,7 +6078,7 @@ func (ec *executionContext) _Alert_metadata(ctx context.Context, field graphql.C }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return ec.resolvers.Alert().Metadata(rctx, obj) + return ec.resolvers.Alert().Feedback(rctx, obj) }) if err != nil { ec.Error(ctx, err) @@ -6087,12 +6087,12 @@ func (ec *executionContext) _Alert_metadata(ctx context.Context, field graphql.C if resTmp == nil { return graphql.Null } - res := resTmp.(*AlertMetadata) + res := resTmp.(*AlertFeedback) fc.Result = res - return ec.marshalOAlertMetadata2ᚖgithub.comᚋtargetᚋgoalertᚋgraphql2ᚐAlertMetadata(ctx, field.Selections, res) + return ec.marshalOAlertFeedback2ᚖgithub.comᚋtargetᚋgoalertᚋgraphql2ᚐAlertFeedback(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext_Alert_metadata(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_Alert_feedback(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "Alert", Field: field, @@ -6101,11 +6101,11 @@ func (ec *executionContext) fieldContext_Alert_metadata(ctx context.Context, fie Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { switch field.Name { case "sentiment": - return ec.fieldContext_AlertMetadata_sentiment(ctx, field) + return ec.fieldContext_AlertFeedback_sentiment(ctx, field) case "note": - return ec.fieldContext_AlertMetadata_note(ctx, field) + return ec.fieldContext_AlertFeedback_note(ctx, field) } - return nil, fmt.Errorf("no field named %q was found under type AlertMetadata", field.Name) + return nil, fmt.Errorf("no field named %q was found under type AlertFeedback", field.Name) }, } return fc, nil @@ -6174,8 +6174,8 @@ func (ec *executionContext) fieldContext_AlertConnection_nodes(ctx context.Conte return ec.fieldContext_Alert_pendingNotifications(ctx, field) case "metrics": return ec.fieldContext_Alert_metrics(ctx, field) - case "metadata": - return ec.fieldContext_Alert_metadata(ctx, field) + case "feedback": + return ec.fieldContext_Alert_feedback(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type Alert", field.Name) }, @@ -6321,6 +6321,91 @@ func (ec *executionContext) fieldContext_AlertDataPoint_alertCount(ctx context.C return fc, nil } +func (ec *executionContext) _AlertFeedback_sentiment(ctx context.Context, field graphql.CollectedField, obj *AlertFeedback) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_AlertFeedback_sentiment(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Sentiment, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(int) + fc.Result = res + return ec.marshalNInt2int(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_AlertFeedback_sentiment(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "AlertFeedback", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type Int does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _AlertFeedback_note(ctx context.Context, field graphql.CollectedField, obj *AlertFeedback) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_AlertFeedback_note(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Note, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*string) + fc.Result = res + return ec.marshalOString2ᚖstring(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_AlertFeedback_note(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "AlertFeedback", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + func (ec *executionContext) _AlertLogEntry_id(ctx context.Context, field graphql.CollectedField, obj *alertlog.Entry) (ret graphql.Marshaler) { fc, err := ec.fieldContext_AlertLogEntry_id(ctx, field) if err != nil { @@ -6606,91 +6691,6 @@ func (ec *executionContext) fieldContext_AlertLogEntryConnection_pageInfo(ctx co return fc, nil } -func (ec *executionContext) _AlertMetadata_sentiment(ctx context.Context, field graphql.CollectedField, obj *AlertMetadata) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_AlertMetadata_sentiment(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { - ctx = rctx // use context from middleware stack in children - return obj.Sentiment, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.(int) - fc.Result = res - return ec.marshalNInt2int(ctx, field.Selections, res) -} - -func (ec *executionContext) fieldContext_AlertMetadata_sentiment(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "AlertMetadata", - Field: field, - IsMethod: false, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type Int does not have child fields") - }, - } - return fc, nil -} - -func (ec *executionContext) _AlertMetadata_note(ctx context.Context, field graphql.CollectedField, obj *AlertMetadata) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_AlertMetadata_note(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { - ctx = rctx // use context from middleware stack in children - return obj.Note, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - return graphql.Null - } - res := resTmp.(*string) - fc.Result = res - return ec.marshalOString2ᚖstring(ctx, field.Selections, res) -} - -func (ec *executionContext) fieldContext_AlertMetadata_note(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "AlertMetadata", - Field: field, - IsMethod: false, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type String does not have child fields") - }, - } - return fc, nil -} - func (ec *executionContext) _AlertMetric_escalated(ctx context.Context, field graphql.CollectedField, obj *alertmetrics.Metric) (ret graphql.Marshaler) { fc, err := ec.fieldContext_AlertMetric_escalated(ctx, field) if err != nil { @@ -11266,8 +11266,8 @@ func (ec *executionContext) fieldContext_Mutation_updateAlerts(ctx context.Conte return ec.fieldContext_Alert_pendingNotifications(ctx, field) case "metrics": return ec.fieldContext_Alert_metrics(ctx, field) - case "metadata": - return ec.fieldContext_Alert_metadata(ctx, field) + case "feedback": + return ec.fieldContext_Alert_feedback(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type Alert", field.Name) }, @@ -11401,8 +11401,8 @@ func (ec *executionContext) fieldContext_Mutation_escalateAlerts(ctx context.Con return ec.fieldContext_Alert_pendingNotifications(ctx, field) case "metrics": return ec.fieldContext_Alert_metrics(ctx, field) - case "metadata": - return ec.fieldContext_Alert_metadata(ctx, field) + case "feedback": + return ec.fieldContext_Alert_feedback(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type Alert", field.Name) }, @@ -11756,8 +11756,8 @@ func (ec *executionContext) fieldContext_Mutation_createAlert(ctx context.Contex return ec.fieldContext_Alert_pendingNotifications(ctx, field) case "metrics": return ec.fieldContext_Alert_metrics(ctx, field) - case "metadata": - return ec.fieldContext_Alert_metadata(ctx, field) + case "feedback": + return ec.fieldContext_Alert_feedback(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type Alert", field.Name) }, @@ -11776,8 +11776,8 @@ func (ec *executionContext) fieldContext_Mutation_createAlert(ctx context.Contex return fc, nil } -func (ec *executionContext) _Mutation_updateAlertMetadata(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_Mutation_updateAlertMetadata(ctx, field) +func (ec *executionContext) _Mutation_updateAlertFeedback(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Mutation_updateAlertFeedback(ctx, field) if err != nil { return graphql.Null } @@ -11790,7 +11790,7 @@ func (ec *executionContext) _Mutation_updateAlertMetadata(ctx context.Context, f }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return ec.resolvers.Mutation().UpdateAlertMetadata(rctx, fc.Args["input"].(UpdateAlertMetadataInput)) + return ec.resolvers.Mutation().UpdateAlertFeedback(rctx, fc.Args["input"].(UpdateAlertFeedbackInput)) }) if err != nil { ec.Error(ctx, err) @@ -11807,7 +11807,7 @@ func (ec *executionContext) _Mutation_updateAlertMetadata(ctx context.Context, f return ec.marshalNBoolean2bool(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext_Mutation_updateAlertMetadata(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_Mutation_updateAlertFeedback(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "Mutation", Field: field, @@ -11824,7 +11824,7 @@ func (ec *executionContext) fieldContext_Mutation_updateAlertMetadata(ctx contex } }() ctx = graphql.WithFieldContext(ctx, fc) - if fc.Args, err = ec.field_Mutation_updateAlertMetadata_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + if fc.Args, err = ec.field_Mutation_updateAlertFeedback_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { ec.Error(ctx, err) return } @@ -14948,8 +14948,8 @@ func (ec *executionContext) fieldContext_Query_alert(ctx context.Context, field return ec.fieldContext_Alert_pendingNotifications(ctx, field) case "metrics": return ec.fieldContext_Alert_metrics(ctx, field) - case "metadata": - return ec.fieldContext_Alert_metadata(ctx, field) + case "feedback": + return ec.fieldContext_Alert_feedback(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type Alert", field.Name) }, @@ -29087,8 +29087,8 @@ func (ec *executionContext) unmarshalInputTimeZoneSearchOptions(ctx context.Cont return it, nil } -func (ec *executionContext) unmarshalInputUpdateAlertMetadataInput(ctx context.Context, obj interface{}) (UpdateAlertMetadataInput, error) { - var it UpdateAlertMetadataInput +func (ec *executionContext) unmarshalInputUpdateAlertFeedbackInput(ctx context.Context, obj interface{}) (UpdateAlertFeedbackInput, error) { + var it UpdateAlertFeedbackInput asMap := map[string]interface{}{} for k, v := range obj.(map[string]interface{}) { asMap[k] = v @@ -30334,7 +30334,7 @@ func (ec *executionContext) _Alert(ctx context.Context, sel ast.SelectionSet, ob return innerFunc(ctx) }) - case "metadata": + case "feedback": field := field innerFunc := func(ctx context.Context) (res graphql.Marshaler) { @@ -30343,7 +30343,7 @@ func (ec *executionContext) _Alert(ctx context.Context, sel ast.SelectionSet, ob ec.Error(ctx, ec.Recover(ctx, r)) } }() - res = ec._Alert_metadata(ctx, field, obj) + res = ec._Alert_feedback(ctx, field, obj) return res } @@ -30432,6 +30432,38 @@ func (ec *executionContext) _AlertDataPoint(ctx context.Context, sel ast.Selecti return out } +var alertFeedbackImplementors = []string{"AlertFeedback"} + +func (ec *executionContext) _AlertFeedback(ctx context.Context, sel ast.SelectionSet, obj *AlertFeedback) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, alertFeedbackImplementors) + out := graphql.NewFieldSet(fields) + var invalids uint32 + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("AlertFeedback") + case "sentiment": + + out.Values[i] = ec._AlertFeedback_sentiment(ctx, field, obj) + + if out.Values[i] == graphql.Null { + invalids++ + } + case "note": + + out.Values[i] = ec._AlertFeedback_note(ctx, field, obj) + + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch() + if invalids > 0 { + return graphql.Null + } + return out +} + var alertLogEntryImplementors = []string{"AlertLogEntry"} func (ec *executionContext) _AlertLogEntry(ctx context.Context, sel ast.SelectionSet, obj *alertlog.Entry) graphql.Marshaler { @@ -30539,38 +30571,6 @@ func (ec *executionContext) _AlertLogEntryConnection(ctx context.Context, sel as return out } -var alertMetadataImplementors = []string{"AlertMetadata"} - -func (ec *executionContext) _AlertMetadata(ctx context.Context, sel ast.SelectionSet, obj *AlertMetadata) graphql.Marshaler { - fields := graphql.CollectFields(ec.OperationContext, sel, alertMetadataImplementors) - out := graphql.NewFieldSet(fields) - var invalids uint32 - for i, field := range fields { - switch field.Name { - case "__typename": - out.Values[i] = graphql.MarshalString("AlertMetadata") - case "sentiment": - - out.Values[i] = ec._AlertMetadata_sentiment(ctx, field, obj) - - if out.Values[i] == graphql.Null { - invalids++ - } - case "note": - - out.Values[i] = ec._AlertMetadata_note(ctx, field, obj) - - default: - panic("unknown field " + strconv.Quote(field.Name)) - } - } - out.Dispatch() - if invalids > 0 { - return graphql.Null - } - return out -} - var alertMetricImplementors = []string{"AlertMetric"} func (ec *executionContext) _AlertMetric(ctx context.Context, sel ast.SelectionSet, obj *alertmetrics.Metric) graphql.Marshaler { @@ -31950,10 +31950,10 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) return ec._Mutation_createAlert(ctx, field) }) - case "updateAlertMetadata": + case "updateAlertFeedback": out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { - return ec._Mutation_updateAlertMetadata(ctx, field) + return ec._Mutation_updateAlertFeedback(ctx, field) }) if out.Values[i] == graphql.Null { @@ -38247,8 +38247,8 @@ func (ec *executionContext) marshalNTimeZoneConnection2ᚖgithub.comᚋtarget return ec._TimeZoneConnection(ctx, sel, v) } -func (ec *executionContext) unmarshalNUpdateAlertMetadataInput2githubᚗcomᚋtargetᚋgoalertᚋgraphql2ᚐUpdateAlertMetadataInput(ctx context.Context, v interface{}) (UpdateAlertMetadataInput, error) { - res, err := ec.unmarshalInputUpdateAlertMetadataInput(ctx, v) +func (ec *executionContext) unmarshalNUpdateAlertFeedbackInput2githubᚗcomᚋtargetᚋgoalertᚋgraphql2ᚐUpdateAlertFeedbackInput(ctx context.Context, v interface{}) (UpdateAlertFeedbackInput, error) { + res, err := ec.unmarshalInputUpdateAlertFeedbackInput(ctx, v) return res, graphql.ErrorOnPath(ctx, err) } @@ -38975,11 +38975,11 @@ func (ec *executionContext) marshalOAlert2ᚖgithub.comᚋtargetᚋgoalertᚋa return ec._Alert(ctx, sel, v) } -func (ec *executionContext) marshalOAlertMetadata2ᚖgithub.comᚋtargetᚋgoalertᚋgraphql2ᚐAlertMetadata(ctx context.Context, sel ast.SelectionSet, v *AlertMetadata) graphql.Marshaler { +func (ec *executionContext) marshalOAlertFeedback2ᚖgithub.comᚋtargetᚋgoalertᚋgraphql2ᚐAlertFeedback(ctx context.Context, sel ast.SelectionSet, v *AlertFeedback) graphql.Marshaler { if v == nil { return graphql.Null } - return ec._AlertMetadata(ctx, sel, v) + return ec._AlertFeedback(ctx, sel, v) } func (ec *executionContext) marshalOAlertMetric2ᚖgithub.comᚋtargetᚋgoalertᚋalertᚋalertmetricsᚐMetric(ctx context.Context, sel ast.SelectionSet, v *alertmetrics.Metric) graphql.Marshaler { diff --git a/graphql2/graphqlapp/alert.go b/graphql2/graphqlapp/alert.go index 88f6c85c2a..48930a762d 100644 --- a/graphql2/graphqlapp/alert.go +++ b/graphql2/graphqlapp/alert.go @@ -360,18 +360,18 @@ func (m *Mutation) CreateAlert(ctx context.Context, input graphql2.CreateAlertIn return m.AlertStore.Create(ctx, a) } -func (a *Alert) Metadata(ctx context.Context, raw *alert.Alert) (*graphql2.AlertMetadata, error) { +func (a *Alert) Feedback(ctx context.Context, raw *alert.Alert) (*graphql2.AlertFeedback, error) { am, err := a.AlertStore.Metadata(ctx, raw.ID) if err != nil { return nil, err } - return &graphql2.AlertMetadata{ + return &graphql2.AlertFeedback{ Sentiment: am.Sentiment, Note: &am.Note, }, nil } -func (m *Mutation) UpdateAlertMetadata(ctx context.Context, input graphql2.UpdateAlertMetadataInput) (bool, error) { +func (m *Mutation) UpdateAlertFeedback(ctx context.Context, input graphql2.UpdateAlertFeedbackInput) (bool, error) { am := &alert.Metadata{ AlertID: input.AlertID, } diff --git a/graphql2/models_gen.go b/graphql2/models_gen.go index 57d9b6d3c6..3a7d744f9b 100644 --- a/graphql2/models_gen.go +++ b/graphql2/models_gen.go @@ -37,16 +37,16 @@ type AlertDataPoint struct { AlertCount int `json:"alertCount"` } +type AlertFeedback struct { + Sentiment int `json:"sentiment"` + Note *string `json:"note,omitempty"` +} + type AlertLogEntryConnection struct { Nodes []alertlog.Entry `json:"nodes"` PageInfo *PageInfo `json:"pageInfo"` } -type AlertMetadata struct { - Sentiment int `json:"sentiment"` - Note *string `json:"note,omitempty"` -} - type AlertMetricsOptions struct { RInterval timeutil.ISORInterval `json:"rInterval"` FilterByServiceID []string `json:"filterByServiceID,omitempty"` @@ -553,7 +553,7 @@ type TimeZoneSearchOptions struct { Omit []string `json:"omit,omitempty"` } -type UpdateAlertMetadataInput struct { +type UpdateAlertFeedbackInput struct { AlertID int `json:"alertID"` Sentiment *int `json:"sentiment,omitempty"` Note *string `json:"note,omitempty"` diff --git a/graphql2/schema.graphql b/graphql2/schema.graphql index 83d66d7d02..4f781f4a68 100644 --- a/graphql2/schema.graphql +++ b/graphql2/schema.graphql @@ -514,7 +514,7 @@ type Mutation { deleteAll(input: [TargetInput!]): Boolean! createAlert(input: CreateAlertInput!): Alert - updateAlertMetadata(input: UpdateAlertMetadataInput!): Boolean! + updateAlertFeedback(input: UpdateAlertFeedbackInput!): Boolean! createService(input: CreateServiceInput!): Service createEscalationPolicy(input: CreateEscalationPolicyInput!): EscalationPolicy @@ -592,7 +592,7 @@ input CreateAlertInput { sanitize: Boolean } -input UpdateAlertMetadataInput { +input UpdateAlertFeedbackInput { alertID: Int! sentiment: Int note: String @@ -1075,7 +1075,7 @@ type Alert { # metrics are only available for closed alerts metrics: AlertMetric - metadata: AlertMetadata + feedback: AlertFeedback } type AlertMetric { @@ -1125,7 +1125,7 @@ type AlertState { repeatCount: Int! } -type AlertMetadata { +type AlertFeedback { sentiment: Int! note: String } diff --git a/migrate/migrations/20230616110941-add-alerts-metadata.sql b/migrate/migrations/20230616110941-add-alerts-feedback.sql similarity index 74% rename from migrate/migrations/20230616110941-add-alerts-metadata.sql rename to migrate/migrations/20230616110941-add-alerts-feedback.sql index e68a65378b..a520d8309b 100644 --- a/migrate/migrations/20230616110941-add-alerts-metadata.sql +++ b/migrate/migrations/20230616110941-add-alerts-feedback.sql @@ -1,5 +1,5 @@ -- +migrate Up -CREATE TABLE alert_metadata +CREATE TABLE alert_feedback ( alert_id BIGINT PRIMARY KEY REFERENCES alerts (id) ON DELETE CASCADE, sentiment INT NOT NULL DEFAULT 0, @@ -7,4 +7,4 @@ CREATE TABLE alert_metadata ); -- +migrate Down -DROP TABLE alert_metadata; +DROP TABLE alert_feedback; diff --git a/web/src/app/alerts/components/AlertDetails.tsx b/web/src/app/alerts/components/AlertDetails.tsx index 61718299f3..07c34417b8 100644 --- a/web/src/app/alerts/components/AlertDetails.tsx +++ b/web/src/app/alerts/components/AlertDetails.tsx @@ -43,7 +43,7 @@ import { } from '../../../schema' import ServiceNotices from '../../services/ServiceNotices' import { Time } from '../../util/Time' -import AlertMetadata from './AlertMetadata' +import AlertFeedback from './AlertFeedback' interface AlertDetailsProps { data: Alert @@ -389,7 +389,7 @@ export default function AlertDetails(props: AlertDetailsProps): JSX.Element { , + , ]} /> diff --git a/web/src/app/alerts/components/AlertMetadata.tsx b/web/src/app/alerts/components/AlertFeedback.tsx similarity index 82% rename from web/src/app/alerts/components/AlertMetadata.tsx rename to web/src/app/alerts/components/AlertFeedback.tsx index fad2d634e0..b0295652cb 100644 --- a/web/src/app/alerts/components/AlertMetadata.tsx +++ b/web/src/app/alerts/components/AlertFeedback.tsx @@ -11,10 +11,10 @@ import { import { DEBOUNCE_DELAY } from '../../config' const query = gql` - query AlertMetadataQuery($id: Int!) { + query AlertFeedbackQuery($id: Int!) { alert(id: $id) { id - metadata { + feedback { sentiment note } @@ -23,16 +23,16 @@ const query = gql` ` const mutation = gql` - mutation UpdateMetadataMutation($input: UpdateAlertMetadataInput!) { - updateAlertMetadata(input: $input) + mutation UpdateFeedbackMutation($input: UpdateAlertFeedbackInput!) { + updateAlertFeedback(input: $input) } ` -interface AlertMetadataProps { +interface AlertFeedbackProps { alertID: number } -export default function AlertMetadata(props: AlertMetadataProps): JSX.Element { +export default function AlertFeedback(props: AlertFeedbackProps): JSX.Element { const { alertID } = props const [{ data, fetching, error }] = useQuery({ query, @@ -41,7 +41,7 @@ export default function AlertMetadata(props: AlertMetadataProps): JSX.Element { }, requestPolicy: 'cache-first', }) - const [note, setNote] = useState(data?.alert?.metadata?.note ?? '') + const [note, setNote] = useState(data?.alert?.feedback?.note ?? '') const [mutationStatus, commit] = useMutation(mutation) // Debounce setting note @@ -59,17 +59,17 @@ export default function AlertMetadata(props: AlertMetadataProps): JSX.Element { }, [note]) useEffect(() => { - setNote(data?.alert?.metadata?.note ?? '') - }, [data?.alert?.metadata?.note]) + setNote(data?.alert?.feedback?.note ?? '') + }, [data?.alert?.feedback?.note]) const thumbUp = - !fetching && !error && data?.alert?.metadata?.sentiment === 1 ? ( + !fetching && !error && data?.alert?.feedback?.sentiment === 1 ? ( ) : ( ) - const isDown = data?.alert?.metadata?.sentiment === -1 + const isDown = data?.alert?.feedback?.sentiment === -1 const thumbDown = !fetching && !error && isDown ? : diff --git a/web/src/schema.d.ts b/web/src/schema.d.ts index a7e3f532f5..840ea13df7 100644 --- a/web/src/schema.d.ts +++ b/web/src/schema.d.ts @@ -389,7 +389,7 @@ export interface Mutation { updateEscalationPolicyStep: boolean deleteAll: boolean createAlert?: null | Alert - updateAlertMetadata: boolean + updateAlertFeedback: boolean createService?: null | Service createEscalationPolicy?: null | EscalationPolicy createEscalationPolicyStep?: null | EscalationPolicyStep @@ -442,7 +442,7 @@ export interface CreateAlertInput { sanitize?: null | boolean } -export interface UpdateAlertMetadataInput { +export interface UpdateAlertFeedbackInput { alertID: number sentiment?: null | number note?: null | string @@ -842,7 +842,7 @@ export interface Alert { recentEvents: AlertLogEntryConnection pendingNotifications: AlertPendingNotification[] metrics?: null | AlertMetric - metadata?: null | AlertMetadata + feedback?: null | AlertFeedback } export interface AlertMetric { @@ -887,7 +887,7 @@ export interface AlertState { repeatCount: number } -export interface AlertMetadata { +export interface AlertFeedback { sentiment: number note?: null | string } From 4649165ceb2b3a7af5df0acfc284d96e41e70aac Mon Sep 17 00:00:00 2001 From: Nathaniel Cook Date: Tue, 20 Jun 2023 11:28:36 -0700 Subject: [PATCH 04/35] update transition for textfields --- .../app/alerts/components/AlertFeedback.tsx | 14 ++++++++-- web/src/app/util/Search.js | 28 ++----------------- web/src/app/util/Transitions.tsx | 26 +++++++++++++++++ 3 files changed, 39 insertions(+), 29 deletions(-) diff --git a/web/src/app/alerts/components/AlertFeedback.tsx b/web/src/app/alerts/components/AlertFeedback.tsx index b0295652cb..3f336feaaf 100644 --- a/web/src/app/alerts/components/AlertFeedback.tsx +++ b/web/src/app/alerts/components/AlertFeedback.tsx @@ -9,6 +9,7 @@ import { Info, } from '@mui/icons-material' import { DEBOUNCE_DELAY } from '../../config' +import { transitionStyles } from '../../util/Transitions' const query = gql` query AlertFeedbackQuery($id: Int!) { @@ -34,6 +35,8 @@ interface AlertFeedbackProps { export default function AlertFeedback(props: AlertFeedbackProps): JSX.Element { const { alertID } = props + const classes = transitionStyles() + const [{ data, fetching, error }] = useQuery({ query, variables: { @@ -117,16 +120,21 @@ export default function AlertFeedback(props: AlertFeedbackProps): JSX.Element { - + - setNote(e.target.value)} /> + setNote(e.target.value)} + /> diff --git a/web/src/app/util/Search.js b/web/src/app/util/Search.js index 68853b5638..474f3a5ca8 100644 --- a/web/src/app/util/Search.js +++ b/web/src/app/util/Search.js @@ -1,6 +1,5 @@ import React, { useState, useEffect, useRef } from 'react' import p from 'prop-types' -import makeStyles from '@mui/styles/makeStyles' import AppBar from '@mui/material/AppBar' import Hidden from '@mui/material/Hidden' import IconButton from '@mui/material/IconButton' @@ -12,30 +11,7 @@ import { Close as CloseIcon, Search as SearchIcon } from '@mui/icons-material' import { DEBOUNCE_DELAY } from '../config' import AppBarSearchContainer from './AppBarSearchContainer' import { useURLParam } from '../actions' - -const useStyles = makeStyles((theme) => { - return { - transition: { - [theme.breakpoints.down('md')]: { - flex: 1, - }, - [theme.breakpoints.up('md')]: { - '& input:focus': { - minWidth: 275, - }, - '& input:not(:placeholder-shown)': { - minWidth: 275, - }, - '& input': { - minWidth: 180, - transitionProperty: 'min-width', - transitionDuration: theme.transitions.duration.standard, - transitionTimingFunction: theme.transitions.easing.easeInOut, - }, - }, - }, - } -}) +import { transitionStyles } from './Transitions' /* * Renders a search text field that utilizes the URL params to regulate @@ -52,7 +28,7 @@ export default function Search(props) { // or from a local event so we don't lose typed characters. const [prevParamValue, setPrevParamValue] = useState(searchParam) - const classes = useStyles() + const classes = transitionStyles() const [search, setSearch] = useState(searchParam) const [showMobile, setShowMobile] = useState(Boolean(search)) const fieldRef = useRef() diff --git a/web/src/app/util/Transitions.tsx b/web/src/app/util/Transitions.tsx index e3a6cd99bb..6381aff810 100644 --- a/web/src/app/util/Transitions.tsx +++ b/web/src/app/util/Transitions.tsx @@ -1,8 +1,34 @@ import React, { ReactElement } from 'react' +import { Theme } from '@mui/material' import Fade from '@mui/material/Fade' import Slide from '@mui/material/Slide' +import makeStyles from '@mui/styles/makeStyles' import { TransitionProps } from '@mui/material/transitions' +export const transitionStyles = makeStyles((theme: Theme) => { + return { + transition: { + [theme.breakpoints.down('md')]: { + flex: 1, + }, + [theme.breakpoints.up('md')]: { + '& input:focus': { + minWidth: 275, + }, + '& input:not(:placeholder-shown)': { + minWidth: 275, + }, + '& input': { + minWidth: 180, + transitionProperty: 'min-width', + transitionDuration: theme.transitions.duration.standard, + transitionTimingFunction: theme.transitions.easing.easeInOut, + }, + }, + }, + } +}) + export const FadeTransition = React.forwardRef( ({ children, ...props }: TransitionProps, ref) => ( From b1a2c242d7ac0c7e9ad85270a77ce184fef4951e Mon Sep 17 00:00:00 2001 From: Nathaniel Cook Date: Tue, 20 Jun 2023 11:33:50 -0700 Subject: [PATCH 05/35] fix console warning --- web/src/app/alerts/components/AlertDetails.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/web/src/app/alerts/components/AlertDetails.tsx b/web/src/app/alerts/components/AlertDetails.tsx index 07c34417b8..2024e56669 100644 --- a/web/src/app/alerts/components/AlertDetails.tsx +++ b/web/src/app/alerts/components/AlertDetails.tsx @@ -327,6 +327,7 @@ export default function AlertDetails(props: AlertDetailsProps): JSX.Element { return ( From 7ce3c3cf49f7a02fbc9d5f807b8ca9085a31bc56 Mon Sep 17 00:00:00 2001 From: Nathaniel Cook Date: Tue, 20 Jun 2023 11:45:45 -0700 Subject: [PATCH 06/35] invalidate cache on feedback button press --- .../app/alerts/components/AlertFeedback.tsx | 37 +++++++++++++------ 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/web/src/app/alerts/components/AlertFeedback.tsx b/web/src/app/alerts/components/AlertFeedback.tsx index 3f336feaaf..fc1284c2b3 100644 --- a/web/src/app/alerts/components/AlertFeedback.tsx +++ b/web/src/app/alerts/components/AlertFeedback.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react' +import React, { useState, useEffect, useMemo } from 'react' import { useQuery, useMutation, gql } from 'urql' import { Grid, Grow, IconButton, Tooltip, TextField } from '@mui/material' import { @@ -36,9 +36,16 @@ interface AlertFeedbackProps { export default function AlertFeedback(props: AlertFeedbackProps): JSX.Element { const { alertID } = props const classes = transitionStyles() + const [cacheCount, setCacheCount] = useState(0) // reset cache on tick + // stable query reference + const context = useMemo( + () => ({ additionalTypenames: ['Feedback'] }), + [cacheCount], + ) const [{ data, fetching, error }] = useQuery({ query, + context, variables: { id: alertID, }, @@ -88,12 +95,16 @@ export default function AlertFeedback(props: AlertFeedbackProps): JSX.Element { { - commit({ - input: { - alertID, - sentiment: 1, + setCacheCount(cacheCount + 1) + commit( + { + input: { + alertID, + sentiment: 1, + }, }, - }) + { additionalTypenames: ['Feedback'] }, + ) }} size='large' > @@ -103,12 +114,16 @@ export default function AlertFeedback(props: AlertFeedbackProps): JSX.Element { { - commit({ - input: { - alertID, - sentiment: -1, + setCacheCount(cacheCount + 1) + commit( + { + input: { + alertID, + sentiment: -1, + }, }, - }) + { additionalTypenames: ['Feedback'] }, + ) }} size='large' > From 1c23810b309a6fa645fcc9853912508e1b2bcc1b Mon Sep 17 00:00:00 2001 From: Nathaniel Cook Date: Tue, 20 Jun 2023 13:04:51 -0700 Subject: [PATCH 07/35] add testing and aria labels --- .../app/alerts/components/AlertFeedback.tsx | 6 ++++ web/src/cypress/e2e/alerts.cy.ts | 28 +++++++++++++++++-- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/web/src/app/alerts/components/AlertFeedback.tsx b/web/src/app/alerts/components/AlertFeedback.tsx index fc1284c2b3..5939822306 100644 --- a/web/src/app/alerts/components/AlertFeedback.tsx +++ b/web/src/app/alerts/components/AlertFeedback.tsx @@ -79,6 +79,7 @@ export default function AlertFeedback(props: AlertFeedbackProps): JSX.Element { ) + const isUp = data?.alert?.feedback?.sentiment === 1 const isDown = data?.alert?.feedback?.sentiment === -1 const thumbDown = !fetching && !error && isDown ? : @@ -94,6 +95,7 @@ export default function AlertFeedback(props: AlertFeedbackProps): JSX.Element { {mutationStatus.error?.message} { setCacheCount(cacheCount + 1) commit( @@ -113,6 +115,9 @@ export default function AlertFeedback(props: AlertFeedbackProps): JSX.Element { { setCacheCount(cacheCount + 1) commit( @@ -145,6 +150,7 @@ export default function AlertFeedback(props: AlertFeedbackProps): JSX.Element { }} > { // ack - cy.get('button[aria-label=Acknowledge]').click() + cy.get('button').contains('Acknowledge').click() cy.get('body').should('contain', 'ACKNOWLEDGED') cy.get('body').should('not.contain', 'UNACKNOWLEDGED') cy.get('body').should('contain', 'Acknowledged by Cypress User') // escalate - cy.get('button[aria-label=Escalate]').click() + cy.get('button').contains('Escalate').click() cy.get('body').should('contain', 'Escalation requested by Cypress User') cy.reload() // allows time for escalation request to process // close - cy.get('button[aria-label=Close]').click() + cy.get('button').contains('Close').click() cy.get('body').should('contain', 'Closed by Cypress User') cy.get('body').should('contain', 'CLOSED') }) + + it('should set alert feedback', () => { + // upvote alert + cy.get('button[aria-label="Mark alert as useful"]').click() + cy.get('button[aria-label="Mark alert as useful"]').should('not.exist') + cy.get('button[aria-label="Alert marked as useful"]').should('be.visible') + cy.get('[aria-label="Alert Note"]').should('not.exist') + + // downvote alert + cy.get('button[aria-label="Mark alert as not useful"]').click() + cy.get('button[aria-label="Mark alert as not useful"]').should( + 'not.exist', + ) + cy.get('button[aria-label="Alert marked as not useful"]').should( + 'be.visible', + ) + + // set alert note + const value = 'Test' + cy.get('[aria-label="Alert Note"]').type(value) + cy.get('[aria-label="Alert Note"] input').should('have.value', value) + }) }) describe('Alert Details Logs', () => { From e8c89f08572930207e1d740c2a5b7f437836e088 Mon Sep 17 00:00:00 2001 From: Nathaniel Cook Date: Tue, 20 Jun 2023 13:17:12 -0700 Subject: [PATCH 08/35] metadata -> feedback --- alert/{metadata.go => feedback.go} | 4 ++-- alert/store.go | 32 +++++++++++++++--------------- graphql2/graphqlapp/alert.go | 10 +++++----- 3 files changed, 23 insertions(+), 23 deletions(-) rename alert/{metadata.go => feedback.go} (53%) diff --git a/alert/metadata.go b/alert/feedback.go similarity index 53% rename from alert/metadata.go rename to alert/feedback.go index 7d8c5c78ba..6cbe4211be 100644 --- a/alert/metadata.go +++ b/alert/feedback.go @@ -1,7 +1,7 @@ package alert -// Metadata represents user provided information about a given alert -type Metadata struct { +// Feedback represents user provided information about a given alert +type Feedback struct { // ID is the ID of the alert. AlertID int Sentiment int diff --git a/alert/store.go b/alert/store.go index d04e52a4d1..fae7c926e2 100644 --- a/alert/store.go +++ b/alert/store.go @@ -52,8 +52,8 @@ type Store struct { epState *sql.Stmt svcInfo *sql.Stmt - metadata *sql.Stmt - updateMetadata *sql.Stmt + feedback *sql.Stmt + updateFeedback *sql.Stmt } // A Trigger signals that an alert needs to be processed @@ -226,14 +226,14 @@ func NewStore(ctx context.Context, db *sql.DB, logDB *alertlog.Store) (*Store, e WHERE id = $1 `), - metadata: p(` + feedback: p(` SELECT alert_id, sentiment, note FROM alert_feedback WHERE alert_id = $1 `), - updateMetadata: p(` + updateFeedback: p(` INSERT INTO alert_feedback (alert_id, sentiment, note) VALUES ($1, $2, $3) ON CONFLICT (alert_id) DO UPDATE @@ -876,37 +876,37 @@ func (s *Store) State(ctx context.Context, alertIDs []int) ([]State, error) { return list, nil } -func (s *Store) Metadata(ctx context.Context, alertID int) (am Metadata, err error) { - row := s.metadata.QueryRowContext(ctx, alertID) - err = row.Scan(&am.AlertID, &am.Sentiment, &am.Note) +func (s *Store) Feedback(ctx context.Context, alertID int) (f Feedback, err error) { + row := s.feedback.QueryRowContext(ctx, alertID) + err = row.Scan(&f.AlertID, &f.Sentiment, &f.Note) if errors.Is(err, sql.ErrNoRows) { - return Metadata{ + return Feedback{ AlertID: alertID, Sentiment: 0, Note: "", }, nil } - return am, err + return f, err } -func (s Store) UpdateMetadata(ctx context.Context, metadata *Metadata) error { +func (s Store) UpdateFeedback(ctx context.Context, feedback *Feedback) error { err := permission.LimitCheckAny(ctx, permission.System, permission.User) if err != nil { return err } - am, err := (s).Metadata(ctx, metadata.AlertID) + f, err := (s).Feedback(ctx, feedback.AlertID) if err != nil { return err } - if metadata.Sentiment != 0 { - am.Sentiment = metadata.Sentiment + if feedback.Sentiment != 0 { + f.Sentiment = feedback.Sentiment } - if metadata.Note != "" { - am.Note = metadata.Note + if feedback.Note != "" { + f.Note = feedback.Note } - _, err = s.updateMetadata.ExecContext(ctx, metadata.AlertID, am.Sentiment, am.Note) + _, err = s.updateFeedback.ExecContext(ctx, feedback.AlertID, f.Sentiment, f.Note) if err != nil { return err } diff --git a/graphql2/graphqlapp/alert.go b/graphql2/graphqlapp/alert.go index 48930a762d..a390f00581 100644 --- a/graphql2/graphqlapp/alert.go +++ b/graphql2/graphqlapp/alert.go @@ -361,7 +361,7 @@ func (m *Mutation) CreateAlert(ctx context.Context, input graphql2.CreateAlertIn } func (a *Alert) Feedback(ctx context.Context, raw *alert.Alert) (*graphql2.AlertFeedback, error) { - am, err := a.AlertStore.Metadata(ctx, raw.ID) + am, err := a.AlertStore.Feedback(ctx, raw.ID) if err != nil { return nil, err } @@ -372,18 +372,18 @@ func (a *Alert) Feedback(ctx context.Context, raw *alert.Alert) (*graphql2.Alert } func (m *Mutation) UpdateAlertFeedback(ctx context.Context, input graphql2.UpdateAlertFeedbackInput) (bool, error) { - am := &alert.Metadata{ + f := &alert.Feedback{ AlertID: input.AlertID, } if input.Sentiment != nil { - am.Sentiment = *input.Sentiment + f.Sentiment = *input.Sentiment } if input.Note != nil { - am.Note = *input.Note + f.Note = *input.Note } - err := m.AlertStore.UpdateMetadata(ctx, am) + err := m.AlertStore.UpdateFeedback(ctx, f) if err != nil { return false, err } From 80b629cf897f2268d6ae2b40d4b6b249c0dcaac5 Mon Sep 17 00:00:00 2001 From: Nathaniel Cook Date: Tue, 20 Jun 2023 13:23:11 -0700 Subject: [PATCH 09/35] remove comment --- alert/feedback.go | 1 - 1 file changed, 1 deletion(-) diff --git a/alert/feedback.go b/alert/feedback.go index 6cbe4211be..7263d5d35d 100644 --- a/alert/feedback.go +++ b/alert/feedback.go @@ -2,7 +2,6 @@ package alert // Feedback represents user provided information about a given alert type Feedback struct { - // ID is the ID of the alert. AlertID int Sentiment int Note string From e369f3689a96b1034a5d8b37451f802709545be4 Mon Sep 17 00:00:00 2001 From: Nathaniel Cook Date: Tue, 27 Jun 2023 08:51:09 -0700 Subject: [PATCH 10/35] remove sentiment field --- alert/feedback.go | 5 +- alert/store.go | 20 ++-- graphql2/generated.go | 76 +------------ graphql2/graphqlapp/alert.go | 10 +- graphql2/models_gen.go | 8 +- graphql2/schema.graphql | 4 +- .../20230616110941-add-alerts-feedback.sql | 1 - .../app/alerts/components/AlertFeedback.tsx | 100 +----------------- 8 files changed, 20 insertions(+), 204 deletions(-) diff --git a/alert/feedback.go b/alert/feedback.go index 7263d5d35d..b2a0daf48a 100644 --- a/alert/feedback.go +++ b/alert/feedback.go @@ -2,7 +2,6 @@ package alert // Feedback represents user provided information about a given alert type Feedback struct { - AlertID int - Sentiment int - Note string + AlertID int + Note string } diff --git a/alert/store.go b/alert/store.go index fae7c926e2..50061c3757 100644 --- a/alert/store.go +++ b/alert/store.go @@ -228,16 +228,16 @@ func NewStore(ctx context.Context, db *sql.DB, logDB *alertlog.Store) (*Store, e feedback: p(` SELECT - alert_id, sentiment, note + alert_id, note FROM alert_feedback WHERE alert_id = $1 `), updateFeedback: p(` - INSERT INTO alert_feedback (alert_id, sentiment, note) - VALUES ($1, $2, $3) + INSERT INTO alert_feedback (alert_id, note) + VALUES ($1, $2) ON CONFLICT (alert_id) DO UPDATE - SET sentiment = $2, note = $3 + SET note = $2 WHERE alert_feedback.alert_id = $1 `), }, prep.Err @@ -878,12 +878,11 @@ func (s *Store) State(ctx context.Context, alertIDs []int) ([]State, error) { func (s *Store) Feedback(ctx context.Context, alertID int) (f Feedback, err error) { row := s.feedback.QueryRowContext(ctx, alertID) - err = row.Scan(&f.AlertID, &f.Sentiment, &f.Note) + err = row.Scan(&f.AlertID, &f.Note) if errors.Is(err, sql.ErrNoRows) { return Feedback{ - AlertID: alertID, - Sentiment: 0, - Note: "", + AlertID: alertID, + Note: "", }, nil } return f, err @@ -899,14 +898,11 @@ func (s Store) UpdateFeedback(ctx context.Context, feedback *Feedback) error { if err != nil { return err } - if feedback.Sentiment != 0 { - f.Sentiment = feedback.Sentiment - } if feedback.Note != "" { f.Note = feedback.Note } - _, err = s.updateFeedback.ExecContext(ctx, feedback.AlertID, f.Sentiment, f.Note) + _, err = s.updateFeedback.ExecContext(ctx, feedback.AlertID, f.Note) if err != nil { return err } diff --git a/graphql2/generated.go b/graphql2/generated.go index 6acd16823a..bf1eb98924 100644 --- a/graphql2/generated.go +++ b/graphql2/generated.go @@ -119,8 +119,7 @@ type ComplexityRoot struct { } AlertFeedback struct { - Note func(childComplexity int) int - Sentiment func(childComplexity int) int + Note func(childComplexity int) int } AlertLogEntry struct { @@ -1028,13 +1027,6 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.AlertFeedback.Note(childComplexity), true - case "AlertFeedback.sentiment": - if e.complexity.AlertFeedback.Sentiment == nil { - break - } - - return e.complexity.AlertFeedback.Sentiment(childComplexity), true - case "AlertLogEntry.id": if e.complexity.AlertLogEntry.ID == nil { break @@ -6100,8 +6092,6 @@ func (ec *executionContext) fieldContext_Alert_feedback(ctx context.Context, fie IsResolver: true, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { switch field.Name { - case "sentiment": - return ec.fieldContext_AlertFeedback_sentiment(ctx, field) case "note": return ec.fieldContext_AlertFeedback_note(ctx, field) } @@ -6321,50 +6311,6 @@ func (ec *executionContext) fieldContext_AlertDataPoint_alertCount(ctx context.C return fc, nil } -func (ec *executionContext) _AlertFeedback_sentiment(ctx context.Context, field graphql.CollectedField, obj *AlertFeedback) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_AlertFeedback_sentiment(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { - ctx = rctx // use context from middleware stack in children - return obj.Sentiment, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.(int) - fc.Result = res - return ec.marshalNInt2int(ctx, field.Selections, res) -} - -func (ec *executionContext) fieldContext_AlertFeedback_sentiment(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "AlertFeedback", - Field: field, - IsMethod: false, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type Int does not have child fields") - }, - } - return fc, nil -} - func (ec *executionContext) _AlertFeedback_note(ctx context.Context, field graphql.CollectedField, obj *AlertFeedback) (ret graphql.Marshaler) { fc, err := ec.fieldContext_AlertFeedback_note(ctx, field) if err != nil { @@ -29094,7 +29040,7 @@ func (ec *executionContext) unmarshalInputUpdateAlertFeedbackInput(ctx context.C asMap[k] = v } - fieldsInOrder := [...]string{"alertID", "sentiment", "note"} + fieldsInOrder := [...]string{"alertID", "note"} for _, k := range fieldsInOrder { v, ok := asMap[k] if !ok { @@ -29110,20 +29056,11 @@ func (ec *executionContext) unmarshalInputUpdateAlertFeedbackInput(ctx context.C return it, err } it.AlertID = data - case "sentiment": - var err error - - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("sentiment")) - data, err := ec.unmarshalOInt2ᚖint(ctx, v) - if err != nil { - return it, err - } - it.Sentiment = data case "note": var err error ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("note")) - data, err := ec.unmarshalOString2ᚖstring(ctx, v) + data, err := ec.unmarshalNString2string(ctx, v) if err != nil { return it, err } @@ -30442,13 +30379,6 @@ func (ec *executionContext) _AlertFeedback(ctx context.Context, sel ast.Selectio switch field.Name { case "__typename": out.Values[i] = graphql.MarshalString("AlertFeedback") - case "sentiment": - - out.Values[i] = ec._AlertFeedback_sentiment(ctx, field, obj) - - if out.Values[i] == graphql.Null { - invalids++ - } case "note": out.Values[i] = ec._AlertFeedback_note(ctx, field, obj) diff --git a/graphql2/graphqlapp/alert.go b/graphql2/graphqlapp/alert.go index a390f00581..b6e20833e4 100644 --- a/graphql2/graphqlapp/alert.go +++ b/graphql2/graphqlapp/alert.go @@ -366,8 +366,7 @@ func (a *Alert) Feedback(ctx context.Context, raw *alert.Alert) (*graphql2.Alert return nil, err } return &graphql2.AlertFeedback{ - Sentiment: am.Sentiment, - Note: &am.Note, + Note: &am.Note, }, nil } @@ -376,13 +375,6 @@ func (m *Mutation) UpdateAlertFeedback(ctx context.Context, input graphql2.Updat AlertID: input.AlertID, } - if input.Sentiment != nil { - f.Sentiment = *input.Sentiment - } - if input.Note != nil { - f.Note = *input.Note - } - err := m.AlertStore.UpdateFeedback(ctx, f) if err != nil { return false, err diff --git a/graphql2/models_gen.go b/graphql2/models_gen.go index 3a7d744f9b..e9be0a92a2 100644 --- a/graphql2/models_gen.go +++ b/graphql2/models_gen.go @@ -38,8 +38,7 @@ type AlertDataPoint struct { } type AlertFeedback struct { - Sentiment int `json:"sentiment"` - Note *string `json:"note,omitempty"` + Note *string `json:"note,omitempty"` } type AlertLogEntryConnection struct { @@ -554,9 +553,8 @@ type TimeZoneSearchOptions struct { } type UpdateAlertFeedbackInput struct { - AlertID int `json:"alertID"` - Sentiment *int `json:"sentiment,omitempty"` - Note *string `json:"note,omitempty"` + AlertID int `json:"alertID"` + Note string `json:"note"` } type UpdateAlertsByServiceInput struct { diff --git a/graphql2/schema.graphql b/graphql2/schema.graphql index 4f781f4a68..7cdca022c6 100644 --- a/graphql2/schema.graphql +++ b/graphql2/schema.graphql @@ -594,8 +594,7 @@ input CreateAlertInput { input UpdateAlertFeedbackInput { alertID: Int! - sentiment: Int - note: String + note: String! } input CreateUserInput { @@ -1126,7 +1125,6 @@ type AlertState { } type AlertFeedback { - sentiment: Int! note: String } diff --git a/migrate/migrations/20230616110941-add-alerts-feedback.sql b/migrate/migrations/20230616110941-add-alerts-feedback.sql index a520d8309b..f197dfc36c 100644 --- a/migrate/migrations/20230616110941-add-alerts-feedback.sql +++ b/migrate/migrations/20230616110941-add-alerts-feedback.sql @@ -2,7 +2,6 @@ CREATE TABLE alert_feedback ( alert_id BIGINT PRIMARY KEY REFERENCES alerts (id) ON DELETE CASCADE, - sentiment INT NOT NULL DEFAULT 0, note TEXT ); diff --git a/web/src/app/alerts/components/AlertFeedback.tsx b/web/src/app/alerts/components/AlertFeedback.tsx index 5939822306..efa5fdbbea 100644 --- a/web/src/app/alerts/components/AlertFeedback.tsx +++ b/web/src/app/alerts/components/AlertFeedback.tsx @@ -1,13 +1,6 @@ import React, { useState, useEffect, useMemo } from 'react' import { useQuery, useMutation, gql } from 'urql' -import { Grid, Grow, IconButton, Tooltip, TextField } from '@mui/material' -import { - ThumbUp, - ThumbDown, - ThumbUpOutlined, - ThumbDownOutlined, - Info, -} from '@mui/icons-material' +import { Button } from '@mui/material' import { DEBOUNCE_DELAY } from '../../config' import { transitionStyles } from '../../util/Transitions' @@ -16,7 +9,6 @@ const query = gql` alert(id: $id) { id feedback { - sentiment note } } @@ -35,7 +27,6 @@ interface AlertFeedbackProps { export default function AlertFeedback(props: AlertFeedbackProps): JSX.Element { const { alertID } = props - const classes = transitionStyles() const [cacheCount, setCacheCount] = useState(0) // reset cache on tick // stable query reference @@ -72,92 +63,5 @@ export default function AlertFeedback(props: AlertFeedbackProps): JSX.Element { setNote(data?.alert?.feedback?.note ?? '') }, [data?.alert?.feedback?.note]) - const thumbUp = - !fetching && !error && data?.alert?.feedback?.sentiment === 1 ? ( - - ) : ( - - ) - - const isUp = data?.alert?.feedback?.sentiment === 1 - const isDown = data?.alert?.feedback?.sentiment === -1 - const thumbDown = - !fetching && !error && isDown ? : - - return ( - - {mutationStatus.error?.message} - - { - setCacheCount(cacheCount + 1) - commit( - { - input: { - alertID, - sentiment: 1, - }, - }, - { additionalTypenames: ['Feedback'] }, - ) - }} - size='large' - > - {thumbUp} - - - - { - setCacheCount(cacheCount + 1) - commit( - { - input: { - alertID, - sentiment: -1, - }, - }, - { additionalTypenames: ['Feedback'] }, - ) - }} - size='large' - > - {thumbDown} - - - - - - - - - - setNote(e.target.value)} - /> - - - - ) + return } From c95283820a783b5aa577eb969c3d06b9a24820b3 Mon Sep 17 00:00:00 2001 From: Nathaniel Cook Date: Tue, 27 Jun 2023 11:04:04 -0700 Subject: [PATCH 11/35] use dialog to set alert feedback --- graphql2/graphqlapp/alert.go | 2 + .../app/alerts/components/AlertFeedback.tsx | 110 +++++++++++++----- web/src/schema.d.ts | 4 +- 3 files changed, 84 insertions(+), 32 deletions(-) diff --git a/graphql2/graphqlapp/alert.go b/graphql2/graphqlapp/alert.go index b6e20833e4..db8f107ea5 100644 --- a/graphql2/graphqlapp/alert.go +++ b/graphql2/graphqlapp/alert.go @@ -375,6 +375,8 @@ func (m *Mutation) UpdateAlertFeedback(ctx context.Context, input graphql2.Updat AlertID: input.AlertID, } + f.Note = input.Note + err := m.AlertStore.UpdateFeedback(ctx, f) if err != nil { return false, err diff --git a/web/src/app/alerts/components/AlertFeedback.tsx b/web/src/app/alerts/components/AlertFeedback.tsx index efa5fdbbea..e1f2db3a0e 100644 --- a/web/src/app/alerts/components/AlertFeedback.tsx +++ b/web/src/app/alerts/components/AlertFeedback.tsx @@ -1,8 +1,12 @@ -import React, { useState, useEffect, useMemo } from 'react' +import React, { useState, useEffect } from 'react' import { useQuery, useMutation, gql } from 'urql' -import { Button } from '@mui/material' -import { DEBOUNCE_DELAY } from '../../config' -import { transitionStyles } from '../../util/Transitions' +import Button from '@mui/material/Button' +import Radio from '@mui/material/Radio' +import RadioGroup from '@mui/material/RadioGroup' +import FormControlLabel from '@mui/material/FormControlLabel' +import TextField from '@mui/material/TextField' +import FormDialog from '../../dialogs/FormDialog' +import { nonFieldErrors } from '../../util/errutil' const query = gql` query AlertFeedbackQuery($id: Int!) { @@ -27,41 +31,89 @@ interface AlertFeedbackProps { export default function AlertFeedback(props: AlertFeedbackProps): JSX.Element { const { alertID } = props - const [cacheCount, setCacheCount] = useState(0) // reset cache on tick + const [showDialog, setShowDialog] = useState(false) - // stable query reference - const context = useMemo( - () => ({ additionalTypenames: ['Feedback'] }), - [cacheCount], - ) - const [{ data, fetching, error }] = useQuery({ + const [{ data }] = useQuery({ query, - context, variables: { id: alertID, }, - requestPolicy: 'cache-first', }) const [note, setNote] = useState(data?.alert?.feedback?.note ?? '') + const [other, setOther] = useState('') const [mutationStatus, commit] = useMutation(mutation) - // Debounce setting note - useEffect(() => { - const t = setTimeout(() => { - commit({ - input: { - alertID, - note, - }, - }) - }, DEBOUNCE_DELAY) - - return () => clearTimeout(t) - }, [note]) + const options = [ + 'False positive', + 'Resolved itself', + "Wasn't actionable", + 'Poor details', + ] + const dataNote = data?.alert?.feedback?.note ?? '' useEffect(() => { - setNote(data?.alert?.feedback?.note ?? '') - }, [data?.alert?.feedback?.note]) + if (options.includes(dataNote)) { + setNote(dataNote) + } else { + setNote('Other') + setOther(dataNote) + } + }, [dataNote]) - return + return ( + + + {showDialog && ( + setShowDialog(false)} + onSubmit={() => + commit({ + input: { + alertID, + note: note === 'Other' ? other : note, + }, + }).then((result) => { + if (!result.error) setShowDialog(false) + }) + } + form={ + setNote(e.target.value)} + > + {options.map((o) => ( + } + /> + ))} + setNote('Other')} + value={other} + placeholder='Other (please specify)' + onChange={(e) => setOther(e.target.value)} + /> + } + control={} + disableTypography + /> + + } + /> + )} + + ) } diff --git a/web/src/schema.d.ts b/web/src/schema.d.ts index 840ea13df7..a6e14212ee 100644 --- a/web/src/schema.d.ts +++ b/web/src/schema.d.ts @@ -444,8 +444,7 @@ export interface CreateAlertInput { export interface UpdateAlertFeedbackInput { alertID: number - sentiment?: null | number - note?: null | string + note: string } export interface CreateUserInput { @@ -888,7 +887,6 @@ export interface AlertState { } export interface AlertFeedback { - sentiment: number note?: null | string } From e63ed43d15e79d893089fbe5f033042f1f2f8968 Mon Sep 17 00:00:00 2001 From: Nathaniel Cook Date: Tue, 27 Jun 2023 11:27:42 -0700 Subject: [PATCH 12/35] add notice to show problem alert reason with undo --- alert/store.go | 4 +- .../app/alerts/components/AlertDetails.tsx | 47 ++++++++++++++++--- .../app/alerts/components/AlertFeedback.tsx | 18 +++---- web/src/app/alerts/pages/AlertDetailPage.tsx | 3 ++ web/src/app/services/ServiceNotices.tsx | 3 +- 5 files changed, 54 insertions(+), 21 deletions(-) diff --git a/alert/store.go b/alert/store.go index 50061c3757..5888990cdb 100644 --- a/alert/store.go +++ b/alert/store.go @@ -898,9 +898,7 @@ func (s Store) UpdateFeedback(ctx context.Context, feedback *Feedback) error { if err != nil { return err } - if feedback.Note != "" { - f.Note = feedback.Note - } + f.Note = feedback.Note _, err = s.updateFeedback.ExecContext(ctx, feedback.AlertID, f.Note) if err != nil { diff --git a/web/src/app/alerts/components/AlertDetails.tsx b/web/src/app/alerts/components/AlertDetails.tsx index 2024e56669..1bb7750314 100644 --- a/web/src/app/alerts/components/AlertDetails.tsx +++ b/web/src/app/alerts/components/AlertDetails.tsx @@ -43,7 +43,11 @@ import { } from '../../../schema' import ServiceNotices from '../../services/ServiceNotices' import { Time } from '../../util/Time' -import AlertFeedback from './AlertFeedback' +import AlertFeedback, { + mutation as undoFeedbackMutation, +} from './AlertFeedback' +import LoadingButton from '../../loading/components/LoadingButton' +import { Notice } from '../../details/Notices' interface AlertDetailsProps { data: Alert @@ -83,6 +87,15 @@ const updateStatusMutation = gql` export default function AlertDetails(props: AlertDetailsProps): JSX.Element { const classes = useStyles() + const [undoFeedback, undoFeedbackStatus] = useMutation(undoFeedbackMutation, { + variables: { + input: { + alertID: props.data.id, + note: '', + }, + }, + }) + const [ack] = useMutation(updateStatusMutation, { variables: { input: { @@ -351,16 +364,36 @@ export default function AlertDetails(props: AlertDetailsProps): JSX.Element { } const { data: alert } = props + + let extraNotices: Notice[] = alert.pendingNotifications.map((n) => ({ + type: 'WARNING', + message: `Notification Pending for ${n.destination}`, + details: + 'This could be due to rate-limiting, processing, or network delays.', + })) + + if (alert.feedback?.note !== '') { + extraNotices = [ + ...extraNotices, + { + type: 'WARNING', + message: 'This alert has been marked as problematic', + details: 'Reason: ' + alert.feedback?.note ?? '', + action: ( + undoFeedback()} + /> + ), + }, + ] + } return ( ({ - type: 'WARNING', - message: `Notification Pending for ${n.destination}`, - details: - 'This could be due to rate-limiting, processing, or network delays.', - }))} + extraNotices={extraNotices as Notice[]} /> {/* Main Alert Info */} diff --git a/web/src/app/alerts/components/AlertFeedback.tsx b/web/src/app/alerts/components/AlertFeedback.tsx index e1f2db3a0e..f61c7ee1d9 100644 --- a/web/src/app/alerts/components/AlertFeedback.tsx +++ b/web/src/app/alerts/components/AlertFeedback.tsx @@ -19,7 +19,7 @@ const query = gql` } ` -const mutation = gql` +export const mutation = gql` mutation UpdateFeedbackMutation($input: UpdateAlertFeedbackInput!) { updateAlertFeedback(input: $input) } @@ -33,23 +33,23 @@ export default function AlertFeedback(props: AlertFeedbackProps): JSX.Element { const { alertID } = props const [showDialog, setShowDialog] = useState(false) + const options = [ + 'False positive', + 'Resolved itself', + "Wasn't actionable", + 'Poor details', + ] + const [{ data }] = useQuery({ query, variables: { id: alertID, }, }) - const [note, setNote] = useState(data?.alert?.feedback?.note ?? '') + const [note, setNote] = useState(data?.alert?.feedback?.note ?? options[0]) const [other, setOther] = useState('') const [mutationStatus, commit] = useMutation(mutation) - const options = [ - 'False positive', - 'Resolved itself', - "Wasn't actionable", - 'Poor details', - ] - const dataNote = data?.alert?.feedback?.note ?? '' useEffect(() => { if (options.includes(dataNote)) { diff --git a/web/src/app/alerts/pages/AlertDetailPage.tsx b/web/src/app/alerts/pages/AlertDetailPage.tsx index cfc393310e..aa4f50839f 100644 --- a/web/src/app/alerts/pages/AlertDetailPage.tsx +++ b/web/src/app/alerts/pages/AlertDetailPage.tsx @@ -38,6 +38,9 @@ const query = gql` pendingNotifications { destination } + feedback { + note + } } } ` diff --git a/web/src/app/services/ServiceNotices.tsx b/web/src/app/services/ServiceNotices.tsx index e324e3ac2d..788116262c 100644 --- a/web/src/app/services/ServiceNotices.tsx +++ b/web/src/app/services/ServiceNotices.tsx @@ -2,9 +2,8 @@ import React from 'react' import { gql, useQuery, useMutation } from 'urql' import { Button, Grid } from '@mui/material' import { DateTime } from 'luxon' -import Notices from '../details/Notices' +import Notices, { Notice } from '../details/Notices' import { Time } from '../util/Time' -import { Notice } from '../../schema' const query = gql` query serviceMaintenanceQuery($serviceID: ID!) { From 07062191ecd4f713279fa1a3d3a00c5f6f6201d6 Mon Sep 17 00:00:00 2001 From: Nathaniel Cook Date: Tue, 27 Jun 2023 11:35:20 -0700 Subject: [PATCH 13/35] styling updates --- web/src/app/alerts/components/AlertDetails.tsx | 1 + web/src/app/alerts/components/AlertFeedback.tsx | 2 +- web/src/app/loading/components/LoadingButton.tsx | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/web/src/app/alerts/components/AlertDetails.tsx b/web/src/app/alerts/components/AlertDetails.tsx index 1bb7750314..c0a2a61748 100644 --- a/web/src/app/alerts/components/AlertDetails.tsx +++ b/web/src/app/alerts/components/AlertDetails.tsx @@ -382,6 +382,7 @@ export default function AlertDetails(props: AlertDetailsProps): JSX.Element { action: ( undoFeedback()} /> diff --git a/web/src/app/alerts/components/AlertFeedback.tsx b/web/src/app/alerts/components/AlertFeedback.tsx index f61c7ee1d9..98ca924005 100644 --- a/web/src/app/alerts/components/AlertFeedback.tsx +++ b/web/src/app/alerts/components/AlertFeedback.tsx @@ -67,7 +67,7 @@ export default function AlertFeedback(props: AlertFeedbackProps): JSX.Element { {showDialog && ( setShowDialog(false)} diff --git a/web/src/app/loading/components/LoadingButton.tsx b/web/src/app/loading/components/LoadingButton.tsx index 54056f9dba..7cdf804613 100644 --- a/web/src/app/loading/components/LoadingButton.tsx +++ b/web/src/app/loading/components/LoadingButton.tsx @@ -26,9 +26,9 @@ const LoadingButton = (props: LoadingButtonProps): JSX.Element => { return (
- {showDialog && ( - setShowDialog(false)} - onSubmit={handleSubmit} - form={ - - {options.map((option) => ( - handleCheck(e, option)} - /> - } - /> - ))} - + + + + + {options.map((option) => ( setOtherChecked(true)} - onChange={(e) => { - setOther(e.target.value) - }} - /> - } + key={option} + label={option} control={ { - setOtherChecked(e.target.checked) - if (!e) { - setOther('') - } - }} + checked={notes.includes(option)} + onChange={(e) => handleCheck(e, option)} /> } - disableTypography /> - - } - /> - )} + ))} + + setOtherChecked(true)} + onChange={(e) => { + setOther(e.target.value) + }} + /> + } + control={ + { + setOtherChecked(e.target.checked) + if (!e.target.checked) { + setOther('') + } + }} + /> + } + disableTypography + /> + + {error?.message && ( + + {error?.message} + + )} + + ) } From 129761bea147692ce741833fef29767f0dea1493 Mon Sep 17 00:00:00 2001 From: Nathaniel Cook Date: Wed, 28 Jun 2023 10:41:51 -0700 Subject: [PATCH 18/35] use submit, hide card when banner showing --- .../app/alerts/components/AlertDetails.tsx | 11 +++++--- .../app/alerts/components/AlertFeedback.tsx | 25 +++++++++---------- 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/web/src/app/alerts/components/AlertDetails.tsx b/web/src/app/alerts/components/AlertDetails.tsx index 4e6c4dce29..a6ca71d0e2 100644 --- a/web/src/app/alerts/components/AlertDetails.tsx +++ b/web/src/app/alerts/components/AlertDetails.tsx @@ -394,6 +394,7 @@ export default function AlertDetails(props: AlertDetailsProps): JSX.Element { }, ] } + return ( - - - + {!note && ( + + + + )} {renderAlertDetails()} {/* Escalation Policy Info */} diff --git a/web/src/app/alerts/components/AlertFeedback.tsx b/web/src/app/alerts/components/AlertFeedback.tsx index e29a0e065a..4a193ee21f 100644 --- a/web/src/app/alerts/components/AlertFeedback.tsx +++ b/web/src/app/alerts/components/AlertFeedback.tsx @@ -10,6 +10,8 @@ import { nonFieldErrors } from '../../util/errutil' import { Card, CardContent, CardHeader, Typography } from '@mui/material' import ErrorBoundary from '../../main/ErrorBoundary' import { DEBOUNCE_DELAY } from '../../config' +import { AlertStatus } from '../../../schema' +import CardActions from '../../details/CardActions' const query = gql` query AlertFeedbackQuery($id: Int!) { @@ -34,7 +36,6 @@ interface AlertFeedbackProps { export default function AlertFeedback(props: AlertFeedbackProps): JSX.Element { const { alertID } = props - const [showDialog, setShowDialog] = useState(false) const [{ data }] = useQuery({ query, @@ -45,8 +46,8 @@ export default function AlertFeedback(props: AlertFeedbackProps): JSX.Element { const options = [ 'False positive', - 'Self resolving', - 'Not actionable', + "Wasn't Actionable", + 'Resolved itself', 'Poor details', ] @@ -72,7 +73,7 @@ export default function AlertFeedback(props: AlertFeedbackProps): JSX.Element { const [other, setOther] = useState(defaults[1]) const [otherChecked, setOtherChecked] = useState(Boolean(defaults[1])) const [mutationStatus, commit] = useMutation(mutation) - const { fetching, error } = mutationStatus + const { error } = mutationStatus useEffect(() => { const v = getDefaults() @@ -92,15 +93,6 @@ export default function AlertFeedback(props: AlertFeedbackProps): JSX.Element { }) } - // Debounce submitting notes on changes - useEffect(() => { - const t = setTimeout(() => { - handleSubmit() - }, DEBOUNCE_DELAY) - - return () => clearTimeout(t) - }, [other, notes]) - function handleCheck( e: React.ChangeEvent, note: string, @@ -165,6 +157,13 @@ export default function AlertFeedback(props: AlertFeedbackProps): JSX.Element { )} + + Submit + , + ]} + /> ) From 4c2f89697f66c115676834fd0896251465b9ecd1 Mon Sep 17 00:00:00 2001 From: Nathaniel Cook Date: Wed, 28 Jun 2023 10:42:14 -0700 Subject: [PATCH 19/35] remove unused imports --- web/src/app/alerts/components/AlertFeedback.tsx | 5 ----- 1 file changed, 5 deletions(-) diff --git a/web/src/app/alerts/components/AlertFeedback.tsx b/web/src/app/alerts/components/AlertFeedback.tsx index 4a193ee21f..c7233594fd 100644 --- a/web/src/app/alerts/components/AlertFeedback.tsx +++ b/web/src/app/alerts/components/AlertFeedback.tsx @@ -5,12 +5,7 @@ import Checkbox from '@mui/material/Checkbox' import FormGroup from '@mui/material/FormGroup' import FormControlLabel from '@mui/material/FormControlLabel' import TextField from '@mui/material/TextField' -import FormDialog from '../../dialogs/FormDialog' -import { nonFieldErrors } from '../../util/errutil' import { Card, CardContent, CardHeader, Typography } from '@mui/material' -import ErrorBoundary from '../../main/ErrorBoundary' -import { DEBOUNCE_DELAY } from '../../config' -import { AlertStatus } from '../../../schema' import CardActions from '../../details/CardActions' const query = gql` From a23f11c011f7527059c1ebbad45cf0566358e85d Mon Sep 17 00:00:00 2001 From: Nathaniel Cook Date: Wed, 28 Jun 2023 10:47:25 -0700 Subject: [PATCH 20/35] Grammar/wording --- web/src/app/alerts/components/AlertDetails.tsx | 5 +++-- web/src/app/alerts/components/AlertFeedback.tsx | 7 +------ 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/web/src/app/alerts/components/AlertDetails.tsx b/web/src/app/alerts/components/AlertDetails.tsx index a6ca71d0e2..6db8126ee2 100644 --- a/web/src/app/alerts/components/AlertDetails.tsx +++ b/web/src/app/alerts/components/AlertDetails.tsx @@ -376,13 +376,14 @@ export default function AlertDetails(props: AlertDetailsProps): JSX.Element { const note = alert?.feedback?.note ?? '' if (note !== '') { - const reasons = note.split('|').join(', ') + const notesArr = note.split('|') + const reasons = notesArr.join(', ') extraNotices = [ ...extraNotices, { type: 'INFO', message: 'This alert has been marked as noise', - details: `Reason${reasons.length > 1 ? 's' : ''}: ${reasons}`, + details: `Reason${notesArr.length > 1 ? 's' : ''}: ${reasons}`, action: ( Date: Wed, 28 Jun 2023 11:02:24 -0700 Subject: [PATCH 21/35] add transition and disable submit when empty --- .../app/alerts/components/AlertDetails.tsx | 5 +- .../app/alerts/components/AlertFeedback.tsx | 113 +++++++++--------- 2 files changed, 61 insertions(+), 57 deletions(-) diff --git a/web/src/app/alerts/components/AlertDetails.tsx b/web/src/app/alerts/components/AlertDetails.tsx index 6db8126ee2..aa34594804 100644 --- a/web/src/app/alerts/components/AlertDetails.tsx +++ b/web/src/app/alerts/components/AlertDetails.tsx @@ -49,6 +49,7 @@ import AlertFeedback, { import LoadingButton from '../../loading/components/LoadingButton' import { Notice } from '../../details/Notices' import { useIsWidthDown } from '../../util/useWidth' +import { Fade } from '@mui/material' interface AlertDetailsProps { data: Alert @@ -443,11 +444,11 @@ export default function AlertDetails(props: AlertDetailsProps): JSX.Element { - {!note && ( + - )} + {renderAlertDetails()} {/* Escalation Policy Info */} diff --git a/web/src/app/alerts/components/AlertFeedback.tsx b/web/src/app/alerts/components/AlertFeedback.tsx index 0976a84038..123ffcda41 100644 --- a/web/src/app/alerts/components/AlertFeedback.tsx +++ b/web/src/app/alerts/components/AlertFeedback.tsx @@ -95,66 +95,69 @@ export default function AlertFeedback(props: AlertFeedbackProps): JSX.Element { } return ( - - - - - - {options.map((option) => ( - handleCheck(e, option)} - /> - } - /> - ))} - + + + + + {options.map((option) => ( setOtherChecked(true)} - onChange={(e) => { - setOther(e.target.value) - }} - /> - } + key={option} + label={option} control={ { - setOtherChecked(e.target.checked) - if (!e.target.checked) { - setOther('') - } - }} + checked={notes.includes(option)} + onChange={(e) => handleCheck(e, option)} /> } - disableTypography /> - - {error?.message && ( - - {error?.message} - - )} - - - Submit - , - ]} - /> - - + ))} + + setOtherChecked(true)} + onChange={(e) => { + setOther(e.target.value) + }} + /> + } + control={ + { + setOtherChecked(e.target.checked) + if (!e.target.checked) { + setOther('') + } + }} + /> + } + disableTypography + /> + + {error?.message && ( + + {error?.message} + + )} + + + Submit + , + ]} + /> + ) } From c611bdd9c70a2531ad092c4ef6e9fc3081a63c55 Mon Sep 17 00:00:00 2001 From: Nathaniel Cook Date: Wed, 28 Jun 2023 12:02:06 -0700 Subject: [PATCH 22/35] add testing --- .../app/alerts/components/AlertDetails.tsx | 1 + .../app/alerts/components/AlertFeedback.tsx | 2 + web/src/cypress/e2e/alerts.cy.ts | 52 +++++++++++++------ 3 files changed, 38 insertions(+), 17 deletions(-) diff --git a/web/src/app/alerts/components/AlertDetails.tsx b/web/src/app/alerts/components/AlertDetails.tsx index aa34594804..104e68da0e 100644 --- a/web/src/app/alerts/components/AlertDetails.tsx +++ b/web/src/app/alerts/components/AlertDetails.tsx @@ -388,6 +388,7 @@ export default function AlertDetails(props: AlertDetailsProps): JSX.Element { action: ( undoFeedback()} diff --git a/web/src/app/alerts/components/AlertFeedback.tsx b/web/src/app/alerts/components/AlertFeedback.tsx index 123ffcda41..73e771031f 100644 --- a/web/src/app/alerts/components/AlertFeedback.tsx +++ b/web/src/app/alerts/components/AlertFeedback.tsx @@ -105,6 +105,7 @@ export default function AlertFeedback(props: AlertFeedbackProps): JSX.Element { label={option} control={ handleCheck(e, option)} /> @@ -149,6 +150,7 @@ export default function AlertFeedback(props: AlertFeedbackProps): JSX.Element { { - // upvote alert - cy.get('button[aria-label="Mark alert as useful"]').click() - cy.get('button[aria-label="Mark alert as useful"]').should('not.exist') - cy.get('button[aria-label="Alert marked as useful"]').should('be.visible') - cy.get('[aria-label="Alert Note"]').should('not.exist') - - // downvote alert - cy.get('button[aria-label="Mark alert as not useful"]').click() - cy.get('button[aria-label="Mark alert as not useful"]').should( - 'not.exist', + it.only('should set alert notes', () => { + // set all notes, checking carefully because of async setState + cy.get('body').should('contain.text', 'Is this alert noise?') + cy.get('[data-cy="False positive"] input[type="checkbox"]').check() + cy.get('[data-cy="False positive"] input[type="checkbox"]').should( + 'be.checked', ) - cy.get('button[aria-label="Alert marked as not useful"]').should( - 'be.visible', + cy.get('[data-cy="Not actionable"] input[type="checkbox"]').check() + cy.get('[data-cy="Not actionable"] input[type="checkbox"]').should( + 'be.checked', ) + cy.get('[data-cy="Poor details"] input[type="checkbox"]').check() + cy.get('[data-cy="Poor details"] input[type="checkbox"]').should( + 'be.checked', + ) + cy.get('[placeholder="Other (please specify)"]').type('Test') - // set alert note - const value = 'Test' - cy.get('[aria-label="Alert Note"]').type(value) - cy.get('[aria-label="Alert Note"] input').should('have.value', value) + // submit + cy.get('button[aria-label="Submit alert notes"]').should( + 'not.be.disabled', + ) + cy.get('button[aria-label="Submit alert notes"]').click() + cy.get('label').contains('False positive').should('not.exist') + + // see notice + const noticeTitle = 'Info: This alert has been marked as noise' + cy.get('body').should('contain.text', noticeTitle) + cy.get('body').should( + 'contain.text', + 'Reasons: False positive, Not actionable, Poor details, Test', + ) + + // undo + cy.get('button[aria-label="Reset alert notes"]').click() + cy.get('body').should('not.contain.text', noticeTitle) + cy.get('[data-cy="False positive"] input[type="checkbox"]').should( + 'not.be.checked', + ) }) }) From 50ec5cab0d1117825b057fcbdc7a69ecbe40fe9d Mon Sep 17 00:00:00 2001 From: Nathaniel Cook Date: Wed, 28 Jun 2023 12:07:16 -0700 Subject: [PATCH 23/35] remove .only from test --- web/src/cypress/e2e/alerts.cy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/cypress/e2e/alerts.cy.ts b/web/src/cypress/e2e/alerts.cy.ts index c92863cfb8..7555121372 100644 --- a/web/src/cypress/e2e/alerts.cy.ts +++ b/web/src/cypress/e2e/alerts.cy.ts @@ -394,7 +394,7 @@ function testAlerts(screen: ScreenFormat): void { cy.get('body').should('contain', 'CLOSED') }) - it.only('should set alert notes', () => { + it('should set alert notes', () => { // set all notes, checking carefully because of async setState cy.get('body').should('contain.text', 'Is this alert noise?') cy.get('[data-cy="False positive"] input[type="checkbox"]').check() From 8bcd087304fe34c4ac74e1d8703111700dad8955 Mon Sep 17 00:00:00 2001 From: Nathaniel Cook Date: Wed, 28 Jun 2023 12:09:39 -0700 Subject: [PATCH 24/35] revert exporting transitions --- web/src/app/util/Search.js | 28 ++++++++++++++++++++++++++-- web/src/app/util/Transitions.tsx | 26 -------------------------- 2 files changed, 26 insertions(+), 28 deletions(-) diff --git a/web/src/app/util/Search.js b/web/src/app/util/Search.js index 474f3a5ca8..68853b5638 100644 --- a/web/src/app/util/Search.js +++ b/web/src/app/util/Search.js @@ -1,5 +1,6 @@ import React, { useState, useEffect, useRef } from 'react' import p from 'prop-types' +import makeStyles from '@mui/styles/makeStyles' import AppBar from '@mui/material/AppBar' import Hidden from '@mui/material/Hidden' import IconButton from '@mui/material/IconButton' @@ -11,7 +12,30 @@ import { Close as CloseIcon, Search as SearchIcon } from '@mui/icons-material' import { DEBOUNCE_DELAY } from '../config' import AppBarSearchContainer from './AppBarSearchContainer' import { useURLParam } from '../actions' -import { transitionStyles } from './Transitions' + +const useStyles = makeStyles((theme) => { + return { + transition: { + [theme.breakpoints.down('md')]: { + flex: 1, + }, + [theme.breakpoints.up('md')]: { + '& input:focus': { + minWidth: 275, + }, + '& input:not(:placeholder-shown)': { + minWidth: 275, + }, + '& input': { + minWidth: 180, + transitionProperty: 'min-width', + transitionDuration: theme.transitions.duration.standard, + transitionTimingFunction: theme.transitions.easing.easeInOut, + }, + }, + }, + } +}) /* * Renders a search text field that utilizes the URL params to regulate @@ -28,7 +52,7 @@ export default function Search(props) { // or from a local event so we don't lose typed characters. const [prevParamValue, setPrevParamValue] = useState(searchParam) - const classes = transitionStyles() + const classes = useStyles() const [search, setSearch] = useState(searchParam) const [showMobile, setShowMobile] = useState(Boolean(search)) const fieldRef = useRef() diff --git a/web/src/app/util/Transitions.tsx b/web/src/app/util/Transitions.tsx index 6381aff810..e3a6cd99bb 100644 --- a/web/src/app/util/Transitions.tsx +++ b/web/src/app/util/Transitions.tsx @@ -1,34 +1,8 @@ import React, { ReactElement } from 'react' -import { Theme } from '@mui/material' import Fade from '@mui/material/Fade' import Slide from '@mui/material/Slide' -import makeStyles from '@mui/styles/makeStyles' import { TransitionProps } from '@mui/material/transitions' -export const transitionStyles = makeStyles((theme: Theme) => { - return { - transition: { - [theme.breakpoints.down('md')]: { - flex: 1, - }, - [theme.breakpoints.up('md')]: { - '& input:focus': { - minWidth: 275, - }, - '& input:not(:placeholder-shown)': { - minWidth: 275, - }, - '& input': { - minWidth: 180, - transitionProperty: 'min-width', - transitionDuration: theme.transitions.duration.standard, - transitionTimingFunction: theme.transitions.easing.easeInOut, - }, - }, - }, - } -}) - export const FadeTransition = React.forwardRef( ({ children, ...props }: TransitionProps, ref) => ( From e41ee2728f530eae68f7eebb3ed40f7bb1a5e757 Mon Sep 17 00:00:00 2001 From: Nathaniel Cook Date: Wed, 28 Jun 2023 13:26:02 -0700 Subject: [PATCH 25/35] fix key --- web/src/app/alerts/components/AlertDetails.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/web/src/app/alerts/components/AlertDetails.tsx b/web/src/app/alerts/components/AlertDetails.tsx index 104e68da0e..a95fdeea8f 100644 --- a/web/src/app/alerts/components/AlertDetails.tsx +++ b/web/src/app/alerts/components/AlertDetails.tsx @@ -336,12 +336,12 @@ export default function AlertDetails(props: AlertDetailsProps): JSX.Element { /* * Options to show for alert details menu */ - function getMenuOptions(): JSX.Element { + function getMenuOptions(): Array { const { status } = props.data - if (status === 'StatusClosed') return + if (status === 'StatusClosed') return [] const isMaintMode = Boolean(props.data?.service?.maintenanceExpiresAt) - return ( + return [ } onClick={() => close()}> Close - - ) + , + ] } const { data: alert } = props @@ -442,7 +442,7 @@ export default function AlertDetails(props: AlertDetailsProps): JSX.Element { - + From 6876d46faf43f223deda8ccf9650a5d1d533774c Mon Sep 17 00:00:00 2001 From: Nathaniel Cook Date: Wed, 28 Jun 2023 13:40:56 -0700 Subject: [PATCH 26/35] remove fade --- web/src/app/alerts/components/AlertDetails.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/web/src/app/alerts/components/AlertDetails.tsx b/web/src/app/alerts/components/AlertDetails.tsx index a95fdeea8f..6c00646457 100644 --- a/web/src/app/alerts/components/AlertDetails.tsx +++ b/web/src/app/alerts/components/AlertDetails.tsx @@ -408,7 +408,6 @@ export default function AlertDetails(props: AlertDetailsProps): JSX.Element { {/* Main Alert Info */} @@ -445,11 +444,11 @@ export default function AlertDetails(props: AlertDetailsProps): JSX.Element { - + {!note && ( - + )} {renderAlertDetails()} {/* Escalation Policy Info */} From ed7a16ec223a6fdbd6f7eb6c64e6a71c435b8a01 Mon Sep 17 00:00:00 2001 From: Nathaniel Cook Date: Thu, 29 Jun 2023 07:15:51 -0700 Subject: [PATCH 27/35] remove unused import --- web/src/app/alerts/components/AlertDetails.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/web/src/app/alerts/components/AlertDetails.tsx b/web/src/app/alerts/components/AlertDetails.tsx index 6c00646457..b50a14d3b5 100644 --- a/web/src/app/alerts/components/AlertDetails.tsx +++ b/web/src/app/alerts/components/AlertDetails.tsx @@ -49,7 +49,6 @@ import AlertFeedback, { import LoadingButton from '../../loading/components/LoadingButton' import { Notice } from '../../details/Notices' import { useIsWidthDown } from '../../util/useWidth' -import { Fade } from '@mui/material' interface AlertDetailsProps { data: Alert From b2e83901520cde90416a3bd7776bf80fb7b3c2a7 Mon Sep 17 00:00:00 2001 From: Nathaniel Cook Date: Thu, 29 Jun 2023 12:38:36 -0700 Subject: [PATCH 28/35] note -> noiseReason --- alert/feedback.go | 4 +- alert/store.go | 16 +- graphql2/generated.go | 315 ++++++------------ graphql2/graphqlapp/alert.go | 10 +- graphql2/models_gen.go | 14 +- graphql2/schema.graphql | 12 +- .../20230616110941-add-alerts-feedback.sql | 2 +- .../app/alerts/components/AlertDetails.tsx | 18 +- .../app/alerts/components/AlertFeedback.tsx | 34 +- web/src/app/alerts/pages/AlertDetailPage.tsx | 4 +- web/src/cypress/e2e/alerts.cy.ts | 6 +- web/src/schema.d.ts | 12 +- 12 files changed, 165 insertions(+), 282 deletions(-) diff --git a/alert/feedback.go b/alert/feedback.go index b2a0daf48a..b1aac279c9 100644 --- a/alert/feedback.go +++ b/alert/feedback.go @@ -2,6 +2,6 @@ package alert // Feedback represents user provided information about a given alert type Feedback struct { - AlertID int - Note string + AlertID int + NoiseReason string } diff --git a/alert/store.go b/alert/store.go index 21cb7f25c5..9778b64d46 100644 --- a/alert/store.go +++ b/alert/store.go @@ -208,16 +208,16 @@ func NewStore(ctx context.Context, db *sql.DB, logDB *alertlog.Store) (*Store, e feedback: p(` SELECT - alert_id, note + alert_id, noise_reason FROM alert_feedback WHERE alert_id = $1 `), updateFeedback: p(` - INSERT INTO alert_feedback (alert_id, note) + INSERT INTO alert_feedback (alert_id, noise_reason) VALUES ($1, $2) ON CONFLICT (alert_id) DO UPDATE - SET note = $2 + SET noise_reason = $2 WHERE alert_feedback.alert_id = $1 `), }, prep.Err @@ -812,11 +812,11 @@ func (s *Store) State(ctx context.Context, alertIDs []int) ([]State, error) { func (s *Store) Feedback(ctx context.Context, alertID int) (f Feedback, err error) { row := s.feedback.QueryRowContext(ctx, alertID) - err = row.Scan(&f.AlertID, &f.Note) + err = row.Scan(&f.AlertID, &f.NoiseReason) if errors.Is(err, sql.ErrNoRows) { return Feedback{ - AlertID: alertID, - Note: "", + AlertID: alertID, + NoiseReason: "", }, nil } return f, err @@ -832,9 +832,9 @@ func (s Store) UpdateFeedback(ctx context.Context, feedback *Feedback) error { if err != nil { return err } - f.Note = feedback.Note + f.NoiseReason = feedback.NoiseReason - _, err = s.updateFeedback.ExecContext(ctx, feedback.AlertID, f.Note) + _, err = s.updateFeedback.ExecContext(ctx, feedback.AlertID, f.NoiseReason) if err != nil { return err } diff --git a/graphql2/generated.go b/graphql2/generated.go index 3535d404b5..215d983e30 100644 --- a/graphql2/generated.go +++ b/graphql2/generated.go @@ -96,9 +96,9 @@ type ComplexityRoot struct { AlertID func(childComplexity int) int CreatedAt func(childComplexity int) int Details func(childComplexity int) int - Feedback func(childComplexity int) int ID func(childComplexity int) int Metrics func(childComplexity int) int + NoiseReason func(childComplexity int) int PendingNotifications func(childComplexity int) int RecentEvents func(childComplexity int, input *AlertRecentEventsOptions) int Service func(childComplexity int) int @@ -118,10 +118,6 @@ type ComplexityRoot struct { Timestamp func(childComplexity int) int } - AlertFeedback struct { - Note func(childComplexity int) int - } - AlertLogEntry struct { ID func(childComplexity int) int Message func(childComplexity int) int @@ -309,6 +305,7 @@ type ComplexityRoot struct { EscalateAlerts func(childComplexity int, input []int) int LinkAccount func(childComplexity int, token string) int SendContactMethodVerification func(childComplexity int, input SendContactMethodVerificationInput) int + SetAlertNoiseReason func(childComplexity int, input SetAlertNoiseReasonInput) int SetConfig func(childComplexity int, input []ConfigValueInput) int SetFavorite func(childComplexity int, input SetFavoriteInput) int SetLabel func(childComplexity int, input SetLabelInput) int @@ -317,7 +314,6 @@ type ComplexityRoot struct { SetTemporarySchedule func(childComplexity int, input SetTemporaryScheduleInput) int SwoAction func(childComplexity int, action SWOAction) int TestContactMethod func(childComplexity int, id string) int - UpdateAlertFeedback func(childComplexity int, input UpdateAlertFeedbackInput) int UpdateAlerts func(childComplexity int, input UpdateAlertsInput) int UpdateAlertsByService func(childComplexity int, input UpdateAlertsByServiceInput) int UpdateBasicAuth func(childComplexity int, input UpdateBasicAuthInput) int @@ -672,7 +668,7 @@ type AlertResolver interface { RecentEvents(ctx context.Context, obj *alert.Alert, input *AlertRecentEventsOptions) (*AlertLogEntryConnection, error) PendingNotifications(ctx context.Context, obj *alert.Alert) ([]AlertPendingNotification, error) Metrics(ctx context.Context, obj *alert.Alert) (*alertmetrics.Metric, error) - Feedback(ctx context.Context, obj *alert.Alert) (*AlertFeedback, error) + NoiseReason(ctx context.Context, obj *alert.Alert) (*string, error) } type AlertLogEntryResolver interface { Message(ctx context.Context, obj *alertlog.Entry) (string, error) @@ -727,7 +723,7 @@ type MutationResolver interface { UpdateEscalationPolicyStep(ctx context.Context, input UpdateEscalationPolicyStepInput) (bool, error) DeleteAll(ctx context.Context, input []assignment.RawTarget) (bool, error) CreateAlert(ctx context.Context, input CreateAlertInput) (*alert.Alert, error) - UpdateAlertFeedback(ctx context.Context, input UpdateAlertFeedbackInput) (bool, error) + SetAlertNoiseReason(ctx context.Context, input SetAlertNoiseReasonInput) (bool, error) CreateService(ctx context.Context, input CreateServiceInput) (*service.Service, error) CreateEscalationPolicy(ctx context.Context, input CreateEscalationPolicyInput) (*escalation.Policy, error) CreateEscalationPolicyStep(ctx context.Context, input CreateEscalationPolicyStepInput) (*escalation.Step, error) @@ -917,13 +913,6 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Alert.Details(childComplexity), true - case "Alert.feedback": - if e.complexity.Alert.Feedback == nil { - break - } - - return e.complexity.Alert.Feedback(childComplexity), true - case "Alert.id": if e.complexity.Alert.ID == nil { break @@ -938,6 +927,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Alert.Metrics(childComplexity), true + case "Alert.noiseReason": + if e.complexity.Alert.NoiseReason == nil { + break + } + + return e.complexity.Alert.NoiseReason(childComplexity), true + case "Alert.pendingNotifications": if e.complexity.Alert.PendingNotifications == nil { break @@ -1020,13 +1016,6 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.AlertDataPoint.Timestamp(childComplexity), true - case "AlertFeedback.note": - if e.complexity.AlertFeedback.Note == nil { - break - } - - return e.complexity.AlertFeedback.Note(childComplexity), true - case "AlertLogEntry.id": if e.complexity.AlertLogEntry.ID == nil { break @@ -1945,6 +1934,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Mutation.SendContactMethodVerification(childComplexity, args["input"].(SendContactMethodVerificationInput)), true + case "Mutation.setAlertNoiseReason": + if e.complexity.Mutation.SetAlertNoiseReason == nil { + break + } + + args, err := ec.field_Mutation_setAlertNoiseReason_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Mutation.SetAlertNoiseReason(childComplexity, args["input"].(SetAlertNoiseReasonInput)), true + case "Mutation.setConfig": if e.complexity.Mutation.SetConfig == nil { break @@ -2041,18 +2042,6 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Mutation.TestContactMethod(childComplexity, args["id"].(string)), true - case "Mutation.updateAlertFeedback": - if e.complexity.Mutation.UpdateAlertFeedback == nil { - break - } - - args, err := ec.field_Mutation_updateAlertFeedback_args(context.TODO(), rawArgs) - if err != nil { - return 0, false - } - - return e.complexity.Mutation.UpdateAlertFeedback(childComplexity, args["input"].(UpdateAlertFeedbackInput)), true - case "Mutation.updateAlerts": if e.complexity.Mutation.UpdateAlerts == nil { break @@ -3953,6 +3942,7 @@ func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler { ec.unmarshalInputScheduleTargetInput, ec.unmarshalInputSendContactMethodVerificationInput, ec.unmarshalInputServiceSearchOptions, + ec.unmarshalInputSetAlertNoiseReasonInput, ec.unmarshalInputSetFavoriteInput, ec.unmarshalInputSetLabelInput, ec.unmarshalInputSetScheduleOnCallNotificationRulesInput, @@ -3964,7 +3954,6 @@ func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler { ec.unmarshalInputTargetInput, ec.unmarshalInputTimeSeriesOptions, ec.unmarshalInputTimeZoneSearchOptions, - ec.unmarshalInputUpdateAlertFeedbackInput, ec.unmarshalInputUpdateAlertsByServiceInput, ec.unmarshalInputUpdateAlertsInput, ec.unmarshalInputUpdateBasicAuthInput, @@ -4472,6 +4461,21 @@ func (ec *executionContext) field_Mutation_sendContactMethodVerification_args(ct return args, nil } +func (ec *executionContext) field_Mutation_setAlertNoiseReason_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { + var err error + args := map[string]interface{}{} + var arg0 SetAlertNoiseReasonInput + if tmp, ok := rawArgs["input"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("input")) + arg0, err = ec.unmarshalNSetAlertNoiseReasonInput2githubᚗcomᚋtargetᚋgoalertᚋgraphql2ᚐSetAlertNoiseReasonInput(ctx, tmp) + if err != nil { + return nil, err + } + } + args["input"] = arg0 + return args, nil +} + func (ec *executionContext) field_Mutation_setConfig_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} @@ -4592,21 +4596,6 @@ func (ec *executionContext) field_Mutation_testContactMethod_args(ctx context.Co return args, nil } -func (ec *executionContext) field_Mutation_updateAlertFeedback_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { - var err error - args := map[string]interface{}{} - var arg0 UpdateAlertFeedbackInput - if tmp, ok := rawArgs["input"]; ok { - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("input")) - arg0, err = ec.unmarshalNUpdateAlertFeedbackInput2githubᚗcomᚋtargetᚋgoalertᚋgraphql2ᚐUpdateAlertFeedbackInput(ctx, tmp) - if err != nil { - return nil, err - } - } - args["input"] = arg0 - return args, nil -} - func (ec *executionContext) field_Mutation_updateAlertsByService_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} @@ -6093,8 +6082,8 @@ func (ec *executionContext) fieldContext_Alert_metrics(ctx context.Context, fiel return fc, nil } -func (ec *executionContext) _Alert_feedback(ctx context.Context, field graphql.CollectedField, obj *alert.Alert) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_Alert_feedback(ctx, field) +func (ec *executionContext) _Alert_noiseReason(ctx context.Context, field graphql.CollectedField, obj *alert.Alert) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Alert_noiseReason(ctx, field) if err != nil { return graphql.Null } @@ -6107,7 +6096,7 @@ func (ec *executionContext) _Alert_feedback(ctx context.Context, field graphql.C }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return ec.resolvers.Alert().Feedback(rctx, obj) + return ec.resolvers.Alert().NoiseReason(rctx, obj) }) if err != nil { ec.Error(ctx, err) @@ -6116,23 +6105,19 @@ func (ec *executionContext) _Alert_feedback(ctx context.Context, field graphql.C if resTmp == nil { return graphql.Null } - res := resTmp.(*AlertFeedback) + res := resTmp.(*string) fc.Result = res - return ec.marshalOAlertFeedback2ᚖgithub.comᚋtargetᚋgoalertᚋgraphql2ᚐAlertFeedback(ctx, field.Selections, res) + return ec.marshalOString2ᚖstring(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext_Alert_feedback(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_Alert_noiseReason(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "Alert", Field: field, IsMethod: true, IsResolver: true, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - switch field.Name { - case "note": - return ec.fieldContext_AlertFeedback_note(ctx, field) - } - return nil, fmt.Errorf("no field named %q was found under type AlertFeedback", field.Name) + return nil, errors.New("field of type String does not have child fields") }, } return fc, nil @@ -6201,8 +6186,8 @@ func (ec *executionContext) fieldContext_AlertConnection_nodes(ctx context.Conte return ec.fieldContext_Alert_pendingNotifications(ctx, field) case "metrics": return ec.fieldContext_Alert_metrics(ctx, field) - case "feedback": - return ec.fieldContext_Alert_feedback(ctx, field) + case "noiseReason": + return ec.fieldContext_Alert_noiseReason(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type Alert", field.Name) }, @@ -6348,47 +6333,6 @@ func (ec *executionContext) fieldContext_AlertDataPoint_alertCount(ctx context.C return fc, nil } -func (ec *executionContext) _AlertFeedback_note(ctx context.Context, field graphql.CollectedField, obj *AlertFeedback) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_AlertFeedback_note(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { - ctx = rctx // use context from middleware stack in children - return obj.Note, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - return graphql.Null - } - res := resTmp.(*string) - fc.Result = res - return ec.marshalOString2ᚖstring(ctx, field.Selections, res) -} - -func (ec *executionContext) fieldContext_AlertFeedback_note(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "AlertFeedback", - Field: field, - IsMethod: false, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type String does not have child fields") - }, - } - return fc, nil -} - func (ec *executionContext) _AlertLogEntry_id(ctx context.Context, field graphql.CollectedField, obj *alertlog.Entry) (ret graphql.Marshaler) { fc, err := ec.fieldContext_AlertLogEntry_id(ctx, field) if err != nil { @@ -11249,8 +11193,8 @@ func (ec *executionContext) fieldContext_Mutation_updateAlerts(ctx context.Conte return ec.fieldContext_Alert_pendingNotifications(ctx, field) case "metrics": return ec.fieldContext_Alert_metrics(ctx, field) - case "feedback": - return ec.fieldContext_Alert_feedback(ctx, field) + case "noiseReason": + return ec.fieldContext_Alert_noiseReason(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type Alert", field.Name) }, @@ -11384,8 +11328,8 @@ func (ec *executionContext) fieldContext_Mutation_escalateAlerts(ctx context.Con return ec.fieldContext_Alert_pendingNotifications(ctx, field) case "metrics": return ec.fieldContext_Alert_metrics(ctx, field) - case "feedback": - return ec.fieldContext_Alert_feedback(ctx, field) + case "noiseReason": + return ec.fieldContext_Alert_noiseReason(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type Alert", field.Name) }, @@ -11739,8 +11683,8 @@ func (ec *executionContext) fieldContext_Mutation_createAlert(ctx context.Contex return ec.fieldContext_Alert_pendingNotifications(ctx, field) case "metrics": return ec.fieldContext_Alert_metrics(ctx, field) - case "feedback": - return ec.fieldContext_Alert_feedback(ctx, field) + case "noiseReason": + return ec.fieldContext_Alert_noiseReason(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type Alert", field.Name) }, @@ -11759,8 +11703,8 @@ func (ec *executionContext) fieldContext_Mutation_createAlert(ctx context.Contex return fc, nil } -func (ec *executionContext) _Mutation_updateAlertFeedback(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_Mutation_updateAlertFeedback(ctx, field) +func (ec *executionContext) _Mutation_setAlertNoiseReason(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Mutation_setAlertNoiseReason(ctx, field) if err != nil { return graphql.Null } @@ -11773,7 +11717,7 @@ func (ec *executionContext) _Mutation_updateAlertFeedback(ctx context.Context, f }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return ec.resolvers.Mutation().UpdateAlertFeedback(rctx, fc.Args["input"].(UpdateAlertFeedbackInput)) + return ec.resolvers.Mutation().SetAlertNoiseReason(rctx, fc.Args["input"].(SetAlertNoiseReasonInput)) }) if err != nil { ec.Error(ctx, err) @@ -11790,7 +11734,7 @@ func (ec *executionContext) _Mutation_updateAlertFeedback(ctx context.Context, f return ec.marshalNBoolean2bool(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext_Mutation_updateAlertFeedback(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_Mutation_setAlertNoiseReason(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "Mutation", Field: field, @@ -11807,7 +11751,7 @@ func (ec *executionContext) fieldContext_Mutation_updateAlertFeedback(ctx contex } }() ctx = graphql.WithFieldContext(ctx, fc) - if fc.Args, err = ec.field_Mutation_updateAlertFeedback_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + if fc.Args, err = ec.field_Mutation_setAlertNoiseReason_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { ec.Error(ctx, err) return fc, err } @@ -14931,8 +14875,8 @@ func (ec *executionContext) fieldContext_Query_alert(ctx context.Context, field return ec.fieldContext_Alert_pendingNotifications(ctx, field) case "metrics": return ec.fieldContext_Alert_metrics(ctx, field) - case "feedback": - return ec.fieldContext_Alert_feedback(ctx, field) + case "noiseReason": + return ec.fieldContext_Alert_noiseReason(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type Alert", field.Name) }, @@ -28514,6 +28458,44 @@ func (ec *executionContext) unmarshalInputServiceSearchOptions(ctx context.Conte return it, nil } +func (ec *executionContext) unmarshalInputSetAlertNoiseReasonInput(ctx context.Context, obj interface{}) (SetAlertNoiseReasonInput, error) { + var it SetAlertNoiseReasonInput + asMap := map[string]interface{}{} + for k, v := range obj.(map[string]interface{}) { + asMap[k] = v + } + + fieldsInOrder := [...]string{"alertID", "noiseReason"} + for _, k := range fieldsInOrder { + v, ok := asMap[k] + if !ok { + continue + } + switch k { + case "alertID": + var err error + + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("alertID")) + data, err := ec.unmarshalNInt2int(ctx, v) + if err != nil { + return it, err + } + it.AlertID = data + case "noiseReason": + var err error + + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("noiseReason")) + data, err := ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } + it.NoiseReason = data + } + } + + return it, nil +} + func (ec *executionContext) unmarshalInputSetFavoriteInput(ctx context.Context, obj interface{}) (SetFavoriteInput, error) { var it SetFavoriteInput asMap := map[string]interface{}{} @@ -29070,44 +29052,6 @@ func (ec *executionContext) unmarshalInputTimeZoneSearchOptions(ctx context.Cont return it, nil } -func (ec *executionContext) unmarshalInputUpdateAlertFeedbackInput(ctx context.Context, obj interface{}) (UpdateAlertFeedbackInput, error) { - var it UpdateAlertFeedbackInput - asMap := map[string]interface{}{} - for k, v := range obj.(map[string]interface{}) { - asMap[k] = v - } - - fieldsInOrder := [...]string{"alertID", "note"} - for _, k := range fieldsInOrder { - v, ok := asMap[k] - if !ok { - continue - } - switch k { - case "alertID": - var err error - - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("alertID")) - data, err := ec.unmarshalNInt2int(ctx, v) - if err != nil { - return it, err - } - it.AlertID = data - case "note": - var err error - - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("note")) - data, err := ec.unmarshalNString2string(ctx, v) - if err != nil { - return it, err - } - it.Note = data - } - } - - return it, nil -} - func (ec *executionContext) unmarshalInputUpdateAlertsByServiceInput(ctx context.Context, obj interface{}) (UpdateAlertsByServiceInput, error) { var it UpdateAlertsByServiceInput asMap := map[string]interface{}{} @@ -30429,7 +30373,7 @@ func (ec *executionContext) _Alert(ctx context.Context, sel ast.SelectionSet, ob } out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) - case "feedback": + case "noiseReason": field := field innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { @@ -30438,7 +30382,7 @@ func (ec *executionContext) _Alert(ctx context.Context, sel ast.SelectionSet, ob ec.Error(ctx, ec.Recover(ctx, r)) } }() - res = ec._Alert_feedback(ctx, field, obj) + res = ec._Alert_noiseReason(ctx, field, obj) return res } @@ -30573,42 +30517,6 @@ func (ec *executionContext) _AlertDataPoint(ctx context.Context, sel ast.Selecti return out } -var alertFeedbackImplementors = []string{"AlertFeedback"} - -func (ec *executionContext) _AlertFeedback(ctx context.Context, sel ast.SelectionSet, obj *AlertFeedback) graphql.Marshaler { - fields := graphql.CollectFields(ec.OperationContext, sel, alertFeedbackImplementors) - - 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("AlertFeedback") - case "note": - out.Values[i] = ec._AlertFeedback_note(ctx, field, obj) - default: - panic("unknown field " + strconv.Quote(field.Name)) - } - } - out.Dispatch(ctx) - if out.Invalids > 0 { - return graphql.Null - } - - atomic.AddInt32(&ec.deferred, int32(len(deferred))) - - for label, dfs := range deferred { - ec.processDeferredGroup(graphql.DeferredGroup{ - Label: label, - Path: graphql.GetPath(ctx), - FieldSet: dfs, - Context: ctx, - }) - } - - return out -} - var alertLogEntryImplementors = []string{"AlertLogEntry"} func (ec *executionContext) _AlertLogEntry(ctx context.Context, sel ast.SelectionSet, obj *alertlog.Entry) graphql.Marshaler { @@ -32455,9 +32363,9 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { return ec._Mutation_createAlert(ctx, field) }) - case "updateAlertFeedback": + case "setAlertNoiseReason": out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { - return ec._Mutation_updateAlertFeedback(ctx, field) + return ec._Mutation_setAlertNoiseReason(ctx, field) }) if out.Values[i] == graphql.Null { out.Invalids++ @@ -39105,6 +39013,11 @@ func (ec *executionContext) marshalNServiceOnCallUser2ᚕgithub.comᚋtarget return ret } +func (ec *executionContext) unmarshalNSetAlertNoiseReasonInput2githubᚗcomᚋtargetᚋgoalertᚋgraphql2ᚐSetAlertNoiseReasonInput(ctx context.Context, v interface{}) (SetAlertNoiseReasonInput, error) { + res, err := ec.unmarshalInputSetAlertNoiseReasonInput(ctx, v) + return res, graphql.ErrorOnPath(ctx, err) +} + func (ec *executionContext) unmarshalNSetFavoriteInput2githubᚗcomᚋtargetᚋgoalertᚋgraphql2ᚐSetFavoriteInput(ctx context.Context, v interface{}) (SetFavoriteInput, error) { res, err := ec.unmarshalInputSetFavoriteInput(ctx, v) return res, graphql.ErrorOnPath(ctx, err) @@ -39669,11 +39582,6 @@ func (ec *executionContext) marshalNTimeZoneConnection2ᚖgithub.comᚋtarget return ec._TimeZoneConnection(ctx, sel, v) } -func (ec *executionContext) unmarshalNUpdateAlertFeedbackInput2githubᚗcomᚋtargetᚋgoalertᚋgraphql2ᚐUpdateAlertFeedbackInput(ctx context.Context, v interface{}) (UpdateAlertFeedbackInput, error) { - res, err := ec.unmarshalInputUpdateAlertFeedbackInput(ctx, v) - return res, graphql.ErrorOnPath(ctx, err) -} - func (ec *executionContext) unmarshalNUpdateAlertsByServiceInput2githubᚗcomᚋtargetᚋgoalertᚋgraphql2ᚐUpdateAlertsByServiceInput(ctx context.Context, v interface{}) (UpdateAlertsByServiceInput, error) { res, err := ec.unmarshalInputUpdateAlertsByServiceInput(ctx, v) return res, graphql.ErrorOnPath(ctx, err) @@ -40397,13 +40305,6 @@ func (ec *executionContext) marshalOAlert2ᚖgithub.comᚋtargetᚋgoalertᚋa return ec._Alert(ctx, sel, v) } -func (ec *executionContext) marshalOAlertFeedback2ᚖgithub.comᚋtargetᚋgoalertᚋgraphql2ᚐAlertFeedback(ctx context.Context, sel ast.SelectionSet, v *AlertFeedback) graphql.Marshaler { - if v == nil { - return graphql.Null - } - return ec._AlertFeedback(ctx, sel, v) -} - func (ec *executionContext) marshalOAlertMetric2ᚖgithub.comᚋtargetᚋgoalertᚋalertᚋalertmetricsᚐMetric(ctx context.Context, sel ast.SelectionSet, v *alertmetrics.Metric) graphql.Marshaler { if v == nil { return graphql.Null diff --git a/graphql2/graphqlapp/alert.go b/graphql2/graphqlapp/alert.go index db8f107ea5..9e3dadcbb4 100644 --- a/graphql2/graphqlapp/alert.go +++ b/graphql2/graphqlapp/alert.go @@ -360,22 +360,20 @@ func (m *Mutation) CreateAlert(ctx context.Context, input graphql2.CreateAlertIn return m.AlertStore.Create(ctx, a) } -func (a *Alert) Feedback(ctx context.Context, raw *alert.Alert) (*graphql2.AlertFeedback, error) { +func (a *Alert) NoiseReason(ctx context.Context, raw *alert.Alert) (*string, error) { am, err := a.AlertStore.Feedback(ctx, raw.ID) if err != nil { return nil, err } - return &graphql2.AlertFeedback{ - Note: &am.Note, - }, nil + return &am.NoiseReason, nil } -func (m *Mutation) UpdateAlertFeedback(ctx context.Context, input graphql2.UpdateAlertFeedbackInput) (bool, error) { +func (m *Mutation) SetAlertNoiseReason(ctx context.Context, input graphql2.SetAlertNoiseReasonInput) (bool, error) { f := &alert.Feedback{ AlertID: input.AlertID, } - f.Note = input.Note + f.NoiseReason = input.NoiseReason err := m.AlertStore.UpdateFeedback(ctx, f) if err != nil { diff --git a/graphql2/models_gen.go b/graphql2/models_gen.go index e9be0a92a2..c7fec2d5c3 100644 --- a/graphql2/models_gen.go +++ b/graphql2/models_gen.go @@ -37,10 +37,6 @@ type AlertDataPoint struct { AlertCount int `json:"alertCount"` } -type AlertFeedback struct { - Note *string `json:"note,omitempty"` -} - type AlertLogEntryConnection struct { Nodes []alertlog.Entry `json:"nodes"` PageInfo *PageInfo `json:"pageInfo"` @@ -460,6 +456,11 @@ type ServiceSearchOptions struct { FavoritesFirst *bool `json:"favoritesFirst,omitempty"` } +type SetAlertNoiseReasonInput struct { + AlertID int `json:"alertID"` + NoiseReason string `json:"noiseReason"` +} + type SetFavoriteInput struct { Target *assignment.RawTarget `json:"target"` Favorite bool `json:"favorite"` @@ -552,11 +553,6 @@ type TimeZoneSearchOptions struct { Omit []string `json:"omit,omitempty"` } -type UpdateAlertFeedbackInput struct { - AlertID int `json:"alertID"` - Note string `json:"note"` -} - type UpdateAlertsByServiceInput struct { ServiceID string `json:"serviceID"` NewStatus AlertStatus `json:"newStatus"` diff --git a/graphql2/schema.graphql b/graphql2/schema.graphql index 7cdca022c6..b150c0fc59 100644 --- a/graphql2/schema.graphql +++ b/graphql2/schema.graphql @@ -514,7 +514,7 @@ type Mutation { deleteAll(input: [TargetInput!]): Boolean! createAlert(input: CreateAlertInput!): Alert - updateAlertFeedback(input: UpdateAlertFeedbackInput!): Boolean! + setAlertNoiseReason(input: SetAlertNoiseReasonInput!): Boolean! createService(input: CreateServiceInput!): Service createEscalationPolicy(input: CreateEscalationPolicyInput!): EscalationPolicy @@ -592,9 +592,9 @@ input CreateAlertInput { sanitize: Boolean } -input UpdateAlertFeedbackInput { +input SetAlertNoiseReasonInput { alertID: Int! - note: String! + noiseReason: String! } input CreateUserInput { @@ -1074,7 +1074,7 @@ type Alert { # metrics are only available for closed alerts metrics: AlertMetric - feedback: AlertFeedback + noiseReason: String } type AlertMetric { @@ -1124,10 +1124,6 @@ type AlertState { repeatCount: Int! } -type AlertFeedback { - note: String -} - type Service { id: ID! name: String! diff --git a/migrate/migrations/20230616110941-add-alerts-feedback.sql b/migrate/migrations/20230616110941-add-alerts-feedback.sql index f197dfc36c..a9aaf9b5d7 100644 --- a/migrate/migrations/20230616110941-add-alerts-feedback.sql +++ b/migrate/migrations/20230616110941-add-alerts-feedback.sql @@ -2,7 +2,7 @@ CREATE TABLE alert_feedback ( alert_id BIGINT PRIMARY KEY REFERENCES alerts (id) ON DELETE CASCADE, - note TEXT + noise_reason TEXT ); -- +migrate Down diff --git a/web/src/app/alerts/components/AlertDetails.tsx b/web/src/app/alerts/components/AlertDetails.tsx index b50a14d3b5..e2f8d06a59 100644 --- a/web/src/app/alerts/components/AlertDetails.tsx +++ b/web/src/app/alerts/components/AlertDetails.tsx @@ -93,7 +93,7 @@ export default function AlertDetails(props: AlertDetailsProps): JSX.Element { variables: { input: { alertID: props.data.id, - note: '', + noiseReason: '', }, }, }) @@ -374,20 +374,20 @@ export default function AlertDetails(props: AlertDetailsProps): JSX.Element { 'This could be due to rate-limiting, processing, or network delays.', })) - const note = alert?.feedback?.note ?? '' - if (note !== '') { - const notesArr = note.split('|') - const reasons = notesArr.join(', ') + const noiseReason = alert?.noiseReason ?? '' + if (noiseReason !== '') { + const nrArr = noiseReason.split('|') + const reasons = nrArr.join(', ') extraNotices = [ ...extraNotices, { type: 'INFO', message: 'This alert has been marked as noise', - details: `Reason${notesArr.length > 1 ? 's' : ''}: ${reasons}`, + details: `Reason${nrArr.length > 1 ? 's' : ''}: ${reasons}`, action: ( undoFeedback()} @@ -407,7 +407,7 @@ export default function AlertDetails(props: AlertDetailsProps): JSX.Element { {/* Main Alert Info */} - {!note && ( + {!noiseReason && ( diff --git a/web/src/app/alerts/components/AlertFeedback.tsx b/web/src/app/alerts/components/AlertFeedback.tsx index 73e771031f..0fb1fc8014 100644 --- a/web/src/app/alerts/components/AlertFeedback.tsx +++ b/web/src/app/alerts/components/AlertFeedback.tsx @@ -12,16 +12,14 @@ const query = gql` query AlertFeedbackQuery($id: Int!) { alert(id: $id) { id - feedback { - note - } + noiseReason } } ` export const mutation = gql` - mutation UpdateFeedbackMutation($input: UpdateAlertFeedbackInput!) { - updateAlertFeedback(input: $input) + mutation SetAlertNoiseReasonMutation($input: SetAlertNoiseReasonInput!) { + setAlertNoiseReason(input: $input) } ` @@ -41,10 +39,10 @@ export default function AlertFeedback(props: AlertFeedbackProps): JSX.Element { const options = ['False positive', 'Not actionable', 'Poor details'] - const dataNotes = data?.alert?.feedback?.note ?? '' + const dataNoiseReason = data?.alert?.noiseReason ?? '' const getDefaults = (): [Array, string] => { - const vals = dataNotes !== '' ? dataNotes.split('|') : [] + const vals = dataNoiseReason !== '' ? dataNoiseReason.split('|') : [] let defaultValue: Array = [] let defaultOther = '' vals.forEach((val: string) => { @@ -59,7 +57,7 @@ export default function AlertFeedback(props: AlertFeedbackProps): JSX.Element { } const defaults = getDefaults() - const [notes, setNotes] = useState>(defaults[0]) + const [noiseReasons, setNoiseReasons] = useState>(defaults[0]) const [other, setOther] = useState(defaults[1]) const [otherChecked, setOtherChecked] = useState(Boolean(defaults[1])) const [mutationStatus, commit] = useMutation(mutation) @@ -67,30 +65,30 @@ export default function AlertFeedback(props: AlertFeedbackProps): JSX.Element { useEffect(() => { const v = getDefaults() - setNotes(v[0]) + setNoiseReasons(v[0]) setOther(v[1]) setOtherChecked(Boolean(v[1])) - }, [dataNotes]) + }, [dataNoiseReason]) function handleSubmit(): void { - let n = notes.slice() + let n = noiseReasons.slice() if (other !== '' && otherChecked) n = [...n, other] commit({ input: { alertID, - note: n.join('|'), + noiseReason: n.join('|'), }, }) } function handleCheck( e: React.ChangeEvent, - note: string, + noiseReason: string, ): void { if (e.target.checked) { - setNotes([...notes, note]) + setNoiseReasons([...noiseReasons, noiseReason]) } else { - setNotes(notes.filter((n) => n !== note)) + setNoiseReasons(noiseReasons.filter((n) => n !== noiseReason)) } } @@ -106,7 +104,7 @@ export default function AlertFeedback(props: AlertFeedbackProps): JSX.Element { control={ handleCheck(e, option)} /> } @@ -150,11 +148,11 @@ export default function AlertFeedback(props: AlertFeedbackProps): JSX.Element { Submit , diff --git a/web/src/app/alerts/pages/AlertDetailPage.tsx b/web/src/app/alerts/pages/AlertDetailPage.tsx index aa4f50839f..ef81d2ce45 100644 --- a/web/src/app/alerts/pages/AlertDetailPage.tsx +++ b/web/src/app/alerts/pages/AlertDetailPage.tsx @@ -13,6 +13,7 @@ const query = gql` summary details createdAt + noiseReason service { id name @@ -38,9 +39,6 @@ const query = gql` pendingNotifications { destination } - feedback { - note - } } } ` diff --git a/web/src/cypress/e2e/alerts.cy.ts b/web/src/cypress/e2e/alerts.cy.ts index 7555121372..eeb7830b00 100644 --- a/web/src/cypress/e2e/alerts.cy.ts +++ b/web/src/cypress/e2e/alerts.cy.ts @@ -412,10 +412,10 @@ function testAlerts(screen: ScreenFormat): void { cy.get('[placeholder="Other (please specify)"]').type('Test') // submit - cy.get('button[aria-label="Submit alert notes"]').should( + cy.get('button[aria-label="Submit noise reasons"]').should( 'not.be.disabled', ) - cy.get('button[aria-label="Submit alert notes"]').click() + cy.get('button[aria-label="Submit noise reasons"]').click() cy.get('label').contains('False positive').should('not.exist') // see notice @@ -427,7 +427,7 @@ function testAlerts(screen: ScreenFormat): void { ) // undo - cy.get('button[aria-label="Reset alert notes"]').click() + cy.get('button[aria-label="Reset noise reasons"]').click() cy.get('body').should('not.contain.text', noticeTitle) cy.get('[data-cy="False positive"] input[type="checkbox"]').should( 'not.be.checked', diff --git a/web/src/schema.d.ts b/web/src/schema.d.ts index a6e14212ee..5acb3e80dc 100644 --- a/web/src/schema.d.ts +++ b/web/src/schema.d.ts @@ -389,7 +389,7 @@ export interface Mutation { updateEscalationPolicyStep: boolean deleteAll: boolean createAlert?: null | Alert - updateAlertFeedback: boolean + setAlertNoiseReason: boolean createService?: null | Service createEscalationPolicy?: null | EscalationPolicy createEscalationPolicyStep?: null | EscalationPolicyStep @@ -442,9 +442,9 @@ export interface CreateAlertInput { sanitize?: null | boolean } -export interface UpdateAlertFeedbackInput { +export interface SetAlertNoiseReasonInput { alertID: number - note: string + noiseReason: string } export interface CreateUserInput { @@ -841,7 +841,7 @@ export interface Alert { recentEvents: AlertLogEntryConnection pendingNotifications: AlertPendingNotification[] metrics?: null | AlertMetric - feedback?: null | AlertFeedback + noiseReason?: null | string } export interface AlertMetric { @@ -886,10 +886,6 @@ export interface AlertState { repeatCount: number } -export interface AlertFeedback { - note?: null | string -} - export interface Service { id: string name: string From afc4203aa52e37e406dbb48e7d7b9caf03ec009a Mon Sep 17 00:00:00 2001 From: Nathaniel Cook Date: Fri, 30 Jun 2023 09:06:13 -0700 Subject: [PATCH 29/35] use sqlc --- alert/queries.sql | 33 +++++++++++++++---- alert/store.go | 32 ++++++------------ gadb/models.go | 5 +++ gadb/queries.sql.go | 52 +++++++++++++++++++++++++---- migrate/schema.sql | 79 ++++++++++++++++++++++++++++++++++----------- 5 files changed, 148 insertions(+), 53 deletions(-) diff --git a/alert/queries.sql b/alert/queries.sql index cf2e1b2bf7..60750bc01a 100644 --- a/alert/queries.sql +++ b/alert/queries.sql @@ -1,5 +1,6 @@ -- name: LockOneAlertService :one -SELECT maintenance_expires_at notnull::bool AS is_maint_mode, +SELECT maintenance_expires_at notnull +::bool AS is_maint_mode, alerts.status FROM services svc JOIN alerts ON alerts.service_id = svc.id @@ -11,13 +12,33 @@ UPDATE escalation_policy_state SET force_escalation = TRUE WHERE alert_id = $1 AND ( - last_escalation <= $2::timestamptz + last_escalation <= $2 +::timestamptz OR last_escalation IS NULL ) RETURNING TRUE; -- name: AlertHasEPState :one -SELECT EXISTS ( +SELECT EXISTS +( SELECT 1 - FROM escalation_policy_state - WHERE alert_id = $1 - ) AS has_ep_state; +FROM escalation_policy_state +WHERE alert_id = $1 + ) +AS has_ep_state; + +-- name: AlertFeedback :one +SELECT + alert_id, noise_reason +FROM alert_feedback +WHERE alert_id = $1; + +-- name: SetAlertFeedback :exec +INSERT INTO alert_feedback + (alert_id, noise_reason) +VALUES + ($1, $2) +ON CONFLICT +(alert_id) DO +UPDATE +SET noise_reason = $2 +WHERE alert_feedback.alert_id = $1; \ No newline at end of file diff --git a/alert/store.go b/alert/store.go index 9778b64d46..a4271e3f92 100644 --- a/alert/store.go +++ b/alert/store.go @@ -49,9 +49,6 @@ type Store struct { escalate *sql.Stmt epState *sql.Stmt svcInfo *sql.Stmt - - feedback *sql.Stmt - updateFeedback *sql.Stmt } // A Trigger signals that an alert needs to be processed @@ -205,21 +202,6 @@ func NewStore(ctx context.Context, db *sql.DB, logDB *alertlog.Store) (*Store, e FROM services WHERE id = $1 `), - - feedback: p(` - SELECT - alert_id, noise_reason - FROM alert_feedback - WHERE alert_id = $1 - `), - - updateFeedback: p(` - INSERT INTO alert_feedback (alert_id, noise_reason) - VALUES ($1, $2) - ON CONFLICT (alert_id) DO UPDATE - SET noise_reason = $2 - WHERE alert_feedback.alert_id = $1 - `), }, prep.Err } @@ -811,15 +793,18 @@ func (s *Store) State(ctx context.Context, alertIDs []int) ([]State, error) { } func (s *Store) Feedback(ctx context.Context, alertID int) (f Feedback, err error) { - row := s.feedback.QueryRowContext(ctx, alertID) - err = row.Scan(&f.AlertID, &f.NoiseReason) + row, err := gadb.New(s.db).AlertFeedback(ctx, int64(alertID)) if errors.Is(err, sql.ErrNoRows) { return Feedback{ AlertID: alertID, NoiseReason: "", }, nil } - return f, err + + return Feedback{ + AlertID: int(row.AlertID), + NoiseReason: row.NoiseReason.String, + }, err } func (s Store) UpdateFeedback(ctx context.Context, feedback *Feedback) error { @@ -834,7 +819,10 @@ func (s Store) UpdateFeedback(ctx context.Context, feedback *Feedback) error { } f.NoiseReason = feedback.NoiseReason - _, err = s.updateFeedback.ExecContext(ctx, feedback.AlertID, f.NoiseReason) + err = gadb.New(s.db).SetAlertFeedback(ctx, gadb.SetAlertFeedbackParams{ + AlertID: int64(feedback.AlertID), + NoiseReason: sql.NullString{String: f.NoiseReason, Valid: true}, + }) if err != nil { return err } diff --git a/gadb/models.go b/gadb/models.go index 9b1f842593..ac4835055c 100644 --- a/gadb/models.go +++ b/gadb/models.go @@ -760,6 +760,11 @@ type Alert struct { Details string } +type AlertFeedback struct { + AlertID int64 + NoiseReason sql.NullString +} + type AlertLog struct { ID int64 AlertID sql.NullInt64 diff --git a/gadb/queries.sql.go b/gadb/queries.sql.go index d295540398..8b345e51ab 100644 --- a/gadb/queries.sql.go +++ b/gadb/queries.sql.go @@ -15,12 +15,28 @@ import ( "github.com/lib/pq" ) +const alertFeedback = `-- name: AlertFeedback :one +SELECT + alert_id, noise_reason +FROM alert_feedback +WHERE alert_id = $1 +` + +func (q *Queries) AlertFeedback(ctx context.Context, alertID int64) (AlertFeedback, error) { + row := q.db.QueryRowContext(ctx, alertFeedback, alertID) + var i AlertFeedback + err := row.Scan(&i.AlertID, &i.NoiseReason) + return i, err +} + const alertHasEPState = `-- name: AlertHasEPState :one -SELECT EXISTS ( +SELECT EXISTS +( SELECT 1 - FROM escalation_policy_state - WHERE alert_id = $1 - ) AS has_ep_state +FROM escalation_policy_state +WHERE alert_id = $1 + ) +AS has_ep_state ` func (q *Queries) AlertHasEPState(ctx context.Context, alertID int64) (bool, error) { @@ -324,7 +340,8 @@ func (q *Queries) FindOneCalSubForUpdate(ctx context.Context, id uuid.UUID) (Fin } const lockOneAlertService = `-- name: LockOneAlertService :one -SELECT maintenance_expires_at notnull::bool AS is_maint_mode, +SELECT maintenance_expires_at notnull +::bool AS is_maint_mode, alerts.status FROM services svc JOIN alerts ON alerts.service_id = svc.id @@ -390,7 +407,8 @@ UPDATE escalation_policy_state SET force_escalation = TRUE WHERE alert_id = $1 AND ( - last_escalation <= $2::timestamptz + last_escalation <= $2 +::timestamptz OR last_escalation IS NULL ) RETURNING TRUE ` @@ -407,6 +425,28 @@ func (q *Queries) RequestAlertEscalationByTime(ctx context.Context, arg RequestA return column_1, err } +const setAlertFeedback = `-- name: SetAlertFeedback :exec +INSERT INTO alert_feedback + (alert_id, noise_reason) +VALUES + ($1, $2) +ON CONFLICT +(alert_id) DO +UPDATE +SET noise_reason = $2 +WHERE alert_feedback.alert_id = $1 +` + +type SetAlertFeedbackParams struct { + AlertID int64 + NoiseReason sql.NullString +} + +func (q *Queries) SetAlertFeedback(ctx context.Context, arg SetAlertFeedbackParams) error { + _, err := q.db.ExecContext(ctx, setAlertFeedback, arg.AlertID, arg.NoiseReason) + return err +} + const statusMgrCMInfo = `-- name: StatusMgrCMInfo :one SELECT user_id, diff --git a/migrate/schema.sql b/migrate/schema.sql index 7b1ca83ce1..9c4412963d 100644 --- a/migrate/schema.sql +++ b/migrate/schema.sql @@ -1,13 +1,13 @@ -- This file is auto-generated by "make db-schema"; DO NOT EDIT --- DATA=f8b8653517556c6064814fa4778fdf024dde96156ce34f9063a28fb49dd6e5f1 - --- DISK=d2225dfa0730854683a85d12e1b83a1c1b5670b5a756c1d52909eb7ada6bb418 - --- PSQL=d2225dfa0730854683a85d12e1b83a1c1b5670b5a756c1d52909eb7ada6bb418 - +-- DATA=9d0f48677fe7449d2c8b638d1332aa46ab2605d699d0b653771387f26e93f4e3 - +-- DISK=334f8f372a31ab442509645785948cbcd3fde9edeb632064547bd7d75e8d7754 - +-- PSQL=334f8f372a31ab442509645785948cbcd3fde9edeb632064547bd7d75e8d7754 - -- -- PostgreSQL database dump -- --- Dumped from database version 13.5 --- Dumped by pg_dump version 13.6 (Ubuntu 13.6-0ubuntu0.21.10.1) +-- Dumped from database version 12.13 +-- Dumped by pg_dump version 15.1 SET statement_timeout = 0; SET lock_timeout = 0; @@ -20,6 +20,13 @@ SET xmloption = content; SET client_min_messages = warning; SET row_security = off; +-- +-- Name: public; Type: SCHEMA; Schema: -; Owner: - +-- + +-- *not* creating schema, since initdb creates it + + -- -- Name: pgcrypto; Type: EXTENSION; Schema: -; Owner: - -- @@ -1511,6 +1518,16 @@ SET default_tablespace = ''; SET default_table_access_method = heap; +-- +-- Name: alert_feedback; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.alert_feedback ( + alert_id bigint NOT NULL, + noise_reason text +); + + -- -- Name: alert_logs; Type: TABLE; Schema: public; Owner: - -- @@ -1847,7 +1864,7 @@ ALTER SEQUENCE public.ep_step_on_call_users_id_seq OWNED BY public.ep_step_on_ca -- CREATE TABLE public.escalation_policies ( - id uuid DEFAULT gen_random_uuid() NOT NULL, + id uuid DEFAULT public.gen_random_uuid() NOT NULL, name text NOT NULL, description text DEFAULT ''::text NOT NULL, repeat integer DEFAULT 0 NOT NULL, @@ -1860,7 +1877,7 @@ CREATE TABLE public.escalation_policies ( -- CREATE TABLE public.escalation_policy_actions ( - id uuid DEFAULT gen_random_uuid() NOT NULL, + id uuid DEFAULT public.gen_random_uuid() NOT NULL, escalation_policy_step_id uuid NOT NULL, user_id uuid, schedule_id uuid, @@ -1929,7 +1946,7 @@ ALTER SEQUENCE public.escalation_policy_state_id_seq OWNED BY public.escalation_ -- CREATE TABLE public.escalation_policy_steps ( - id uuid DEFAULT gen_random_uuid() NOT NULL, + id uuid DEFAULT public.gen_random_uuid() NOT NULL, delay integer DEFAULT 1 NOT NULL, step_number integer DEFAULT '-1'::integer NOT NULL, escalation_policy_id uuid NOT NULL @@ -1977,7 +1994,7 @@ CREATE SEQUENCE public.incident_number_seq -- CREATE TABLE public.integration_keys ( - id uuid DEFAULT gen_random_uuid() NOT NULL, + id uuid DEFAULT public.gen_random_uuid() NOT NULL, name text NOT NULL, type public.enum_integration_keys_type NOT NULL, service_id uuid NOT NULL @@ -2048,7 +2065,7 @@ CREATE TABLE public.notification_channels ( -- CREATE TABLE public.notification_policy_cycles ( - id uuid DEFAULT gen_random_uuid() NOT NULL, + id uuid DEFAULT public.gen_random_uuid() NOT NULL, user_id uuid NOT NULL, alert_id integer NOT NULL, repeat_count integer DEFAULT 0 NOT NULL, @@ -2064,7 +2081,7 @@ WITH (fillfactor='65'); -- CREATE TABLE public.outgoing_messages ( - id uuid DEFAULT gen_random_uuid() NOT NULL, + id uuid DEFAULT public.gen_random_uuid() NOT NULL, message_type public.enum_outgoing_messages_type NOT NULL, contact_method_id uuid, created_at timestamp with time zone DEFAULT now() NOT NULL, @@ -2138,7 +2155,7 @@ ALTER SEQUENCE public.region_ids_id_seq OWNED BY public.region_ids.id; -- CREATE TABLE public.rotation_participants ( - id uuid DEFAULT gen_random_uuid() NOT NULL, + id uuid DEFAULT public.gen_random_uuid() NOT NULL, rotation_id uuid NOT NULL, "position" integer NOT NULL, user_id uuid NOT NULL @@ -2183,7 +2200,7 @@ ALTER SEQUENCE public.rotation_state_id_seq OWNED BY public.rotation_state.id; -- CREATE TABLE public.rotations ( - id uuid DEFAULT gen_random_uuid() NOT NULL, + id uuid DEFAULT public.gen_random_uuid() NOT NULL, name text NOT NULL, description text DEFAULT ''::text NOT NULL, type public.enum_rotation_type NOT NULL, @@ -2265,7 +2282,7 @@ ALTER SEQUENCE public.schedule_on_call_users_id_seq OWNED BY public.schedule_on_ -- CREATE TABLE public.schedule_rules ( - id uuid DEFAULT gen_random_uuid() NOT NULL, + id uuid DEFAULT public.gen_random_uuid() NOT NULL, schedule_id uuid NOT NULL, sunday boolean DEFAULT true NOT NULL, monday boolean DEFAULT true NOT NULL, @@ -2289,7 +2306,7 @@ CREATE TABLE public.schedule_rules ( -- CREATE TABLE public.schedules ( - id uuid DEFAULT gen_random_uuid() NOT NULL, + id uuid DEFAULT public.gen_random_uuid() NOT NULL, name text NOT NULL, description text DEFAULT ''::text NOT NULL, time_zone text NOT NULL, @@ -2302,7 +2319,7 @@ CREATE TABLE public.schedules ( -- CREATE TABLE public.services ( - id uuid DEFAULT gen_random_uuid() NOT NULL, + id uuid DEFAULT public.gen_random_uuid() NOT NULL, name text NOT NULL, description text DEFAULT ''::text NOT NULL, escalation_policy_id uuid NOT NULL, @@ -2328,7 +2345,7 @@ CREATE TABLE public.switchover_log ( CREATE TABLE public.switchover_state ( ok boolean NOT NULL, current_state public.enum_switchover_state NOT NULL, - db_id uuid DEFAULT gen_random_uuid() NOT NULL, + db_id uuid DEFAULT public.gen_random_uuid() NOT NULL, CONSTRAINT switchover_state_ok_check CHECK (ok) ); @@ -2453,7 +2470,7 @@ CREATE TABLE public.user_calendar_subscriptions ( -- CREATE TABLE public.user_contact_methods ( - id uuid DEFAULT gen_random_uuid() NOT NULL, + id uuid DEFAULT public.gen_random_uuid() NOT NULL, name text NOT NULL, type public.enum_user_contact_method_type NOT NULL, value text NOT NULL, @@ -2505,7 +2522,7 @@ ALTER SEQUENCE public.user_favorites_id_seq OWNED BY public.user_favorites.id; -- CREATE TABLE public.user_notification_rules ( - id uuid DEFAULT gen_random_uuid() NOT NULL, + id uuid DEFAULT public.gen_random_uuid() NOT NULL, delay_minutes integer DEFAULT 0 NOT NULL, contact_method_id uuid NOT NULL, user_id uuid NOT NULL, @@ -2694,6 +2711,14 @@ ALTER TABLE ONLY public.twilio_voice_errors ALTER COLUMN id SET DEFAULT nextval( ALTER TABLE ONLY public.user_favorites ALTER COLUMN id SET DEFAULT nextval('public.user_favorites_id_seq'::regclass); +-- +-- Name: alert_feedback alert_feedback_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.alert_feedback + ADD CONSTRAINT alert_feedback_pkey PRIMARY KEY (alert_id); + + -- -- Name: alert_logs alert_logs_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -4067,6 +4092,14 @@ CREATE TRIGGER trg_set_rot_state_pos_on_part_reorder BEFORE UPDATE ON public.rot CREATE TRIGGER trg_start_rotation_on_first_part_add AFTER INSERT ON public.rotation_participants FOR EACH ROW EXECUTE FUNCTION public.fn_start_rotation_on_first_part_add(); +-- +-- Name: alert_feedback alert_feedback_alert_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.alert_feedback + ADD CONSTRAINT alert_feedback_alert_id_fkey FOREIGN KEY (alert_id) REFERENCES public.alerts(id) ON DELETE CASCADE; + + -- -- Name: alert_metrics alert_metrics_alert_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -4603,6 +4636,14 @@ ALTER TABLE ONLY public.users ADD CONSTRAINT users_alert_status_log_contact_method_id_fkey FOREIGN KEY (alert_status_log_contact_method_id) REFERENCES public.user_contact_methods(id) ON DELETE SET NULL DEFERRABLE; +-- +-- Name: SCHEMA public; Type: ACL; Schema: -; Owner: - +-- + +REVOKE USAGE ON SCHEMA public FROM PUBLIC; +GRANT ALL ON SCHEMA public TO PUBLIC; + + -- -- PostgreSQL database dump complete -- From 28aa1996ec1a7fd58a53286b3f158fe692b581b8 Mon Sep 17 00:00:00 2001 From: Nathaniel Cook Date: Fri, 30 Jun 2023 09:12:10 -0700 Subject: [PATCH 30/35] use pgformatter --- alert/queries.sql | 70 +++++++++++++++++++++++++---------------------- 1 file changed, 38 insertions(+), 32 deletions(-) diff --git a/alert/queries.sql b/alert/queries.sql index 60750bc01a..0f0875da10 100644 --- a/alert/queries.sql +++ b/alert/queries.sql @@ -1,44 +1,50 @@ -- name: LockOneAlertService :one -SELECT maintenance_expires_at notnull -::bool AS is_maint_mode, +SELECT + maintenance_expires_at NOTNULL::bool AS is_maint_mode, alerts.status -FROM services svc +FROM + services svc JOIN alerts ON alerts.service_id = svc.id -WHERE alerts.id = $1 FOR -UPDATE; +WHERE + alerts.id = $1 +FOR UPDATE; -- name: RequestAlertEscalationByTime :one -UPDATE escalation_policy_state -SET force_escalation = TRUE -WHERE alert_id = $1 - AND ( - last_escalation <= $2 -::timestamptz - OR last_escalation IS NULL - ) RETURNING TRUE; +UPDATE + escalation_policy_state +SET + force_escalation = TRUE +WHERE + alert_id = $1 + AND (last_escalation <= $2::timestamptz + OR last_escalation IS NULL) +RETURNING + TRUE; -- name: AlertHasEPState :one -SELECT EXISTS -( - SELECT 1 -FROM escalation_policy_state -WHERE alert_id = $1 - ) -AS has_ep_state; +SELECT + EXISTS ( + SELECT + 1 + FROM + escalation_policy_state + WHERE + alert_id = $1) AS has_ep_state; -- name: AlertFeedback :one SELECT - alert_id, noise_reason -FROM alert_feedback -WHERE alert_id = $1; + alert_id, + noise_reason +FROM + alert_feedback +WHERE + alert_id = $1; -- name: SetAlertFeedback :exec -INSERT INTO alert_feedback - (alert_id, noise_reason) -VALUES - ($1, $2) -ON CONFLICT -(alert_id) DO -UPDATE -SET noise_reason = $2 -WHERE alert_feedback.alert_id = $1; \ No newline at end of file +INSERT INTO alert_feedback(alert_id, noise_reason) + VALUES ($1, $2) +ON CONFLICT (alert_id) + DO UPDATE SET + noise_reason = $2 + WHERE + alert_feedback.alert_id = $1; From 285ab59f27d26847e5962d28b1e83d39f21f201a Mon Sep 17 00:00:00 2001 From: Nathaniel Cook Date: Thu, 6 Jul 2023 11:30:01 -0700 Subject: [PATCH 31/35] PR comments --- alert/store.go | 8 +++----- graphql2/graphqlapp/alert.go | 14 +++++++------- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/alert/store.go b/alert/store.go index a4271e3f92..adb1333313 100644 --- a/alert/store.go +++ b/alert/store.go @@ -796,8 +796,7 @@ func (s *Store) Feedback(ctx context.Context, alertID int) (f Feedback, err erro row, err := gadb.New(s.db).AlertFeedback(ctx, int64(alertID)) if errors.Is(err, sql.ErrNoRows) { return Feedback{ - AlertID: alertID, - NoiseReason: "", + AlertID: alertID, }, nil } @@ -813,15 +812,14 @@ func (s Store) UpdateFeedback(ctx context.Context, feedback *Feedback) error { return err } - f, err := (s).Feedback(ctx, feedback.AlertID) + err = validate.Text("Noise Reason", feedback.NoiseReason, 1, 255) if err != nil { return err } - f.NoiseReason = feedback.NoiseReason err = gadb.New(s.db).SetAlertFeedback(ctx, gadb.SetAlertFeedbackParams{ AlertID: int64(feedback.AlertID), - NoiseReason: sql.NullString{String: f.NoiseReason, Valid: true}, + NoiseReason: sql.NullString{String: feedback.NoiseReason, Valid: true}, }) if err != nil { return err diff --git a/graphql2/graphqlapp/alert.go b/graphql2/graphqlapp/alert.go index 9e3dadcbb4..af0788dd0a 100644 --- a/graphql2/graphqlapp/alert.go +++ b/graphql2/graphqlapp/alert.go @@ -365,17 +365,17 @@ func (a *Alert) NoiseReason(ctx context.Context, raw *alert.Alert) (*string, err if err != nil { return nil, err } + if am.NoiseReason == "" { + return nil, nil + } return &am.NoiseReason, nil } func (m *Mutation) SetAlertNoiseReason(ctx context.Context, input graphql2.SetAlertNoiseReasonInput) (bool, error) { - f := &alert.Feedback{ - AlertID: input.AlertID, - } - - f.NoiseReason = input.NoiseReason - - err := m.AlertStore.UpdateFeedback(ctx, f) + err := m.AlertStore.UpdateFeedback(ctx, &alert.Feedback{ + AlertID: input.AlertID, + NoiseReason: input.NoiseReason, + }) if err != nil { return false, err } From dc7468fb7dee4dabd13846aac2fa0f88debdd81c Mon Sep 17 00:00:00 2001 From: Nathaniel Cook Date: Thu, 6 Jul 2023 12:48:24 -0700 Subject: [PATCH 32/35] Update alert/store.go Co-authored-by: Nathaniel Caza --- alert/store.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/alert/store.go b/alert/store.go index adb1333313..f497cd32c4 100644 --- a/alert/store.go +++ b/alert/store.go @@ -812,7 +812,7 @@ func (s Store) UpdateFeedback(ctx context.Context, feedback *Feedback) error { return err } - err = validate.Text("Noise Reason", feedback.NoiseReason, 1, 255) + err = validate.Text("NoiseReason", feedback.NoiseReason, 1, 255) if err != nil { return err } From a8eeef17cf157b6643054ebda439e33a8d34dd0c Mon Sep 17 00:00:00 2001 From: Nathaniel Cook Date: Thu, 6 Jul 2023 13:38:16 -0700 Subject: [PATCH 33/35] feedback permissions and generated file --- alert/store.go | 11 +++++-- gadb/queries.sql.go | 70 ++++++++++++++++++++++++--------------------- 2 files changed, 46 insertions(+), 35 deletions(-) diff --git a/alert/store.go b/alert/store.go index f497cd32c4..81a6cf57e0 100644 --- a/alert/store.go +++ b/alert/store.go @@ -792,15 +792,20 @@ func (s *Store) State(ctx context.Context, alertIDs []int) ([]State, error) { return list, nil } -func (s *Store) Feedback(ctx context.Context, alertID int) (f Feedback, err error) { +func (s *Store) Feedback(ctx context.Context, alertID int) (*Feedback, error) { + err := permission.LimitCheckAny(ctx, permission.System, permission.User) + if err != nil { + return nil, err + } + row, err := gadb.New(s.db).AlertFeedback(ctx, int64(alertID)) if errors.Is(err, sql.ErrNoRows) { - return Feedback{ + return &Feedback{ AlertID: alertID, }, nil } - return Feedback{ + return &Feedback{ AlertID: int(row.AlertID), NoiseReason: row.NoiseReason.String, }, err diff --git a/gadb/queries.sql.go b/gadb/queries.sql.go index 8b345e51ab..a45bb366d5 100644 --- a/gadb/queries.sql.go +++ b/gadb/queries.sql.go @@ -17,9 +17,12 @@ import ( const alertFeedback = `-- name: AlertFeedback :one SELECT - alert_id, noise_reason -FROM alert_feedback -WHERE alert_id = $1 + alert_id, + noise_reason +FROM + alert_feedback +WHERE + alert_id = $1 ` func (q *Queries) AlertFeedback(ctx context.Context, alertID int64) (AlertFeedback, error) { @@ -30,13 +33,14 @@ func (q *Queries) AlertFeedback(ctx context.Context, alertID int64) (AlertFeedba } const alertHasEPState = `-- name: AlertHasEPState :one -SELECT EXISTS -( - SELECT 1 -FROM escalation_policy_state -WHERE alert_id = $1 - ) -AS has_ep_state +SELECT + EXISTS ( + SELECT + 1 + FROM + escalation_policy_state + WHERE + alert_id = $1) AS has_ep_state ` func (q *Queries) AlertHasEPState(ctx context.Context, alertID int64) (bool, error) { @@ -340,13 +344,15 @@ func (q *Queries) FindOneCalSubForUpdate(ctx context.Context, id uuid.UUID) (Fin } const lockOneAlertService = `-- name: LockOneAlertService :one -SELECT maintenance_expires_at notnull -::bool AS is_maint_mode, +SELECT + maintenance_expires_at NOTNULL::bool AS is_maint_mode, alerts.status -FROM services svc +FROM + services svc JOIN alerts ON alerts.service_id = svc.id -WHERE alerts.id = $1 FOR -UPDATE +WHERE + alerts.id = $1 +FOR UPDATE ` type LockOneAlertServiceRow struct { @@ -403,14 +409,16 @@ func (q *Queries) Now(ctx context.Context) (time.Time, error) { } const requestAlertEscalationByTime = `-- name: RequestAlertEscalationByTime :one -UPDATE escalation_policy_state -SET force_escalation = TRUE -WHERE alert_id = $1 - AND ( - last_escalation <= $2 -::timestamptz - OR last_escalation IS NULL - ) RETURNING TRUE +UPDATE + escalation_policy_state +SET + force_escalation = TRUE +WHERE + alert_id = $1 + AND (last_escalation <= $2::timestamptz + OR last_escalation IS NULL) +RETURNING + TRUE ` type RequestAlertEscalationByTimeParams struct { @@ -426,15 +434,13 @@ func (q *Queries) RequestAlertEscalationByTime(ctx context.Context, arg RequestA } const setAlertFeedback = `-- name: SetAlertFeedback :exec -INSERT INTO alert_feedback - (alert_id, noise_reason) -VALUES - ($1, $2) -ON CONFLICT -(alert_id) DO -UPDATE -SET noise_reason = $2 -WHERE alert_feedback.alert_id = $1 +INSERT INTO alert_feedback(alert_id, noise_reason) + VALUES ($1, $2) +ON CONFLICT (alert_id) + DO UPDATE SET + noise_reason = $2 + WHERE + alert_feedback.alert_id = $1 ` type SetAlertFeedbackParams struct { From 2f949f38975281672cbd9480ad5e459dc5e94f60 Mon Sep 17 00:00:00 2001 From: Nathaniel Cook Date: Tue, 11 Jul 2023 10:58:27 -0700 Subject: [PATCH 34/35] update test name --- web/src/cypress/e2e/alerts.cy.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/src/cypress/e2e/alerts.cy.ts b/web/src/cypress/e2e/alerts.cy.ts index eeb7830b00..2591e02cfd 100644 --- a/web/src/cypress/e2e/alerts.cy.ts +++ b/web/src/cypress/e2e/alerts.cy.ts @@ -394,8 +394,8 @@ function testAlerts(screen: ScreenFormat): void { cy.get('body').should('contain', 'CLOSED') }) - it('should set alert notes', () => { - // set all notes, checking carefully because of async setState + it('should set alert noise reasons', () => { + // set all noise reasons, checking carefully because of async setState cy.get('body').should('contain.text', 'Is this alert noise?') cy.get('[data-cy="False positive"] input[type="checkbox"]').check() cy.get('[data-cy="False positive"] input[type="checkbox"]').should( From eadc84a96b9864b445d540216a9673820b193033 Mon Sep 17 00:00:00 2001 From: Nathaniel Cook Date: Tue, 11 Jul 2023 13:00:18 -0700 Subject: [PATCH 35/35] PR comments --- alert/store.go | 7 +++++-- gadb/models.go | 2 +- gadb/queries.sql.go | 2 +- migrate/migrations/20230616110941-add-alerts-feedback.sql | 2 +- migrate/schema.sql | 4 ++-- 5 files changed, 10 insertions(+), 7 deletions(-) diff --git a/alert/store.go b/alert/store.go index 81a6cf57e0..2b152b001f 100644 --- a/alert/store.go +++ b/alert/store.go @@ -804,10 +804,13 @@ func (s *Store) Feedback(ctx context.Context, alertID int) (*Feedback, error) { AlertID: alertID, }, nil } + if err != nil { + return nil, err + } return &Feedback{ AlertID: int(row.AlertID), - NoiseReason: row.NoiseReason.String, + NoiseReason: row.NoiseReason, }, err } @@ -824,7 +827,7 @@ func (s Store) UpdateFeedback(ctx context.Context, feedback *Feedback) error { err = gadb.New(s.db).SetAlertFeedback(ctx, gadb.SetAlertFeedbackParams{ AlertID: int64(feedback.AlertID), - NoiseReason: sql.NullString{String: feedback.NoiseReason, Valid: true}, + NoiseReason: feedback.NoiseReason, }) if err != nil { return err diff --git a/gadb/models.go b/gadb/models.go index ac4835055c..9591b41cb4 100644 --- a/gadb/models.go +++ b/gadb/models.go @@ -762,7 +762,7 @@ type Alert struct { type AlertFeedback struct { AlertID int64 - NoiseReason sql.NullString + NoiseReason string } type AlertLog struct { diff --git a/gadb/queries.sql.go b/gadb/queries.sql.go index a45bb366d5..bf16169206 100644 --- a/gadb/queries.sql.go +++ b/gadb/queries.sql.go @@ -445,7 +445,7 @@ ON CONFLICT (alert_id) type SetAlertFeedbackParams struct { AlertID int64 - NoiseReason sql.NullString + NoiseReason string } func (q *Queries) SetAlertFeedback(ctx context.Context, arg SetAlertFeedbackParams) error { diff --git a/migrate/migrations/20230616110941-add-alerts-feedback.sql b/migrate/migrations/20230616110941-add-alerts-feedback.sql index a9aaf9b5d7..5f2cbbe443 100644 --- a/migrate/migrations/20230616110941-add-alerts-feedback.sql +++ b/migrate/migrations/20230616110941-add-alerts-feedback.sql @@ -2,7 +2,7 @@ CREATE TABLE alert_feedback ( alert_id BIGINT PRIMARY KEY REFERENCES alerts (id) ON DELETE CASCADE, - noise_reason TEXT + noise_reason TEXT NOT NULL ); -- +migrate Down diff --git a/migrate/schema.sql b/migrate/schema.sql index 9c4412963d..d255c0f645 100644 --- a/migrate/schema.sql +++ b/migrate/schema.sql @@ -1,5 +1,5 @@ -- This file is auto-generated by "make db-schema"; DO NOT EDIT --- DATA=9d0f48677fe7449d2c8b638d1332aa46ab2605d699d0b653771387f26e93f4e3 - +-- DATA=834c3a25163544ad7cf2fe079cc7da7119b95359609b20c584f0561411ff5200 - -- DISK=334f8f372a31ab442509645785948cbcd3fde9edeb632064547bd7d75e8d7754 - -- PSQL=334f8f372a31ab442509645785948cbcd3fde9edeb632064547bd7d75e8d7754 - -- @@ -1524,7 +1524,7 @@ SET default_table_access_method = heap; CREATE TABLE public.alert_feedback ( alert_id bigint NOT NULL, - noise_reason text + noise_reason text NOT NULL );