Skip to content

Commit

Permalink
Merge branch 'master' of github.com:target/goalert into user-cm-form-…
Browse files Browse the repository at this point in the history
…dest-ui
  • Loading branch information
tony-tvu committed Feb 7, 2024
2 parents 173d50d + 5951311 commit c0aee79
Show file tree
Hide file tree
Showing 14 changed files with 555 additions and 128 deletions.
384 changes: 354 additions & 30 deletions graphql2/generated.go

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion graphql2/graph/_directives.graphqls
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,4 @@ directive @goField(
# Experimental fields are subject to change/removal without warning.
directive @experimental(
flagName: String! # the name of the feature flag to use to enable this field
) on FIELD_DEFINITION
) on FIELD_DEFINITION | INPUT_FIELD_DEFINITION
3 changes: 3 additions & 0 deletions graphql2/graph/destinations.graphqls
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,9 @@ type DestinationTypeInfo {
isContactMethod: Boolean! # this destination type can be used as a user contact method
isEPTarget: Boolean! # this destination type can be used as an escalation policy step action
isSchedOnCallNotify: Boolean! # this destination type can be used for schedule on-call notifications

supportsStatusUpdates: Boolean! # if true, the destination type supports status updates
statusUpdatesRequired: Boolean! # if true, the destination type requires status updates to be enabled
}

