From b29ec64b6e97aa958b9022c146529f8888c0bce9 Mon Sep 17 00:00:00 2001 From: Nathaniel Caza Date: Tue, 26 Dec 2023 14:00:52 -0600 Subject: [PATCH 01/11] add additional details column --- migrate/migrations/20231226135914-hb-addl-details.sql | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 migrate/migrations/20231226135914-hb-addl-details.sql diff --git a/migrate/migrations/20231226135914-hb-addl-details.sql b/migrate/migrations/20231226135914-hb-addl-details.sql new file mode 100644 index 0000000000..efc6ecffa8 --- /dev/null +++ b/migrate/migrations/20231226135914-hb-addl-details.sql @@ -0,0 +1,8 @@ +-- +migrate Up +ALTER TABLE heartbeat_monitors + ADD COLUMN additional_details TEXT; + +-- +migrate Down +ALTER TABLE heartbeat_monitors + DROP COLUMN additional_details; + From 8038ba3969f072977cb4561be3c911b4ace53782 Mon Sep 17 00:00:00 2001 From: Nathaniel Caza Date: Tue, 26 Dec 2023 14:04:33 -0600 Subject: [PATCH 02/11] update store for new field --- heartbeat/monitor.go | 6 +++++- heartbeat/store.go | 15 ++++++++------- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/heartbeat/monitor.go b/heartbeat/monitor.go index ef9316dc9f..4736cd19f6 100644 --- a/heartbeat/monitor.go +++ b/heartbeat/monitor.go @@ -4,6 +4,7 @@ import ( "time" "github.com/jackc/pgtype" + "github.com/target/goalert/alert" "github.com/target/goalert/util/sqlutil" "github.com/target/goalert/validation/validate" ) @@ -15,6 +16,8 @@ type Monitor struct { ServiceID string `json:"service_id,omitempty"` Timeout time.Duration `json:"timeout,omitempty"` + AddtionalDetails string + lastState State lastHeartbeat time.Time } @@ -31,6 +34,7 @@ func (m Monitor) Normalize() (*Monitor, error) { validate.UUID("ServiceID", m.ServiceID), validate.IDName("Name", m.Name), validate.Duration("Timeout", m.Timeout, 5*time.Minute, 9000*time.Hour), + validate.Text("AdditionalDetails", m.AddtionalDetails, 0, alert.MaxDetailsLength), ) if err != nil { return nil, err @@ -47,7 +51,7 @@ func (m *Monitor) scanFrom(scanFn func(...interface{}) error) error { timeout pgtype.Interval ) - err := scanFn(&m.ID, &m.Name, &m.ServiceID, &timeout, &m.lastState, &t) + err := scanFn(&m.ID, &m.Name, &m.ServiceID, &timeout, &m.lastState, &t, &m.AddtionalDetails) if err != nil { return err } diff --git a/heartbeat/store.go b/heartbeat/store.go index ccf90958ac..4258206411 100644 --- a/heartbeat/store.go +++ b/heartbeat/store.go @@ -36,24 +36,24 @@ func NewStore(ctx context.Context, db *sql.DB) (*Store, error) { create: p.P(` insert into heartbeat_monitors ( - id, name, service_id, heartbeat_interval - ) values ($1, $2, $3, $4) + id, name, service_id, heartbeat_interval, additional_details + ) values ($1, $2, $3, $4, $5) `), findAll: p.P(` select - id, name, service_id, heartbeat_interval, last_state, last_heartbeat + id, name, service_id, heartbeat_interval, last_state, last_heartbeat, coalesce(additional_details, '') from heartbeat_monitors where service_id = $1 `), findMany: p.P(` select - id, name, service_id, heartbeat_interval, last_state, last_heartbeat + id, name, service_id, heartbeat_interval, last_state, last_heartbeat, coalesce(additional_details, '') from heartbeat_monitors where id = any($1) `), findOneUpd: p.P(` select - id, name, service_id, heartbeat_interval, last_state, last_heartbeat + id, name, service_id, heartbeat_interval, last_state, last_heartbeat, coalesce(additional_details, '') from heartbeat_monitors where id = $1 for update @@ -67,6 +67,7 @@ func NewStore(ctx context.Context, db *sql.DB) (*Store, error) { set name = $2, heartbeat_interval = $3 + additional_details = $4 where id = $1 `), getSvcID: p.P(`select service_id from heartbeat_monitors where id = $1`), @@ -98,7 +99,7 @@ func (s *Store) CreateTx(ctx context.Context, tx *sql.Tx, m *Monitor) (*Monitor, n.ID = uuid.New().String() n.lastState = StateInactive - _, err = tx.StmtContext(ctx, s.create).ExecContext(ctx, n.ID, n.Name, n.ServiceID, &timeout) + _, err = tx.StmtContext(ctx, s.create).ExecContext(ctx, n.ID, n.Name, n.ServiceID, &timeout, m.AddtionalDetails) if err != nil { return nil, err } @@ -169,7 +170,7 @@ func (s *Store) UpdateTx(ctx context.Context, tx *sql.Tx, m *Monitor) error { return err } - _, err = stmt.ExecContext(ctx, n.ID, n.Name, &timeout) + _, err = stmt.ExecContext(ctx, n.ID, n.Name, &timeout, m.AddtionalDetails) return err } From aacfde94165f6bf6cd2297e563cb507439e5f162 Mon Sep 17 00:00:00 2001 From: Nathaniel Caza Date: Tue, 26 Dec 2023 14:08:28 -0600 Subject: [PATCH 03/11] add details to created alert --- engine/heartbeatmanager/db.go | 2 +- engine/heartbeatmanager/process.go | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/engine/heartbeatmanager/db.go b/engine/heartbeatmanager/db.go index 2f0f810932..8f2d459479 100644 --- a/engine/heartbeatmanager/db.go +++ b/engine/heartbeatmanager/db.go @@ -53,7 +53,7 @@ func NewDB(ctx context.Context, db *sql.DB, a *alert.Store) (*DB, error) { set last_state = 'unhealthy' from rows where mon.id = rows.id - returning mon.id, name, service_id, last_heartbeat + returning mon.id, name, service_id, last_heartbeat, coalesce(additional_details, '') `), fetchHealthy: p.P(` with rows as ( diff --git a/engine/heartbeatmanager/process.go b/engine/heartbeatmanager/process.go index 778645b766..23b99c6a4e 100644 --- a/engine/heartbeatmanager/process.go +++ b/engine/heartbeatmanager/process.go @@ -11,6 +11,7 @@ import ( "github.com/target/goalert/permission" "github.com/target/goalert/util/log" "github.com/target/goalert/util/sqlutil" + "github.com/target/goalert/validation/validate" ) // UpdateAll will process all heartbeats opening and closing alerts as needed. @@ -38,9 +39,13 @@ func (db *DB) processAll(ctx context.Context) error { return errors.Wrap(err, "fetch unhealthy heartbeats") } for _, row := range bad { + details := "Last heartbeat: " + row.LastHeartbeat.Format(time.UnixDate) + if row.AddlDetails != "" { + details += "\n\n" + row.AddlDetails + } a, isNew, err := db.alertStore.CreateOrUpdateTx(row.Context(ctx), tx, &alert.Alert{ Summary: fmt.Sprintf("Heartbeat monitor '%s' expired.", row.Name), - Details: "Last heartbeat: " + row.LastHeartbeat.Format(time.UnixDate), + Details: validate.SanitizeText(details, alert.MaxDetailsLength), Status: alert.StatusTriggered, ServiceID: row.ServiceID, Dedup: &alert.DedupID{ @@ -97,6 +102,7 @@ type row struct { Name string ServiceID string LastHeartbeat time.Time + AddlDetails string } func (r row) Context(ctx context.Context) context.Context { @@ -115,7 +121,7 @@ func (db *DB) unhealthy(ctx context.Context, tx *sql.Tx) ([]row, error) { var result []row for rows.Next() { var r row - err = rows.Scan(&r.ID, &r.Name, &r.ServiceID, &r.LastHeartbeat) + err = rows.Scan(&r.ID, &r.Name, &r.ServiceID, &r.LastHeartbeat, &r.AddlDetails) if err != nil { return nil, err } From 270453d3813ebf862e94f65e129cee9a60302866 Mon Sep 17 00:00:00 2001 From: Nathaniel Caza Date: Tue, 26 Dec 2023 14:12:04 -0600 Subject: [PATCH 04/11] update graphql for new fields --- graphql2/generated.go | 127 ++++++++++++++++++++++-- graphql2/graphqlapp/heartbeatmonitor.go | 17 +++- graphql2/models_gen.go | 14 +-- graphql2/schema.graphql | 3 + web/src/schema.d.ts | 3 + 5 files changed, 146 insertions(+), 18 deletions(-) diff --git a/graphql2/generated.go b/graphql2/generated.go index f3b1d9bc7f..9554143295 100644 --- a/graphql2/generated.go +++ b/graphql2/generated.go @@ -258,13 +258,14 @@ type ComplexityRoot struct { } HeartbeatMonitor struct { - Href func(childComplexity int) int - ID func(childComplexity int) int - LastHeartbeat func(childComplexity int) int - LastState func(childComplexity int) int - Name func(childComplexity int) int - ServiceID func(childComplexity int) int - TimeoutMinutes func(childComplexity int) int + AdditionalDetails func(childComplexity int) int + Href func(childComplexity int) int + ID func(childComplexity int) int + LastHeartbeat func(childComplexity int) int + LastState func(childComplexity int) int + Name func(childComplexity int) int + ServiceID func(childComplexity int) int + TimeoutMinutes func(childComplexity int) int } IntegrationKey struct { @@ -738,6 +739,7 @@ type HeartbeatMonitorResolver interface { TimeoutMinutes(ctx context.Context, obj *heartbeat.Monitor) (int, error) Href(ctx context.Context, obj *heartbeat.Monitor) (string, error) + AdditionalDetails(ctx context.Context, obj *heartbeat.Monitor) (string, error) } type IntegrationKeyResolver interface { Type(ctx context.Context, obj *integrationkey.IntegrationKey) (IntegrationKeyType, error) @@ -1638,6 +1640,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.GQLAPIKeyUsage.Ua(childComplexity), true + case "HeartbeatMonitor.additionalDetails": + if e.complexity.HeartbeatMonitor.AdditionalDetails == nil { + break + } + + return e.complexity.HeartbeatMonitor.AdditionalDetails(childComplexity), true + case "HeartbeatMonitor.href": if e.complexity.HeartbeatMonitor.Href == nil { break @@ -10698,6 +10707,50 @@ func (ec *executionContext) fieldContext_HeartbeatMonitor_href(ctx context.Conte return fc, nil } +func (ec *executionContext) _HeartbeatMonitor_additionalDetails(ctx context.Context, field graphql.CollectedField, obj *heartbeat.Monitor) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_HeartbeatMonitor_additionalDetails(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.HeartbeatMonitor().AdditionalDetails(rctx, obj) + }) + 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.(string) + fc.Result = res + return ec.marshalNString2string(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_HeartbeatMonitor_additionalDetails(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "HeartbeatMonitor", + Field: field, + IsMethod: true, + IsResolver: true, + 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) _IntegrationKey_id(ctx context.Context, field graphql.CollectedField, obj *integrationkey.IntegrationKey) (ret graphql.Marshaler) { fc, err := ec.fieldContext_IntegrationKey_id(ctx, field) if err != nil { @@ -13499,6 +13552,8 @@ func (ec *executionContext) fieldContext_Mutation_createHeartbeatMonitor(ctx con return ec.fieldContext_HeartbeatMonitor_lastHeartbeat(ctx, field) case "href": return ec.fieldContext_HeartbeatMonitor_href(ctx, field) + case "additionalDetails": + return ec.fieldContext_HeartbeatMonitor_additionalDetails(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type HeartbeatMonitor", field.Name) }, @@ -16664,6 +16719,8 @@ func (ec *executionContext) fieldContext_Query_heartbeatMonitor(ctx context.Cont return ec.fieldContext_HeartbeatMonitor_lastHeartbeat(ctx, field) case "href": return ec.fieldContext_HeartbeatMonitor_href(ctx, field) + case "additionalDetails": + return ec.fieldContext_HeartbeatMonitor_additionalDetails(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type HeartbeatMonitor", field.Name) }, @@ -21825,6 +21882,8 @@ func (ec *executionContext) fieldContext_Service_heartbeatMonitors(ctx context.C return ec.fieldContext_HeartbeatMonitor_lastHeartbeat(ctx, field) case "href": return ec.fieldContext_HeartbeatMonitor_href(ctx, field) + case "additionalDetails": + return ec.fieldContext_HeartbeatMonitor_additionalDetails(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type HeartbeatMonitor", field.Name) }, @@ -28501,7 +28560,7 @@ func (ec *executionContext) unmarshalInputCreateHeartbeatMonitorInput(ctx contex asMap[k] = v } - fieldsInOrder := [...]string{"serviceID", "name", "timeoutMinutes"} + fieldsInOrder := [...]string{"serviceID", "name", "timeoutMinutes", "additionalDetails"} for _, k := range fieldsInOrder { v, ok := asMap[k] if !ok { @@ -28529,6 +28588,13 @@ func (ec *executionContext) unmarshalInputCreateHeartbeatMonitorInput(ctx contex return it, err } it.TimeoutMinutes = data + case "additionalDetails": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("additionalDetails")) + data, err := ec.unmarshalOString2áš–string(ctx, v) + if err != nil { + return it, err + } + it.AdditionalDetails = data } } @@ -30792,7 +30858,7 @@ func (ec *executionContext) unmarshalInputUpdateHeartbeatMonitorInput(ctx contex asMap[k] = v } - fieldsInOrder := [...]string{"id", "name", "timeoutMinutes"} + fieldsInOrder := [...]string{"id", "name", "timeoutMinutes", "additionalDetails"} for _, k := range fieldsInOrder { v, ok := asMap[k] if !ok { @@ -30820,6 +30886,13 @@ func (ec *executionContext) unmarshalInputUpdateHeartbeatMonitorInput(ctx contex return it, err } it.TimeoutMinutes = data + case "additionalDetails": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("additionalDetails")) + data, err := ec.unmarshalOString2áš–string(ctx, v) + if err != nil { + return it, err + } + it.AdditionalDetails = data } } @@ -33394,6 +33467,42 @@ func (ec *executionContext) _HeartbeatMonitor(ctx context.Context, sel ast.Selec continue } + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + case "additionalDetails": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._HeartbeatMonitor_additionalDetails(ctx, field, obj) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + if field.Deferrable != nil { + dfs, ok := deferred[field.Deferrable.Label] + di := 0 + if ok { + dfs.AddField(field) + di = len(dfs.Values) - 1 + } else { + dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) + deferred[field.Deferrable.Label] = dfs + } + dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { + return innerFunc(ctx, dfs) + }) + + // don't run the out.Concurrently() call below + out.Values[i] = graphql.Null + continue + } + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) default: panic("unknown field " + strconv.Quote(field.Name)) diff --git a/graphql2/graphqlapp/heartbeatmonitor.go b/graphql2/graphqlapp/heartbeatmonitor.go index 19d8de9291..76e028e150 100644 --- a/graphql2/graphqlapp/heartbeatmonitor.go +++ b/graphql2/graphqlapp/heartbeatmonitor.go @@ -22,6 +22,9 @@ func (a *HeartbeatMonitor) Href(ctx context.Context, hb *heartbeat.Monitor) (str cfg := config.FromContext(ctx) return cfg.CallbackURL("/api/v2/heartbeat/" + url.PathEscape(hb.ID)), nil } +func (a *HeartbeatMonitor) AdditionalDetails(ctx context.Context, hb *heartbeat.Monitor) (string, error) { + return hb.AddtionalDetails, nil +} func (q *Query) HeartbeatMonitor(ctx context.Context, id string) (*heartbeat.Monitor, error) { return (*App)(q).FindOneHeartbeatMonitor(ctx, id) @@ -33,10 +36,15 @@ func (m *Mutation) CreateHeartbeatMonitor(ctx context.Context, input graphql2.Cr serviceID = *input.ServiceID } err = withContextTx(ctx, m.DB, func(ctx context.Context, tx *sql.Tx) error { + var details string + if input.AdditionalDetails != nil { + details = *input.AdditionalDetails + } hb = &heartbeat.Monitor{ - ServiceID: serviceID, - Name: input.Name, - Timeout: time.Duration(input.TimeoutMinutes) * time.Minute, + ServiceID: serviceID, + Name: input.Name, + Timeout: time.Duration(input.TimeoutMinutes) * time.Minute, + AddtionalDetails: details, } hb, err = m.HeartbeatStore.CreateTx(ctx, tx, hb) return err @@ -56,6 +64,9 @@ func (m *Mutation) UpdateHeartbeatMonitor(ctx context.Context, input graphql2.Up if input.TimeoutMinutes != nil { hb.Timeout = time.Duration(*input.TimeoutMinutes) * time.Minute } + if input.AdditionalDetails != nil { + hb.AddtionalDetails = *input.AdditionalDetails + } return m.HeartbeatStore.UpdateTx(ctx, tx, hb) }) diff --git a/graphql2/models_gen.go b/graphql2/models_gen.go index 158277c60a..f93c768623 100644 --- a/graphql2/models_gen.go +++ b/graphql2/models_gen.go @@ -157,9 +157,10 @@ type CreateGQLAPIKeyInput struct { } type CreateHeartbeatMonitorInput struct { - ServiceID *string `json:"serviceID,omitempty"` - Name string `json:"name"` - TimeoutMinutes int `json:"timeoutMinutes"` + ServiceID *string `json:"serviceID,omitempty"` + Name string `json:"name"` + TimeoutMinutes int `json:"timeoutMinutes"` + AdditionalDetails *string `json:"additionalDetails,omitempty"` } type CreateIntegrationKeyInput struct { @@ -641,9 +642,10 @@ type UpdateGQLAPIKeyInput struct { } type UpdateHeartbeatMonitorInput struct { - ID string `json:"id"` - Name *string `json:"name,omitempty"` - TimeoutMinutes *int `json:"timeoutMinutes,omitempty"` + ID string `json:"id"` + Name *string `json:"name,omitempty"` + TimeoutMinutes *int `json:"timeoutMinutes,omitempty"` + AdditionalDetails *string `json:"additionalDetails,omitempty"` } type UpdateRotationInput struct { diff --git a/graphql2/schema.graphql b/graphql2/schema.graphql index 4f0cbfdf9f..63083bb78c 100644 --- a/graphql2/schema.graphql +++ b/graphql2/schema.graphql @@ -971,12 +971,14 @@ input CreateHeartbeatMonitorInput { serviceID: ID name: String! timeoutMinutes: Int! + additionalDetails: String } input UpdateHeartbeatMonitorInput { id: ID! name: String timeoutMinutes: Int + additionalDetails: String } enum HeartbeatMonitorState { @@ -993,6 +995,7 @@ type HeartbeatMonitor { lastState: HeartbeatMonitorState! lastHeartbeat: ISOTimestamp href: String! + additionalDetails: String! } type Label { diff --git a/web/src/schema.d.ts b/web/src/schema.d.ts index 1415e7daf8..d2e22efcef 100644 --- a/web/src/schema.d.ts +++ b/web/src/schema.d.ts @@ -198,6 +198,7 @@ export interface CreateGQLAPIKeyInput { } export interface CreateHeartbeatMonitorInput { + additionalDetails?: null | string name: string serviceID?: null | string timeoutMinutes: number @@ -395,6 +396,7 @@ export interface GQLAPIKeyUsage { } export interface HeartbeatMonitor { + additionalDetails: string href: string id: string lastHeartbeat?: null | ISOTimestamp @@ -1037,6 +1039,7 @@ export interface UpdateGQLAPIKeyInput { } export interface UpdateHeartbeatMonitorInput { + additionalDetails?: null | string id: string name?: null | string timeoutMinutes?: null | number From aa72e505f16fa389ea4ad9207f1f1542f6a111cd Mon Sep 17 00:00:00 2001 From: Nathaniel Caza Date: Tue, 26 Dec 2023 14:23:31 -0600 Subject: [PATCH 05/11] add missing comma --- heartbeat/store.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/heartbeat/store.go b/heartbeat/store.go index 4258206411..09f57ad6a5 100644 --- a/heartbeat/store.go +++ b/heartbeat/store.go @@ -66,7 +66,7 @@ func NewStore(ctx context.Context, db *sql.DB) (*Store, error) { update heartbeat_monitors set name = $2, - heartbeat_interval = $3 + heartbeat_interval = $3, additional_details = $4 where id = $1 `), From d8f5ed2e79bfd4b317e01258df537ce855f84826 Mon Sep 17 00:00:00 2001 From: Nathaniel Caza Date: Tue, 26 Dec 2023 14:23:49 -0600 Subject: [PATCH 06/11] update forms for new details --- .../app/services/HeartbeatMonitorCreateDialog.tsx | 7 ++++++- .../app/services/HeartbeatMonitorEditDialog.tsx | 2 ++ web/src/app/services/HeartbeatMonitorForm.tsx | 15 +++++++++++++++ 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/web/src/app/services/HeartbeatMonitorCreateDialog.tsx b/web/src/app/services/HeartbeatMonitorCreateDialog.tsx index d622b403b2..8deb15e4b9 100644 --- a/web/src/app/services/HeartbeatMonitorCreateDialog.tsx +++ b/web/src/app/services/HeartbeatMonitorCreateDialog.tsx @@ -17,7 +17,11 @@ export default function HeartbeatMonitorCreateDialog(props: { serviceID: string onClose: () => void }): JSX.Element { - const [value, setValue] = useState({ name: '', timeoutMinutes: 15 }) + const [value, setValue] = useState({ + name: '', + timeoutMinutes: 15, + additionalDetails: '', + }) const [createHeartbeatStatus, createHeartbeat] = useMutation(createMutation) return ( @@ -34,6 +38,7 @@ export default function HeartbeatMonitorCreateDialog(props: { name: value.name, timeoutMinutes: value.timeoutMinutes, serviceID: props.serviceID, + additionalDetails: value.additionalDetails, }, }, { additionalTypenames: ['HeartbeatMonitor', 'Service'] }, diff --git a/web/src/app/services/HeartbeatMonitorEditDialog.tsx b/web/src/app/services/HeartbeatMonitorEditDialog.tsx index 6a242977e4..518dfe07d1 100644 --- a/web/src/app/services/HeartbeatMonitorEditDialog.tsx +++ b/web/src/app/services/HeartbeatMonitorEditDialog.tsx @@ -17,6 +17,7 @@ const query = gql` id name timeoutMinutes + additionalDetails } } ` @@ -62,6 +63,7 @@ export default function HeartbeatMonitorEditDialog(props: { value || { name: data.heartbeatMonitor.name, timeoutMinutes: data.heartbeatMonitor.timeoutMinutes, + additionalDetails: data.heartbeatMonitor.additionalDetails, } } onChange={(value) => setValue(value)} diff --git a/web/src/app/services/HeartbeatMonitorForm.tsx b/web/src/app/services/HeartbeatMonitorForm.tsx index 6f2aac712c..3ff2f8dbae 100644 --- a/web/src/app/services/HeartbeatMonitorForm.tsx +++ b/web/src/app/services/HeartbeatMonitorForm.tsx @@ -6,6 +6,8 @@ import { FieldError } from '../util/errutil' import { DurationField } from '../util/DurationField' import { Duration } from 'luxon' +const MaxDetailsLength = 6 * 1024 // 6KiB + function clampTimeout(val: string): number | string { if (!val) return '' const dur = Duration.fromISO(val) @@ -14,6 +16,7 @@ function clampTimeout(val: string): number | string { export interface Value { name: string timeoutMinutes: number + additionalDetails: string } interface HeartbeatMonitorFormProps { value: Value @@ -30,6 +33,7 @@ export default function HeartbeatMonitorForm( props: HeartbeatMonitorFormProps, ): JSX.Element { const { ...formProps } = props + console.log(props.value) return ( @@ -59,6 +63,17 @@ export default function HeartbeatMonitorForm( mapOnChangeValue={clampTimeout} /> + + + ) From c9346c53445ab8ae73da9260f37687cee10cc884 Mon Sep 17 00:00:00 2001 From: Nathaniel Caza Date: Tue, 26 Dec 2023 14:24:02 -0600 Subject: [PATCH 07/11] fix char count bug in FormField --- web/src/app/forms/FormField.js | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/web/src/app/forms/FormField.js b/web/src/app/forms/FormField.js index a14ff82a62..96041e8ada 100644 --- a/web/src/app/forms/FormField.js +++ b/web/src/app/forms/FormField.js @@ -110,7 +110,7 @@ export function FormField(props) { } // wraps hints/errors within a grid containing character counter to align horizontally - function charCountWrapper(component, count) { + function charCountWrapper(component, count, fieldName) { return ( @@ -118,14 +118,14 @@ export function FormField(props) { - {value.description.length}/{count} + {value[fieldName].length}/{count} ) } - function renderFormHelperText(error, hint, count) { + function renderFormHelperText(error, hint, count, fieldName) { // handle optional count parameter if (count === undefined) { count = 0 @@ -148,7 +148,7 @@ export function FormField(props) { ) if (count) { - return charCountWrapper(errorText, count) + return charCountWrapper(errorText, count, fieldName) } return errorText } @@ -156,7 +156,11 @@ export function FormField(props) { if (hint) { if (count) { - return charCountWrapper({hint}, count) + return charCountWrapper( + {hint}, + count, + fieldName, + ) } return {hint} } @@ -181,7 +185,12 @@ export function FormField(props) { > {fieldProps.children} - {renderFormHelperText(fieldProps.error, fieldProps.hint, charCount)} + {renderFormHelperText( + fieldProps.error, + fieldProps.hint, + charCount, + fieldProps.name, + )} ) } From 41e3fd9e8a5c9a073ac00469655e63b9fa38c98e Mon Sep 17 00:00:00 2001 From: Nathaniel Caza Date: Wed, 27 Dec 2023 09:32:27 -0600 Subject: [PATCH 08/11] update db schema --- migrate/schema.sql | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/migrate/schema.sql b/migrate/schema.sql index cacddd22b7..6ceba8d641 100644 --- a/migrate/schema.sql +++ b/migrate/schema.sql @@ -1,7 +1,7 @@ -- This file is auto-generated by "make db-schema"; DO NOT EDIT --- DATA=5064b6e0d3f391bf44fee4dfa88da2b83e03cbf7ead5b9380529ce9c0d2c3460 - --- DISK=ad8fee2ce10b4f87e0ea8620202aa04b07c5a53774089ced9c3cb6c880747963 - --- PSQL=ad8fee2ce10b4f87e0ea8620202aa04b07c5a53774089ced9c3cb6c880747963 - +-- DATA=4937bf25a982871c2843e99ba8b1c9fad81590c1370069c7f23f331edd05cbfd - +-- DISK=9d1b013e2326cc3bb5be8ede57f83706e990af0767a432820bbc09a641170311 - +-- PSQL=9d1b013e2326cc3bb5be8ede57f83706e990af0767a432820bbc09a641170311 - -- -- pgdump-lite database dump -- @@ -1641,6 +1641,7 @@ CREATE UNIQUE INDEX gql_api_keys_pkey ON public.gql_api_keys USING btree (id); CREATE TABLE heartbeat_monitors ( + additional_details text, heartbeat_interval interval NOT NULL, id uuid NOT NULL, last_heartbeat timestamp with time zone, From 57b4111b7fb06242367e17e366e4f582f7f8ed03 Mon Sep 17 00:00:00 2001 From: Nathaniel Caza Date: Thu, 28 Dec 2023 10:15:48 -0600 Subject: [PATCH 09/11] regen sqlc --- gadb/models.go | 1 + 1 file changed, 1 insertion(+) diff --git a/gadb/models.go b/gadb/models.go index 518331f23a..86a3ccbba8 100644 --- a/gadb/models.go +++ b/gadb/models.go @@ -928,6 +928,7 @@ type GqlApiKeyUsage struct { } type HeartbeatMonitor struct { + AdditionalDetails sql.NullString HeartbeatInterval int64 ID uuid.UUID LastHeartbeat sql.NullTime From 05621947a6c75254330ff4961665ee62268b1cd0 Mon Sep 17 00:00:00 2001 From: Nathaniel Caza Date: Mon, 15 Jan 2024 14:13:14 -0600 Subject: [PATCH 10/11] fix field typo --- graphql2/graphqlapp/heartbeatmonitor.go | 12 ++++++------ heartbeat/monitor.go | 6 +++--- heartbeat/store.go | 4 ++-- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/graphql2/graphqlapp/heartbeatmonitor.go b/graphql2/graphqlapp/heartbeatmonitor.go index 76e028e150..12d1ae4de7 100644 --- a/graphql2/graphqlapp/heartbeatmonitor.go +++ b/graphql2/graphqlapp/heartbeatmonitor.go @@ -23,7 +23,7 @@ func (a *HeartbeatMonitor) Href(ctx context.Context, hb *heartbeat.Monitor) (str return cfg.CallbackURL("/api/v2/heartbeat/" + url.PathEscape(hb.ID)), nil } func (a *HeartbeatMonitor) AdditionalDetails(ctx context.Context, hb *heartbeat.Monitor) (string, error) { - return hb.AddtionalDetails, nil + return hb.AdditionalDetails, nil } func (q *Query) HeartbeatMonitor(ctx context.Context, id string) (*heartbeat.Monitor, error) { @@ -41,10 +41,10 @@ func (m *Mutation) CreateHeartbeatMonitor(ctx context.Context, input graphql2.Cr details = *input.AdditionalDetails } hb = &heartbeat.Monitor{ - ServiceID: serviceID, - Name: input.Name, - Timeout: time.Duration(input.TimeoutMinutes) * time.Minute, - AddtionalDetails: details, + ServiceID: serviceID, + Name: input.Name, + Timeout: time.Duration(input.TimeoutMinutes) * time.Minute, + AdditionalDetails: details, } hb, err = m.HeartbeatStore.CreateTx(ctx, tx, hb) return err @@ -65,7 +65,7 @@ func (m *Mutation) UpdateHeartbeatMonitor(ctx context.Context, input graphql2.Up hb.Timeout = time.Duration(*input.TimeoutMinutes) * time.Minute } if input.AdditionalDetails != nil { - hb.AddtionalDetails = *input.AdditionalDetails + hb.AdditionalDetails = *input.AdditionalDetails } return m.HeartbeatStore.UpdateTx(ctx, tx, hb) diff --git a/heartbeat/monitor.go b/heartbeat/monitor.go index 4736cd19f6..1b6f80f5c1 100644 --- a/heartbeat/monitor.go +++ b/heartbeat/monitor.go @@ -16,7 +16,7 @@ type Monitor struct { ServiceID string `json:"service_id,omitempty"` Timeout time.Duration `json:"timeout,omitempty"` - AddtionalDetails string + AdditionalDetails string lastState State lastHeartbeat time.Time @@ -34,7 +34,7 @@ func (m Monitor) Normalize() (*Monitor, error) { validate.UUID("ServiceID", m.ServiceID), validate.IDName("Name", m.Name), validate.Duration("Timeout", m.Timeout, 5*time.Minute, 9000*time.Hour), - validate.Text("AdditionalDetails", m.AddtionalDetails, 0, alert.MaxDetailsLength), + validate.Text("AdditionalDetails", m.AdditionalDetails, 0, alert.MaxDetailsLength), ) if err != nil { return nil, err @@ -51,7 +51,7 @@ func (m *Monitor) scanFrom(scanFn func(...interface{}) error) error { timeout pgtype.Interval ) - err := scanFn(&m.ID, &m.Name, &m.ServiceID, &timeout, &m.lastState, &t, &m.AddtionalDetails) + err := scanFn(&m.ID, &m.Name, &m.ServiceID, &timeout, &m.lastState, &t, &m.AdditionalDetails) if err != nil { return err } diff --git a/heartbeat/store.go b/heartbeat/store.go index 09f57ad6a5..2a9c1bf577 100644 --- a/heartbeat/store.go +++ b/heartbeat/store.go @@ -99,7 +99,7 @@ func (s *Store) CreateTx(ctx context.Context, tx *sql.Tx, m *Monitor) (*Monitor, n.ID = uuid.New().String() n.lastState = StateInactive - _, err = tx.StmtContext(ctx, s.create).ExecContext(ctx, n.ID, n.Name, n.ServiceID, &timeout, m.AddtionalDetails) + _, err = tx.StmtContext(ctx, s.create).ExecContext(ctx, n.ID, n.Name, n.ServiceID, &timeout, m.AdditionalDetails) if err != nil { return nil, err } @@ -170,7 +170,7 @@ func (s *Store) UpdateTx(ctx context.Context, tx *sql.Tx, m *Monitor) error { return err } - _, err = stmt.ExecContext(ctx, n.ID, n.Name, &timeout, m.AddtionalDetails) + _, err = stmt.ExecContext(ctx, n.ID, n.Name, &timeout, m.AdditionalDetails) return err } From f2527ba3598485ef221486803cce156ca871499f Mon Sep 17 00:00:00 2001 From: Nathaniel Caza Date: Thu, 25 Jan 2024 09:24:32 -0600 Subject: [PATCH 11/11] make generate --- graphql2/generated.go | 44 ++++++------------------------------------- 1 file changed, 6 insertions(+), 38 deletions(-) diff --git a/graphql2/generated.go b/graphql2/generated.go index b65aa54fdc..b16d2a1782 100644 --- a/graphql2/generated.go +++ b/graphql2/generated.go @@ -802,7 +802,6 @@ type HeartbeatMonitorResolver interface { TimeoutMinutes(ctx context.Context, obj *heartbeat.Monitor) (int, error) Href(ctx context.Context, obj *heartbeat.Monitor) (string, error) - AdditionalDetails(ctx context.Context, obj *heartbeat.Monitor) (string, error) } type IntegrationKeyResolver interface { Type(ctx context.Context, obj *integrationkey.IntegrationKey) (IntegrationKeyType, error) @@ -12791,7 +12790,7 @@ func (ec *executionContext) _HeartbeatMonitor_additionalDetails(ctx context.Cont }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return ec.resolvers.HeartbeatMonitor().AdditionalDetails(rctx, obj) + return obj.AdditionalDetails, nil }) if err != nil { ec.Error(ctx, err) @@ -12812,8 +12811,8 @@ func (ec *executionContext) fieldContext_HeartbeatMonitor_additionalDetails(ctx fc = &graphql.FieldContext{ Object: "HeartbeatMonitor", Field: field, - IsMethod: true, - IsResolver: true, + 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") }, @@ -36500,41 +36499,10 @@ func (ec *executionContext) _HeartbeatMonitor(ctx context.Context, sel ast.Selec out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) case "additionalDetails": - field := field - - innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - } - }() - res = ec._HeartbeatMonitor_additionalDetails(ctx, field, obj) - if res == graphql.Null { - atomic.AddUint32(&fs.Invalids, 1) - } - return res - } - - if field.Deferrable != nil { - dfs, ok := deferred[field.Deferrable.Label] - di := 0 - if ok { - dfs.AddField(field) - di = len(dfs.Values) - 1 - } else { - dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) - deferred[field.Deferrable.Label] = dfs - } - dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { - return innerFunc(ctx, dfs) - }) - - // don't run the out.Concurrently() call below - out.Values[i] = graphql.Null - continue + out.Values[i] = ec._HeartbeatMonitor_additionalDetails(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) } - - out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) default: panic("unknown field " + strconv.Quote(field.Name)) }