|
package views |
|
|
|
import ( |
|
"context" |
|
"fmt" |
|
|
|
"github.com/incident-io/core/server/api/gen/common" |
|
"github.com/incident-io/core/server/api/gen/escalations" |
|
"github.com/incident-io/core/server/pkg/domain" |
|
"github.com/incident-io/core/server/pkg/errors" |
|
"github.com/incident-io/core/server/pkg/log" |
|
"github.com/incident-io/core/server/pkg/partial" |
|
"github.com/incident-io/core/server/pkg/rbac" |
|
"github.com/incident-io/core/server/pkg/service/escalatorv2" |
|
"github.com/incident-io/core/server/pkg/slackv2/components" |
|
"github.com/incident-io/core/server/pkg/slackv2/fixtures" |
|
slackpreview "github.com/incident-io/core/server/pkg/slackv2/preview" |
|
"github.com/incident-io/core/server/pkg/slackv2/typeaheads" |
|
"github.com/slack-go/slack" |
|
"gorm.io/gorm" |
|
|
|
"github.com/incident-io/core/server/pkg/slackv2/views/modal" |
|
. "github.com/incident-io/core/server/pkg/slackv2/views/modal" |
|
) |
|
|
|
func init() { |
|
Register(func(render func(modal EscalateSplunkOnCall, props EscalateSplunkOnCallProps, state EscalateSplunkOnCallState) *slack.ModalViewRequest) slackpreview.Template { |
|
buildProps := func(incTweaks partial.Partial[domain.Incident]) EscalateSplunkOnCallProps { |
|
props := EscalateSplunkOnCallProps{ |
|
Incident: incTweaks.Apply(*fixtures.IncidentStandard), |
|
} |
|
|
|
return props |
|
} |
|
|
|
return slackpreview.Template{ |
|
Name: "Modal: Escalate: Splunk On-Call", |
|
Description: "Escalate to Splunk On-Call", |
|
Variants: []slackpreview.Variant{ |
|
{ |
|
Name: "default", |
|
BlockKit: render( |
|
EscalateSplunkOnCall{}, |
|
buildProps(domain.IncidentBuilder()), |
|
EscalateSplunkOnCallState{}, |
|
), |
|
}, |
|
{ |
|
Name: "private incident", |
|
BlockKit: render( |
|
EscalateSplunkOnCall{}, |
|
buildProps(domain.IncidentBuilder( |
|
domain.IncidentBuilder.IsPrivate(true), |
|
)), |
|
EscalateSplunkOnCallState{}, |
|
), |
|
}, |
|
}, |
|
} |
|
}) |
|
} |
|
|
|
type EscalateSplunkOnCall struct { |
|
IncidentID string `json:"incident_id"` |
|
IdempotencyKey string `json:"idempotency_key"` |
|
} |
|
|
|
type EscalateSplunkOnCallProps struct { |
|
Incident *domain.Incident |
|
} |
|
|
|
type EscalateSplunkOnCallState struct { |
|
Description *string `modal:"description.value"` |
|
Teams *[]slack.OptionBlockObject `modal:"teams.value"` |
|
Policies *[]slack.OptionBlockObject `modal:"policies.value"` |
|
Users *[]slack.OptionBlockObject `modal:"users.value"` |
|
} |
|
|
|
func (m EscalateSplunkOnCall) CallbackID() string { |
|
return "EscalateSplunkOnCall" |
|
} |
|
|
|
func (m EscalateSplunkOnCall) DefaultState() *EscalateSplunkOnCallState { |
|
return &EscalateSplunkOnCallState{} |
|
} |
|
|
|
func (m EscalateSplunkOnCall) BuildProps(ctx context.Context, db *gorm.DB, identity *rbac.Identity, state *EscalateSplunkOnCallState) (EscalateSplunkOnCallProps, error) { |
|
inc, err := GetIncident(ctx, db, identity.Organisation, m.IncidentID) |
|
if err != nil { |
|
return EscalateSplunkOnCallProps{}, err |
|
} |
|
|
|
return EscalateSplunkOnCallProps{ |
|
Incident: inc, |
|
}, nil |
|
} |
|
|
|
func (m EscalateSplunkOnCall) OnAction(ctx context.Context, db *gorm.DB, identity *rbac.Identity, deps Dependencies, props EscalateSplunkOnCallProps, state *EscalateSplunkOnCallState, cb slack.InteractionCallback) (bool, error) { |
|
return false, nil |
|
} |
|
|
|
func (m EscalateSplunkOnCall) OnSubmit(ctx context.Context, db *gorm.DB, identity *rbac.Identity, deps Dependencies, props EscalateSplunkOnCallProps, state *EscalateSplunkOnCallState) (resp *slack.ViewSubmissionResponse, err error) { |
|
if state.Policies == nil { |
|
state.Policies = new([]slack.OptionBlockObject) |
|
} |
|
if state.Teams == nil { |
|
state.Teams = new([]slack.OptionBlockObject) |
|
} |
|
if state.Users == nil { |
|
state.Users = new([]slack.OptionBlockObject) |
|
} |
|
|
|
if len(*state.Policies)+len(*state.Teams)+len(*state.Users) == 0 { |
|
return nil, errors.InvalidValue( |
|
"policies", errors.New("Must provide at least one policy, team or user"), |
|
) |
|
} |
|
|
|
targets := []*common.EscalationTarget{} |
|
for _, policy := range *state.Policies { |
|
targets = append(targets, &common.EscalationTarget{ |
|
Type: "EscalationPolicy", |
|
ID: policy.Value, |
|
}) |
|
} |
|
for _, team := range *state.Teams { |
|
targets = append(targets, &common.EscalationTarget{ |
|
Type: "Team", |
|
ID: team.Value, |
|
}) |
|
} |
|
for _, user := range *state.Users { |
|
targets = append(targets, &common.EscalationTarget{ |
|
Type: "User", |
|
ID: user.Value, |
|
}) |
|
} |
|
|
|
payload := &escalations.CreatePayload{ |
|
IncidentID: m.IncidentID, |
|
IdempotencyKey: &m.IdempotencyKey, |
|
Provider: string(domain.EscalationProviderSplunkOnCall), |
|
Title: &props.Incident.Name, |
|
Description: state.Description, |
|
Targets: targets, |
|
} |
|
|
|
log.Info(ctx, "Creating escalation with payload", map[string]any{ |
|
"escalation_payload": payload, |
|
}) |
|
escalation, err := escalatorv2.Create(ctx, db, deps.EventService, identity, payload) |
|
if err != nil { |
|
return nil, errors.Wrap(err, "creating escalation") |
|
} |
|
|
|
log.Info(ctx, "Created escalation", map[string]any{ |
|
"escalation": escalation.ID, |
|
}) |
|
|
|
return nil, nil // close modal |
|
} |
|
|
|
func (m EscalateSplunkOnCall) Render(props EscalateSplunkOnCallProps, state *EscalateSplunkOnCallState, isFirstRender bool) ModalViewRequest { |
|
blocks := []slack.Block{} |
|
|
|
// Why do you need them? |
|
{ |
|
element := slack.NewPlainTextInputBlockElement(components.TextPlain("The database is having issues"), "value") |
|
if props.Incident.Summary.Valid && props.Incident.Summary.String != "" { |
|
element.InitialValue = props.Incident.Summary.String |
|
} else { |
|
element.InitialValue = fmt.Sprintf("Escalating from incident %s", props.Incident.Name) |
|
} |
|
|
|
block := slack.NewInputBlock("description", components.TextPlain("Why do you need them?"), element) |
|
block.Hint = components.TextPlain( |
|
"This will be what they see/hear when Splunk On-Call contacts them. Make it something a robot can pronounce!") |
|
|
|
blocks = append(blocks, block) |
|
} |
|
|
|
// Which policies? |
|
{ |
|
element := slack.NewOptionsMultiSelectBlockElement(slack.MultiOptTypeExternal, nil, "value") |
|
element.MinQueryLength = new(int) // set this to zero so we get options without needing to type anything |
|
|
|
block := slack.NewInputBlock( |
|
"policies", components.TextPlain("Which policies?"), element) |
|
block.Optional = true |
|
block.Hint = components.TextPlain("Pick an escalation policy to decide who is paged.") |
|
|
|
blockWithSuggestion := modal.SuggestionHandler(block, |
|
func(ctx context.Context, db *gorm.DB, identity *rbac.Identity, searchString string) ([]*slack.OptionBlockObject, error) { |
|
return typeaheads.SplunkOnCallEscalationPolicies(ctx, db, identity.Organisation, nil, searchString) |
|
}) |
|
|
|
blocks = append(blocks, blockWithSuggestion) |
|
} |
|
|
|
// Which teams? |
|
{ |
|
element := slack.NewOptionsMultiSelectBlockElement(slack.MultiOptTypeExternal, nil, "value") |
|
element.MinQueryLength = new(int) // set this to zero so we get options without needing to type anything |
|
|
|
block := slack.NewInputBlock( |
|
"teams", components.TextPlain("Which teams?"), element) |
|
block.Optional = true |
|
block.Hint = components.TextPlain("Or choose a team to page directly.") |
|
|
|
blockWithSuggestion := SuggestionHandler(block, |
|
func(ctx context.Context, db *gorm.DB, identity *rbac.Identity, searchString string) ([]*slack.OptionBlockObject, error) { |
|
return typeaheads.SplunkOnCallTeams(ctx, db, identity.Organisation, nil, searchString) |
|
}) |
|
|
|
blocks = append(blocks, blockWithSuggestion) |
|
} |
|
|
|
// Which users? |
|
{ |
|
element := slack.NewOptionsMultiSelectBlockElement(slack.MultiOptTypeExternal, nil, "value") |
|
element.MinQueryLength = new(int) // set this to zero so we get options without needing to type anything |
|
|
|
block := slack.NewInputBlock( |
|
"users", components.TextPlain("Who do you need?"), element) |
|
block.Optional = true |
|
block.Hint = components.TextPlain("Or a selection of users, if you need someone specific.") |
|
|
|
blockWithSuggestion := modal.SuggestionHandler(block, |
|
func(ctx context.Context, db *gorm.DB, identity *rbac.Identity, searchString string) ([]*slack.OptionBlockObject, error) { |
|
return typeaheads.SplunkOnCallUsers(ctx, db, identity.Organisation, nil, searchString) |
|
}) |
|
|
|
blocks = append(blocks, blockWithSuggestion) |
|
} |
|
|
|
return ModalViewRequest{ |
|
Title: components.TextPlain("Escalate"), |
|
Close: components.TextPlain("Cancel"), |
|
Submit: components.TextPlain("Escalate"), |
|
Blocks: slack.Blocks{ |
|
BlockSet: blocks, |
|
}, |
|
} |
|
} |