type DestinationFieldConfig {
Expand Down
25 changes: 25 additions & 0 deletions graphql2/graphqlapp/compat.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package graphqlapp

import (
"github.com/target/goalert/graphql2"
"github.com/target/goalert/user/contactmethod"
)

// CompatDestToCMTypeVal converts a graphql2.DestinationInput to a contactmethod.Type and string value
// for the built-in destination types.
func CompatDestToCMTypeVal(d graphql2.DestinationInput) (contactmethod.Type, string) {
switch d.Type {
case destTwilioSMS:
return contactmethod.TypeSMS, d.FieldValue(fieldPhoneNumber)
case destTwilioVoice:
return contactmethod.TypeVoice, d.FieldValue(fieldPhoneNumber)
case destSMTP:
return contactmethod.TypeEmail, d.FieldValue(fieldEmailAddress)
case destWebhook:
return contactmethod.TypeWebhook, d.FieldValue(fieldWebhookURL)
case destSlackDM:
return contactmethod.TypeSlackDM, d.FieldValue(fieldSlackUserID)
}

return "", ""
}
66 changes: 60 additions & 6 deletions graphql2/graphqlapp/contactmethod.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,48 @@ func (a *App) UserContactMethod() graphql2.UserContactMethodResolver {
return (*ContactMethod)(a)
}

func (a *ContactMethod) Dest(ctx context.Context, obj *contactmethod.ContactMethod) (*graphql2.Destination, error) {
switch obj.Type {
case contactmethod.TypeSMS:
return &graphql2.Destination{
Type: destTwilioSMS,
Values: []graphql2.FieldValuePair{
{FieldID: fieldPhoneNumber, Value: obj.Value, Label: a.FormatDestFunc(ctx, notification.DestTypeSMS, obj.Value)},
},
}, nil
case contactmethod.TypeVoice:
return &graphql2.Destination{
Type: destTwilioVoice,
Values: []graphql2.FieldValuePair{
{FieldID: fieldPhoneNumber, Value: obj.Value, Label: a.FormatDestFunc(ctx, notification.DestTypeVoice, obj.Value)},
},
}, nil
case contactmethod.TypeEmail:
return &graphql2.Destination{
Type: destSMTP,
Values: []graphql2.FieldValuePair{
{FieldID: fieldEmailAddress, Value: obj.Value, Label: a.FormatDestFunc(ctx, notification.DestTypeUserEmail, obj.Value)},
},
}, nil
case contactmethod.TypeWebhook:
return &graphql2.Destination{
Type: destWebhook,
Values: []graphql2.FieldValuePair{
{FieldID: fieldWebhookURL, Value: obj.Value, Label: a.FormatDestFunc(ctx, notification.DestTypeUserWebhook, obj.Value)},
},
}, nil
case contactmethod.TypeSlackDM:
return &graphql2.Destination{
Type: destSlackDM,
Values: []graphql2.FieldValuePair{
{FieldID: fieldSlackUserID, Value: obj.Value, Label: a.FormatDestFunc(ctx, notification.DestTypeSlackChannel, obj.Value)},
},
}, nil
}

return nil, validation.NewGenericError("unsupported data type")
}

func (a *ContactMethod) Value(ctx context.Context, obj *contactmethod.ContactMethod) (string, error) {
if obj.Type != contactmethod.TypeWebhook {
return obj.Value, nil
Expand Down Expand Up @@ -96,15 +138,25 @@ func (m *Mutation) CreateUserContactMethod(ctx context.Context, input graphql2.C
var cm *contactmethod.ContactMethod
cfg := config.FromContext(ctx)

if input.Type == contactmethod.TypeWebhook && !cfg.ValidWebhookURL(input.Value) {
if input.Dest != nil {
t, v := CompatDestToCMTypeVal(*input.Dest)
input.Type = &t
input.Value = &v
}

if input.Type == nil || input.Value == nil {
return nil, validation.NewFieldError("dest", "must be provided (or type and value)")
}

if *input.Type == contactmethod.TypeWebhook && !cfg.ValidWebhookURL(*input.Value) {
return nil, validation.NewFieldError("value", "URL not allowed by administrator")
}

if input.Type == contactmethod.TypeSlackDM {
if strings.HasPrefix(input.Value, "@") {
if *input.Type == contactmethod.TypeSlackDM {
if strings.HasPrefix(*input.Value, "@") {
return nil, validation.NewFieldError("value", "Use 'Copy member ID' from your Slack profile to get your user ID.")
}
formatted := m.FormatDestFunc(ctx, notification.DestTypeSlackDM, input.Value)
formatted := m.FormatDestFunc(ctx, notification.DestTypeSlackDM, *input.Value)
if !strings.HasPrefix(formatted, "@") {
return nil, validation.NewFieldError("value", "Not a valid Slack user ID")
}
Expand All @@ -114,10 +166,12 @@ func (m *Mutation) CreateUserContactMethod(ctx context.Context, input graphql2.C
var err error
cm, err = m.CMStore.Create(ctx, tx, &contactmethod.ContactMethod{
Name: input.Name,
Type: input.Type,
Type: *input.Type,
UserID: input.UserID,
Value: input.Value,
Value: *input.Value,
Disabled: true,

StatusUpdates: input.EnableStatusUpdates != nil && *input.EnableStatusUpdates,
})
if err != nil {
return err
Expand Down
79 changes: 44 additions & 35 deletions graphql2/graphqlapp/destinationtypes.go
Original file line number Diff line number Diff line change
Expand Up @@ -192,12 +192,13 @@ func (q *Query) DestinationTypes(ctx context.Context) ([]graphql2.DestinationTyp
cfg := config.FromContext(ctx)
types := []graphql2.DestinationTypeInfo{
{
Type: destTwilioSMS,
Name: "Text Message (SMS)",
Enabled: cfg.Twilio.Enable,
DisabledMessage: "Twilio must be configured by an administrator",
UserDisclaimer: cfg.General.NotificationDisclaimer,
IsContactMethod: true,
Type: destTwilioSMS,
Name: "Text Message (SMS)",
Enabled: cfg.Twilio.Enable,
DisabledMessage: "Twilio must be configured by an administrator",
UserDisclaimer: cfg.General.NotificationDisclaimer,
SupportsStatusUpdates: true,
IsContactMethod: true,
RequiredFields: []graphql2.DestinationFieldConfig{{
FieldID: fieldPhoneNumber,
LabelSingular: "Phone Number",
Expand All @@ -210,12 +211,13 @@ func (q *Query) DestinationTypes(ctx context.Context) ([]graphql2.DestinationTyp
}},
},
{
Type: destTwilioVoice,
Name: "Voice Call",
Enabled: cfg.Twilio.Enable,
DisabledMessage: "Twilio must be configured by an administrator",
UserDisclaimer: cfg.General.NotificationDisclaimer,
IsContactMethod: true,
Type: destTwilioVoice,
Name: "Voice Call",
Enabled: cfg.Twilio.Enable,
DisabledMessage: "Twilio must be configured by an administrator",
UserDisclaimer: cfg.General.NotificationDisclaimer,
IsContactMethod: true,
SupportsStatusUpdates: true,
RequiredFields: []graphql2.DestinationFieldConfig{{
FieldID: fieldPhoneNumber,
LabelSingular: "Phone Number",
Expand All @@ -228,11 +230,12 @@ func (q *Query) DestinationTypes(ctx context.Context) ([]graphql2.DestinationTyp
}},
},
{
Type: destSMTP,
Name: "Email",
Enabled: cfg.SMTP.Enable,
IsContactMethod: true,
DisabledMessage: "SMTP must be configured by an administrator",
Type: destSMTP,
Name: "Email",
Enabled: cfg.SMTP.Enable,
IsContactMethod: true,
SupportsStatusUpdates: true,
DisabledMessage: "SMTP must be configured by an administrator",
RequiredFields: []graphql2.DestinationFieldConfig{{
FieldID: fieldEmailAddress,
LabelSingular: "Email Address",
Expand All @@ -243,13 +246,15 @@ func (q *Query) DestinationTypes(ctx context.Context) ([]graphql2.DestinationTyp
}},
},
{
Type: destWebhook,
Name: "Webhook",
Enabled: cfg.Webhook.Enable,
IsContactMethod: true,
IsEPTarget: true,
IsSchedOnCallNotify: true,
DisabledMessage: "Webhooks must be enabled by an administrator",
Type: destWebhook,
Name: "Webhook",
Enabled: cfg.Webhook.Enable,
IsContactMethod: true,
IsEPTarget: true,
IsSchedOnCallNotify: true,
SupportsStatusUpdates: true,
StatusUpdatesRequired: true,
DisabledMessage: "Webhooks must be enabled by an administrator",
RequiredFields: []graphql2.DestinationFieldConfig{{
FieldID: fieldWebhookURL,
LabelSingular: "Webhook URL",
Expand All @@ -262,11 +267,13 @@ func (q *Query) DestinationTypes(ctx context.Context) ([]graphql2.DestinationTyp
}},
},
{
Type: destSlackDM,
Name: "Slack Message (DM)",
Enabled: cfg.Slack.Enable,
IsContactMethod: true,
DisabledMessage: "Slack must be enabled by an administrator",
Type: destSlackDM,
Name: "Slack Message (DM)",
Enabled: cfg.Slack.Enable,
IsContactMethod: true,
SupportsStatusUpdates: true,
StatusUpdatesRequired: true,
DisabledMessage: "Slack must be enabled by an administrator",
RequiredFields: []graphql2.DestinationFieldConfig{{
FieldID: fieldSlackUserID,
LabelSingular: "Slack User",
Expand All @@ -278,12 +285,14 @@ func (q *Query) DestinationTypes(ctx context.Context) ([]graphql2.DestinationTyp
}},
},
{
Type: destSlackChan,
Name: "Slack Channel",
Enabled: cfg.Slack.Enable,
IsEPTarget: true,
IsSchedOnCallNotify: true,
DisabledMessage: "Slack must be enabled by an administrator",
Type: destSlackChan,
Name: "Slack Channel",
Enabled: cfg.Slack.Enable,
IsEPTarget: true,
IsSchedOnCallNotify: true,
SupportsStatusUpdates: true,
StatusUpdatesRequired: true,
DisabledMessage: "Slack must be enabled by an administrator",
RequiredFields: []graphql2.DestinationFieldConfig{{
FieldID: fieldSlackChanID,
LabelSingular: "Slack Channel",
Expand Down
3 changes: 3 additions & 0 deletions graphql2/graphqlapp/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,9 @@ func (q *Query) Users(ctx context.Context, opts *graphql2.UserSearchOptions, fir
if opts.CMType != nil {
searchOpts.CMType = *opts.CMType
}
if opts.Dest != nil {
searchOpts.CMType, searchOpts.CMValue = CompatDestToCMTypeVal(*opts.Dest)
}
if opts.FavoritesOnly != nil {
searchOpts.FavoritesOnly = *opts.FavoritesOnly
}
Expand Down
31 changes: 18 additions & 13 deletions graphql2/models_gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

23 changes: 19 additions & 4 deletions graphql2/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -827,6 +827,7 @@ input UserSearchOptions {
omit: [ID!]
CMValue: String = ""
CMType: ContactMethodType
dest: DestinationInput @experimental(flagName: "dest-types")

# Include only favorited services in the results.
favoritesOnly: Boolean = false
Expand Down Expand Up @@ -1177,6 +1178,8 @@ type UserContactMethod {
id: ID!
type: ContactMethodType

dest: Destination! @experimental(flagName: "dest-types")

# User-defined label for this contact method.
name: String!
value: String!
Expand All @@ -1201,10 +1204,22 @@ enum StatusUpdateState {
input CreateUserContactMethodInput {
userID: ID!

type: ContactMethodType!
type: ContactMethodType

dest: DestinationInput @experimental(flagName: "dest-types")

name: String!
value: String!

# Only value or dest should be used at a time, never both.
value: String

newUserNotificationRule: CreateUserNotificationRuleInput

# If true, this contact method will receive status updates.
#
# Note: Some contact method types, like Slack, will always receive status
# updates and this value is ignored.
enableStatusUpdates: Boolean
}

input CreateUserNotificationRuleInput {
Expand All @@ -1217,11 +1232,11 @@ input UpdateUserContactMethodInput {
id: ID!

name: String
value: String
value: String @deprecated(reason: "Updating value is not supported, delete and create a new contact method instead.")

# If true, this contact method will receive status updates.
#
# Note: Some contact method types, like webhooks, will always receive status
# Note: Some contact method types, like Slack, will always receive status
# updates and this value is ignored.
enableStatusUpdates: Boolean
}
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
},
"devDependencies": {
"@apollo/client": "3.8.5",
"@babel/core": "7.23.6",
"@babel/core": "7.23.9",
"@babel/plugin-transform-modules-commonjs": "7.23.3",
"@dnd-kit/core": "6.0.8",
"@dnd-kit/sortable": "7.0.2",
Expand Down Expand Up @@ -63,7 +63,7 @@
"@types/chance": "1.1.4",
"@types/diff": "5.0.8",
"@types/glob": "8.1.0",
"@types/jest": "29.5.10",
"@types/jest": "29.5.12",
"@types/lodash": "4.14.202",
"@types/luxon": "3.4.2",
"@types/node": "20.10.5",
Expand Down
Loading

0 comments on commit c0aee79

Please sign in to comment.