TP-48512 | Implementation of RCA and tag migration (#296)

* TP-38709 | Merging the changes to master on the logfix

* TP-48512 | Added button element for RCA section and implemented fill rca details

* TP-48512 | Small fixes

* TP-48512 | adding unit tests

* TP-48512 | added unit tests

* TP-48512 | updated color code for rca card

* TP-48512	| Removed duplicate interface

* TP-48512	| Added one more unit test

* TP-48512 | added comments for jira link validation and update

* TP-48512 | Merging the changes to master on the logfix

# Conflicts:
#	cmd/app/handler/slack_handler.go

* TP-48512 | Added button element for RCA section and implemented fill rca details

# Conflicts:
#	common/util/common_util.go
#	common/util/constant.go
#	internal/processor/action/incident_resolve_action.go
#	internal/processor/action/incident_update_jira-links_action.go
#	internal/processor/action/incident_update_resolution_text_action.go
#	internal/processor/action/view/incident_resolution_text.go
#	internal/processor/action/view/incident_section.go
#	service/slack/slack_service.go

* TP-48512 | Small fixes

* TP-48512 | adding unit tests

* TP-48512 | added unit tests

# Conflicts:
#	Makefile
#	service/incident/incident_service_v2_interface.go

* TP-48512 | updated color code for rca card

* TP-48512	| Removed duplicate interface

* TP-48512	| Added one more unit test

* TP-48512 | added comments for jira link validation and update

* TP-48512 | Fixed merge conflicts

* TP-48512 | Fixed merge conflicts

* TP-48512 | Fixed merge conflicts

* TP-48512 | Added sql migration script for adding tags

* TP-48512 | Updated sql migration script for adding tags

* TP-48512 | Fixed merge conflicts and updated tags in sql migration script
This commit is contained in:
Ajay Devarakonda
2023-12-07 14:13:12 +05:30
committed by GitHub
parent 120d508a05
commit a62ecbe0a5
34 changed files with 1289 additions and 1149 deletions

View File

@@ -54,5 +54,7 @@ generatemocks:
cd $(CURDIR)/repository/rcaInput && minimock -i IRcaInputRepository -s _mock.go -o $(CURDIR)/mocks
cd $(CURDIR)/service/teamService && minimock -i ITeamServiceV2 -s _mock.go -o $(CURDIR)/mocks
cd $(CURDIR)/model/user && minimock -i IUserRepositoryInterface -s _mock.go -o $(CURDIR)/mocks
cd $(CURDIR)/model/tag && minimock -i ITagRepository -s _mock.go -o $(CURDIR)/mocks
cd $(CURDIR)/model/incident && minimock -i IIncidentRepository -s _mock.go -o $(CURDIR)/mocks
cd $(CURDIR)/pkg/monitoringService && minimock -i MonitoringServiceActions -s _mock.go -o $(CURDIR)/mocks
cd $(CURDIR)/service/krakatoa && minimock -i IKrakatoaService -s _mock.go -o $(CURDIR)/mocks

View File

@@ -90,8 +90,8 @@ func NewSlackHandler(gormClient *gorm.DB, socketModeClient *socketmode.Client) *
socketModeClient, incidentService, teamService, severityService, tagService, slackbotClient, incidentServiceV2, slackService, rcaService,
),
viewSubmissionProcessor: processor.NewViewSubmissionProcessor(
socketModeClient, incidentService, teamService, severityService, tagService, teamService, slackbotClient, gormClient, incidentServiceV2,
),
socketModeClient, incidentService, teamService, severityService, tagService, teamService, slackbotClient, gormClient,
rcaService, incidentServiceV2),
userChangeEventProcessor: processor.NewUserChangeEventProcessor(
socketModeClient, userService,
),

View File

@@ -238,6 +238,40 @@ func ConvertSliceToMapOfString(input []string) map[string]string {
return output
}
type CompareArrayResults struct {
UniqueElementsInArrayA []string
UniqueElementsInArrayB []string
CommonElements []string
}
// CompareAndGetStringArrayResults Takes two arrays as inputs and returns the common values of two arrays and unique values of both arrays
func CompareAndGetStringArrayResults(arrayA []string, arrayB []string) CompareArrayResults {
uniqueInA := make([]string, 0)
uniqueInB := make([]string, 0)
common := make([]string, 0)
map1 := make(map[string]bool)
map2 := make(map[string]bool)
for _, val := range arrayA {
map1[val] = true
}
for _, val := range arrayB {
map2[val] = true
}
for key := range map2 {
if _, ok := map1[key]; !ok {
uniqueInB = append(uniqueInB, key) //if element not present in map2 append elements in toBeAdded slice
} else {
common = append(common, key) // Add common elements
}
}
for key := range map1 {
if _, ok := map2[key]; !ok {
uniqueInA = append(uniqueInA, key) //if element not present in map2 append elements in toBeAdded slice
}
}
return CompareArrayResults{uniqueInA, uniqueInB, common}
}
func IsBlank(input string) bool {
trimmedInput := strings.TrimSpace(input)
return trimmedInput == ""

View File

@@ -7,7 +7,6 @@ const (
ShowIncidents = "show_incidents"
HelpCommand = "help_button"
Incident = "incident"
Tags = "tags"
AssignIncidentRole = "assign_incident_role"
ResolveIncident = "resolve_incident"
SetIncidentStatus = "set_incident_status"
@@ -15,11 +14,11 @@ const (
SetIncidentSeverity = "set_incident_severity"
SetIncidentTitle = "set_incident_title"
SetIncidentDescription = "set_incident_description"
SetRCA = "set_rca"
SetRCADetails = "set_rca_details"
RCASection = "rca_section"
ShowRCADetails = "show_rca_details"
SetRCASummary = "set_rca_summary"
SetJiraLinks = "set_jira_links"
AddTags = "add_tags"
ShowTags = "show_tags"
RemoveTag = "remove_tags"
MarkIncidentDuplicate = "mark_incident_duplicate"
)
@@ -33,8 +32,8 @@ const (
SetIncidentDescriptionSubmit = "set_incident_description_submit"
SetIncidentSeveritySubmit = "set_incident_severity_submit"
SetIncidentTypeSubmit = "set_incident_type_submit"
UpdateTagSubmit = "updateTagSubmit"
SetIncidentRcaSubmit = "set_rca_submit"
SetIncidentRCADetailsSubmit = "set_rca_details_submit"
IncidentResolveSubmit = "resolve_incident_submit"
SetIncidentJiraLinksSubmit = "set_Jira_links_submit"
ShowIncidentSubmit = "show_incident_submit"
MarkIncidentDuplicateSubmit = "mark_incident_duplicate_submit"
@@ -61,8 +60,6 @@ const (
ConferenceMessage = "To discuss, use this *<%s|Meet link>*"
)
type DocumentServiceFileTypeKeys string
type ContentType string
const (
@@ -88,10 +85,26 @@ const (
RcaStatusPending = "PENDING"
)
// service names for uploading documents to cloud
type DocumentServiceFileTypeKeys string
// DocumentServiceProvider service names for uploading documents to cloud
const (
DocumentServiceProvider = "SA_DOCUMENT_SERVICE"
)
const (
JiraLinksLabel = "Jira link(s)"
RCASummaryLabel = "RCA summary"
RCADetailsLabel = "RCA details"
JiraIdSeparator = "/browse/"
)
// slack element types
const (
MarkDownElementType = "mrkdwn"
PlainTextType = "plain_text"
)
const (
IdParam = "id"
)

View File

@@ -0,0 +1,66 @@
alter table tag add column if not exists optional boolean not null default false;
alter table tag add column if not exists active boolean not null default false;
alter table tag add column if not exists display_order smallint NOT NULL DEFAULT 0;
alter table tag_value add column if not exists active boolean not null default false;
insert into tag ("name","label","place_holder","action_id","type","created_at","updated_at","deleted_at","active","optional","display_order")
values
('contributing_factors','Contributing factors','select contributing factors','contributing_factors_action_id','single_value','2023-12-01 07:10:20.550404','2023-12-01 07:10:20.550404',NULL,TRUE,FALSE,2),
('business_affected','Business affected','select business affected','business_affected_action_id','multi_value','2023-12-01 07:10:20.550404','2023-12-01 07:10:20.550404',NULL,TRUE,FALSE,1),
('additional_tags','Additional tags','select additional tags','additional_tags_action_id','multi_value','2023-12-01 07:10:20.550404','2023-12-01 07:10:20.550404',NULL,TRUE,TRUE,3);
insert into tag_value ("tag_id","value","create_at","updated_at","deleted_at","active")
values
(22,'Internal','2023-12-01 07:10:20.550404','2023-12-01 07:10:20.550404',NULL,TRUE),
(22,'Vendor','2023-12-01 07:10:20.550404','2023-12-01 07:10:20.550404',NULL,TRUE),
(23,'UPI','2023-12-01 07:10:20.550404','2023-12-01 07:10:20.550404',NULL,TRUE),
(23,'AMC','2023-12-01 07:10:20.550404','2023-12-01 07:10:20.550404',NULL,TRUE),
(23,'Rewards','2023-12-01 07:10:20.550404','2023-12-01 07:10:20.550404',NULL,TRUE),
(23,'Gold','2023-12-01 07:10:20.550404','2023-12-01 07:10:20.550404',NULL,TRUE),
(23,'GI','2023-12-01 07:10:20.550404','2023-12-01 07:10:20.550404',NULL,TRUE),
(23,'HL','2023-12-01 07:10:20.550404','2023-12-01 07:10:20.550404',NULL,TRUE),
(23,'PL','2023-12-01 07:10:20.550404','2023-12-01 07:10:20.550404',NULL,TRUE),
(24,'LoginPage','2023-12-01 07:10:20.550404','2023-12-01 07:10:20.550404',NULL,TRUE),
(24,'PL_BasicDetails','2023-12-01 07:10:20.550404','2023-12-01 07:10:20.550404',NULL,TRUE),
(24,'PL_Permission','2023-12-01 07:10:20.550404','2023-12-01 07:10:20.550404',NULL,TRUE),
(24,'PL_MFIScreen','2023-12-01 07:10:20.550404','2023-12-01 07:10:20.550404',NULL,TRUE),
(24,'PL_WorkDetails','2023-12-01 07:10:20.550404','2023-12-01 07:10:20.550404',NULL,TRUE),
(24,'PL_GenerateOffer','2023-12-01 07:10:20.550404','2023-12-01 07:10:20.550404',NULL,TRUE),
(24,'PL_PennyDrop','2023-12-01 07:10:20.550404','2023-12-01 07:10:20.550404',NULL,TRUE),
(24,'PL_AutoPaySetup','2023-12-01 07:10:20.550404','2023-12-01 07:10:20.550404',NULL,TRUE),
(24,'PL_KYCPage','2023-12-01 07:10:20.550404','2023-12-01 07:10:20.550404',NULL,TRUE),
(24,'PL_Disbursement','2023-12-01 07:10:20.550404','2023-12-01 07:10:20.550404',NULL,TRUE),
(24,'PL_SignAgreement','2023-12-01 07:10:20.550404','2023-12-01 07:10:20.550404',NULL,TRUE),
(24,'PL_Loan/Offer_Details','2023-12-01 07:10:20.550404','2023-12-01 07:10:20.550404',NULL,TRUE),
(24,'PL_PostPurchase','2023-12-01 07:10:20.550404','2023-12-01 07:10:20.550404',NULL,TRUE),
(24,'HL_Property_Details','2023-12-01 07:10:20.550404','2023-12-01 07:10:20.550404',NULL,TRUE),
(24,'HL_Employment_Details','2023-12-01 07:10:20.550404','2023-12-01 07:10:20.550404',NULL,TRUE),
(24,'HL_Document_Upload','2023-12-01 07:10:20.550404','2023-12-01 07:10:20.550404',NULL,TRUE),
(24,'HL_WorkDetails','2023-12-01 07:10:20.550404','2023-12-01 07:10:20.550404',NULL,TRUE),
(24,'HL_PersonalDetails','2023-12-01 07:10:20.550404','2023-12-01 07:10:20.550404',NULL,TRUE),
(24,'HL_IncomeVerification','2023-12-01 07:10:20.550404','2023-12-01 07:10:20.550404',NULL,TRUE),
(24,'HL_KYC','2023-12-01 07:10:20.550404','2023-12-01 07:10:20.550404',NULL,TRUE),
(24,'HL_CommunicationAdd','2023-12-01 07:10:20.550404','2023-12-01 07:10:20.550404',NULL,TRUE),
(24,'HL_MandateSetup','2023-12-01 07:10:20.550404','2023-12-01 07:10:20.550404',NULL,TRUE),
(24,'HL_Disbursement','2023-12-01 07:10:20.550404','2023-12-01 07:10:20.550404',NULL,TRUE),
(24,'HL_PostPurchase','2023-12-01 07:10:20.550404','2023-12-01 07:10:20.550404',NULL,TRUE),
(24,'AMC_KYC','2023-12-01 07:10:20.550404','2023-12-01 07:10:20.550404',NULL,TRUE),
(24,'AMC_PennyDrop','2023-12-01 07:10:20.550404','2023-12-01 07:10:20.550404',NULL,TRUE),
(24,'AMC_MandateSetup','2023-12-01 07:10:20.550404','2023-12-01 07:10:20.550404',NULL,TRUE),
(24,'AMC_PortfolioPage','2023-12-01 07:10:20.550404','2023-12-01 07:10:20.550404',NULL,TRUE),
(24,'AMC_SIPPage','2023-12-01 07:10:20.550404','2023-12-01 07:10:20.550404',NULL,TRUE),
(24,'AMC_OrdersPage','2023-12-01 07:10:20.550404','2023-12-01 07:10:20.550404',NULL,TRUE),
(24,'AMC_FundLandingPage','2023-12-01 07:10:20.550404','2023-12-01 07:10:20.550404',NULL,TRUE),
(24,'DG_KYC','2023-12-01 07:10:20.550404','2023-12-01 07:10:20.550404',NULL,TRUE),
(24,'DG_LandingPage','2023-12-01 07:10:20.550404','2023-12-01 07:10:20.550404',NULL,TRUE),
(24,'DG_TransactionPage','2023-12-01 07:10:20.550404','2023-12-01 07:10:20.550404',NULL,TRUE),
(24,'DG_SetUpSIP','2023-12-01 07:10:20.550404','2023-12-01 07:10:20.550404',NULL,TRUE),
(24,'DG_SellPage','2023-12-01 07:10:20.550404','2023-12-01 07:10:20.550404',NULL,TRUE),
(24,'DG_BuyPage','2023-12-01 07:10:20.550404','2023-12-01 07:10:20.550404',NULL,TRUE),
(24,'GI_Renewal','2023-12-01 07:10:20.550404','2023-12-01 07:10:20.550404',NULL,TRUE),
(24,'GI_KYC','2023-12-01 07:10:20.550404','2023-12-01 07:10:20.550404',NULL,TRUE),
(24,'GI_AddMember','2023-12-01 07:10:20.550404','2023-12-01 07:10:20.550404',NULL,TRUE),
(24,'GI_NCB','2023-12-01 07:10:20.550404','2023-12-01 07:10:20.550404',NULL,TRUE),
(24,'GI_PaymentIssue','2023-12-01 07:10:20.550404','2023-12-01 07:10:20.550404',NULL,TRUE),
(24,'GI_DocumentGeneration','2023-12-01 07:10:20.550404','2023-12-01 07:10:20.550404',NULL,TRUE),
(24,'GI_Claim','2023-12-01 07:10:20.550404','2023-12-01 07:10:20.550404',NULL,TRUE),
(24,'GI_IOS','2023-12-01 07:10:20.550404','2023-12-01 07:10:20.550404',NULL,TRUE),
(24,'GI_exceptions','2023-12-01 07:10:20.550404','2023-12-01 07:10:20.550404',NULL,TRUE);

View File

@@ -0,0 +1,74 @@
package action
import (
"errors"
"fmt"
"github.com/slack-go/slack"
"github.com/spf13/viper"
"houston/common/util"
"houston/internal/processor/action/view"
"houston/logger"
"houston/model/incident"
incidentService "houston/service/incident"
slack2 "houston/service/slack"
"strings"
)
type IncidentJiraLinksAction struct {
incidentService incidentService.IIncidentService
slackService slack2.ISlackService
}
const (
logTag = "[IncidentJiraLinksAction]"
)
func NewIncidentJiraLinksAction(
incidentService incidentService.IIncidentService,
slackService slack2.ISlackService,
) *IncidentJiraLinksAction {
return &IncidentJiraLinksAction{
incidentService: incidentService,
slackService: slackService,
}
}
func (action *IncidentJiraLinksAction) getJiraLinksBlock(initialValue string) *slack.InputBlock {
jiraLinksBlockBasicData := view.BasicInputElementData{Header: "Jira link(s)", PlaceHolder: "Add comma separated jira links here...", ActionId: util.SetJiraLinks, Optional: true, MaxLength: 1500, MultiLine: true}
jiraLinksBlockData := view.SimpleInputBlockElementData{BasicData: jiraLinksBlockBasicData, InitialValue: initialValue}
return view.CreatePlainTextInputBlock(jiraLinksBlockData)
}
func (action *IncidentJiraLinksAction) updateJiraLinks(jiraLinks string, callback slack.InteractionCallback, incidentEntity *incident.IncidentEntity) error {
channelID := callback.View.PrivateMetadata
formattedJiraLinks := strings.Split(
strings.ReplaceAll(strings.ReplaceAll(jiraLinks, "\n", ""), " ", ""),
",",
)
compareResults := util.CompareAndGetStringArrayResults(incidentEntity.JiraLinks, formattedJiraLinks)
//Update jira links only if there is a change. Validate by checking if there are any unique elements in either array
//If there are no unique elements in either array, then there is no change and skip the update
if !(len(compareResults.UniqueElementsInArrayA) == 0 && len(compareResults.UniqueElementsInArrayB) == 0) {
jiraLinksToBeUpdated := append(compareResults.CommonElements, compareResults.UniqueElementsInArrayB...)
//Below validation make sure that blank jira links are not to be tested for valid jira link
if len(jiraLinksToBeUpdated) > 0 && jiraLinksToBeUpdated[0] != "" {
for _, link := range jiraLinksToBeUpdated {
//Validate jira link
if !strings.HasPrefix(link, viper.GetString("navi.jira.base.url")) {
err := action.slackService.PostEphemeralByChannelID(fmt.Sprintf("%s is not a valid jira link", link), callback.User.ID, false, channelID)
if err != nil {
logger.Error(fmt.Sprintf("%s failed to post jira link validation failure ephemeral to slack channel: %s", logTag, incidentEntity.IncidentName))
return err
}
return errors.New(fmt.Sprintf("%s is a invalid jira link", link))
}
}
}
err := action.incidentService.UpdateIncidentJiraLinksEntity(incidentEntity, callback.User.ID, jiraLinksToBeUpdated)
if err != nil {
logger.Error(fmt.Sprintf("%s unable to update jira link(s) for incident %s", logTag, incidentEntity.IncidentName))
return err
}
}
return nil
}

View File

@@ -0,0 +1,84 @@
package action
import (
"errors"
"github.com/gojuno/minimock/v3"
"github.com/slack-go/slack"
"github.com/spf13/viper"
"github.com/stretchr/testify/suite"
"houston/logger"
"houston/mocks"
"strings"
"testing"
)
func getJiraLinks() []string {
return []string{"https://navihq.atlassian.net/browse/TP-44155", "https://navihq.atlassian.net/browse/TP-44157"}
}
type IncidentJiraLinksActionSuite struct {
suite.Suite
}
func TestIncidentJiraLinksAction(t *testing.T) {
suite.Run(t, new(IncidentJiraLinksActionSuite))
}
func (suite *IncidentJiraLinksActionSuite) SetupSuite() {
logger.InitLogger()
viper.Set("navi.jira.base.url", "https://navihq.atlassian.net/browse/")
}
func (suite *IncidentJiraLinksActionSuite) TestUpdateJiraLinksFailureAtUpdatingEntity() {
controller := minimock.NewController(suite.T())
suite.T().Cleanup(controller.Finish)
incidentService := mocks.NewIIncidentServiceMock(controller)
slackService := mocks.NewISlackServiceMock(controller)
incidentService.UpdateIncidentJiraLinksEntityMock.Return(errors.New("failure while updating jira links"))
jiraLinksActions := NewIncidentJiraLinksAction(incidentService, slackService)
err := jiraLinksActions.updateJiraLinks(strings.Join(getJiraLinks(), ", "), slack.InteractionCallback{}, getMockIncidentEntity())
suite.EqualError(err, "failure while updating jira links")
}
func (suite *IncidentJiraLinksActionSuite) TestUpdateJiraLinksFailureForInvalidJiraLink() {
controller := minimock.NewController(suite.T())
suite.T().Cleanup(controller.Finish)
incidentService := mocks.NewIIncidentServiceMock(controller)
slackService := mocks.NewISlackServiceMock(controller)
slackService.PostEphemeralByChannelIDMock.Return(nil)
jiraLinksActions := NewIncidentJiraLinksAction(incidentService, slackService)
err := jiraLinksActions.updateJiraLinks("dsa", slack.InteractionCallback{}, getMockIncidentEntity())
suite.EqualError(err, "dsa is a invalid jira link")
}
func (suite *IncidentJiraLinksActionSuite) TestUpdateJiraLinksSuccessBlankValue() {
controller := minimock.NewController(suite.T())
suite.T().Cleanup(controller.Finish)
incidentService := mocks.NewIIncidentServiceMock(controller)
slackService := mocks.NewISlackServiceMock(controller)
incidentService.UpdateIncidentJiraLinksEntityMock.Return(nil)
jiraLinksActions := NewIncidentJiraLinksAction(incidentService, slackService)
err := jiraLinksActions.updateJiraLinks("", slack.InteractionCallback{}, getMockIncidentEntity())
suite.NoError(err)
}
func (suite *IncidentJiraLinksActionSuite) TestUpdateJiraLinksSuccessDiffValue() {
controller := minimock.NewController(suite.T())
suite.T().Cleanup(controller.Finish)
incidentService := mocks.NewIIncidentServiceMock(controller)
slackService := mocks.NewISlackServiceMock(controller)
incidentService.UpdateIncidentJiraLinksEntityMock.Return(nil)
jiraLinksActions := NewIncidentJiraLinksAction(incidentService, slackService)
err := jiraLinksActions.updateJiraLinks(strings.Join(getJiraLinks(), ", "), slack.InteractionCallback{}, getMockIncidentEntity())
suite.NoError(err)
}
func (suite *IncidentJiraLinksActionSuite) TestUpdateJiraLinksSuccessSameValue() {
controller := minimock.NewController(suite.T())
suite.T().Cleanup(controller.Finish)
incidentService := mocks.NewIIncidentServiceMock(controller)
slackService := mocks.NewISlackServiceMock(controller)
jiraLinksActions := NewIncidentJiraLinksAction(incidentService, slackService)
err := jiraLinksActions.updateJiraLinks(strings.Join(getMockIncidentEntity().JiraLinks, ", "), slack.InteractionCallback{}, getMockIncidentEntity())
suite.NoError(err)
}

View File

@@ -0,0 +1,144 @@
package action
import (
"fmt"
"github.com/slack-go/slack"
"github.com/slack-go/slack/socketmode"
"go.uber.org/zap"
"houston/common/util"
"houston/internal/processor/action/view"
"houston/logger"
"houston/model/incident"
"houston/model/severity"
"houston/model/tag"
"houston/model/team"
"houston/service/rca"
"strings"
)
type IncidentRCASectionAction struct {
client *socketmode.Client
incidentRepository *incident.Repository
teamRepository *team.Repository
tagRepository *tag.Repository
severityRepository *severity.Repository
tagsAction *IncidentTagsAction
rcaSummaryAction *IncidentRCASummaryAction
jiraAction *IncidentJiraLinksAction
rcaService *rca.RcaService
}
func NewIncidentRCASectionAction(client *socketmode.Client, incidentRepo *incident.Repository,
teamService *team.Repository, tagService *tag.Repository, severityRepository *severity.Repository, tagsAction *IncidentTagsAction, rcaSummaryAction *IncidentRCASummaryAction, jiraAction *IncidentJiraLinksAction, rcaService *rca.RcaService) *IncidentRCASectionAction {
return &IncidentRCASectionAction{
client: client,
incidentRepository: incidentRepo,
teamRepository: teamService,
tagRepository: tagService,
severityRepository: severityRepository,
tagsAction: tagsAction,
rcaSummaryAction: rcaSummaryAction,
jiraAction: jiraAction,
rcaService: rcaService,
}
}
func (action *IncidentRCASectionAction) ProcessIncidentRCAActionRequestForSlashCommand(channelId string, triggerId string, request *socketmode.Request, requesterType util.ViewSubmissionType) error {
incidentEntity, err := action.incidentRepository.FindIncidentByChannelId(channelId)
//Validates if incident is valid
if err != nil || incidentEntity == nil {
logger.Error(fmt.Sprintf("failure while getting incident entity for channel: %v", channelId))
return err
}
tagsBlock := action.tagsAction.getTagsBlock(incidentEntity.ID)
rcaBlock := action.rcaSummaryAction.getRCASummaryBlock(incidentEntity.RCA)
jiraBlock := action.jiraAction.getJiraLinksBlock(strings.Join(incidentEntity.JiraLinks, ", "))
blocks := append(tagsBlock, *rcaBlock, *jiraBlock)
modalRequest := view.BuildIncidentRCASectionModal(channelId, blocks, requesterType)
_, err = action.client.OpenView(triggerId, modalRequest)
if err != nil {
logger.Error("houston slackbot open view command for ProcessIncidentRCAActionRequest failed.",
zap.String("trigger_id", triggerId), zap.String("channel_id", channelId), zap.Error(err))
return err
}
var payload interface{}
action.client.Ack(*request, payload)
return nil
}
func (action *IncidentRCASectionAction) ProcessIncidentRCAActionRequest(callback slack.InteractionCallback, request *socketmode.Request, requesterType util.ViewSubmissionType) {
err := action.ProcessIncidentRCAActionRequestForSlashCommand(callback.Channel.ID, callback.TriggerID, request, requesterType)
if err != nil {
logger.Error("houston slackbot open view command for ProcessIncidentRCAActionRequest failed.",
zap.String("trigger_id", callback.TriggerID), zap.String("channel_id", callback.Channel.ID), zap.Error(err))
}
}
func (action *IncidentRCASectionAction) PerformSetIncidentRCADetailsAction(callback slack.InteractionCallback, request *socketmode.Request, requesterType util.ViewSubmissionType) {
incidentEntity, err := action.incidentRepository.FindIncidentByChannelId(callback.View.PrivateMetadata)
if err != nil || incidentEntity == nil {
logger.Error(fmt.Sprintf("failed to get the incicent for channel id: %v", callback.View.PrivateMetadata))
return
}
blockActions := callback.View.State.Values
actions := make(map[string]slack.BlockAction, 0)
for _, a := range blockActions {
for key, value := range a {
actions[key] = value
}
}
var payload interface{}
action.client.Ack(*request, payload)
err = action.tagsAction.updateTags(actions, callback, incidentEntity)
if err != nil {
logger.Error(fmt.Sprintf("failed to update the incicent tags for incident id: %v", incidentEntity.ID))
}
rcaValue := actions[util.SetRCASummary].Value
err = action.rcaSummaryAction.updateRCASummary(rcaValue, incidentEntity)
if err != nil {
logger.Error(fmt.Sprintf("failed to update rca summary for incident id: %v", incidentEntity.ID))
}
jiraLinksValue := actions[util.SetJiraLinks].Value
err = action.jiraAction.updateJiraLinks(jiraLinksValue, callback, incidentEntity)
if err != nil {
logger.Error(fmt.Sprintf("failed to update jira link(s) for incident id: %v", incidentEntity.ID))
}
updatedIncidentEntity, _ := action.incidentRepository.FindIncidentById(incidentEntity.ID)
tagValuesMap, _ := action.tagsAction.getIncidentTagValuesAsMap(incidentEntity.ID)
action.postRCADetailsBlock(updatedIncidentEntity, tagValuesMap)
action.performPostUpdateActions(requesterType, callback, request)
}
func (action *IncidentRCASectionAction) PerformShowRCADetailsAction(callback slack.InteractionCallback, request *socketmode.Request) {
incidentEntity, err := action.incidentRepository.FindIncidentByChannelId(callback.Channel.ID)
if err != nil || incidentEntity == nil {
logger.Error(fmt.Sprintf("failed to get the incicent for channel id: %v", callback.View.PrivateMetadata))
return
}
var payload interface{}
action.client.Ack(*request, payload)
tagValuesMap, _ := action.tagsAction.getIncidentTagValuesAsMap(incidentEntity.ID)
action.postRCADetailsBlock(incidentEntity, tagValuesMap)
}
func (action *IncidentRCASectionAction) performPostUpdateActions(requesterType util.ViewSubmissionType, callback slack.InteractionCallback, request *socketmode.Request) {
switch requesterType {
case util.IncidentResolveSubmit:
resolveAction := ResolveIncidentAction{action.client, action.incidentRepository, action.tagRepository, action.teamRepository, action.severityRepository, action.rcaService}
resolveAction.IncidentResolveProcess(callback, request)
}
}
func (action *IncidentRCASectionAction) postRCADetailsBlock(entity *incident.IncidentEntity, tagValueMaps map[string][]string) {
blocks := view.ConstructShowRCADetailsBlock(entity, tagValueMaps)
color := util.GetColorBySeverity(4)
att := slack.Attachment{Blocks: blocks, Color: color}
_, _, err := action.client.PostMessage(entity.SlackChannel, slack.MsgOptionAttachments(att))
if err != nil {
logger.Error(fmt.Sprintf("exception occurred while posting RCA updates for incident %s", entity.IncidentName), zap.Error(err))
return
}
}

View File

@@ -0,0 +1,43 @@
package action
import (
"fmt"
"github.com/slack-go/slack"
"houston/common/util"
"houston/internal/processor/action/view"
"houston/logger"
"houston/model/incident"
"strings"
)
type IncidentRCASummaryAction struct {
incidentRepository incident.IIncidentRepository
}
func NewIncidentRCASummaryAction(incidentService incident.IIncidentRepository) *IncidentRCASummaryAction {
return &IncidentRCASummaryAction{
incidentRepository: incidentService,
}
}
func (action *IncidentRCASummaryAction) getRCASummaryBlock(rcaInitialValue string) *slack.InputBlock {
rcaBlockBasicData := view.BasicInputElementData{Header: "RCA summary", PlaceHolder: "Write RCA summary here", ActionId: util.SetRCASummary, Optional: false, MultiLine: true, MaxLength: 3000}
rcaBlockData := view.SimpleInputBlockElementData{BasicData: rcaBlockBasicData, InitialValue: rcaInitialValue}
return view.CreatePlainTextInputBlock(rcaBlockData)
}
func (action *IncidentRCASummaryAction) updateRCASummary(rcaValue string, incidentEntity *incident.IncidentEntity) error {
trimmedRCA := strings.TrimSpace(rcaValue)
//Update RCA only if provided RCA is nt blank and different from existing
if trimmedRCA != incidentEntity.RCA {
incidentEntity.RCA = trimmedRCA
err := action.incidentRepository.UpdateIncident(incidentEntity)
if err != nil {
logger.Error(fmt.Sprintf("IncidentUpdateRca error for incident %s : %s", incidentEntity.IncidentName, err.Error()))
return err
}
} else {
logger.Info(fmt.Sprintf("RCA is not changed for incident %s", incidentEntity.IncidentName))
}
return nil
}

View File

@@ -0,0 +1,56 @@
package action
import (
"errors"
"github.com/gojuno/minimock/v3"
"github.com/stretchr/testify/suite"
"houston/logger"
"houston/mocks"
"houston/model/incident"
"testing"
)
func getMockIncidentEntity() *incident.IncidentEntity {
return &incident.IncidentEntity{IncidentName: "test", RCA: "test", JiraLinks: []string{"https://navihq.atlassian.net/browse/TP-44157", "https://navihq.atlassian.net/browse/TP-44158"}}
}
type IncidentRCASummaryActionSuite struct {
suite.Suite
}
func TestIncidentRCASummaryActions(t *testing.T) {
suite.Run(t, new(IncidentRCASummaryActionSuite))
}
func (suite *IncidentRCASummaryActionSuite) SetupSuite() {
logger.InitLogger()
}
func (suite *IncidentRCASummaryActionSuite) TestUpdateRCASummaryFailure() {
controller := minimock.NewController(suite.T())
suite.T().Cleanup(controller.Finish)
incidentRepo := mocks.NewIIncidentRepositoryMock(controller)
incidentRepo.UpdateIncidentMock.Return(errors.New("failure while updating incident"))
rcaSummaryActions := NewIncidentRCASummaryAction(incidentRepo)
err := rcaSummaryActions.updateRCASummary("", getMockIncidentEntity())
suite.EqualError(err, "failure while updating incident")
}
func (suite *IncidentRCASummaryActionSuite) TestUpdateRCASummarySuccessForSameRCAValue() {
controller := minimock.NewController(suite.T())
suite.T().Cleanup(controller.Finish)
incidentRepo := mocks.NewIIncidentRepositoryMock(controller)
rcaSummaryActions := NewIncidentRCASummaryAction(incidentRepo)
err := rcaSummaryActions.updateRCASummary("test", getMockIncidentEntity())
suite.NoError(err)
}
func (suite *IncidentRCASummaryActionSuite) TestUpdateRCASummarySuccess() {
controller := minimock.NewController(suite.T())
suite.T().Cleanup(controller.Finish)
incidentRepo := mocks.NewIIncidentRepositoryMock(controller)
incidentRepo.UpdateIncidentMock.Return(nil)
rcaSummaryActions := NewIncidentRCASummaryAction(incidentRepo)
err := rcaSummaryActions.updateRCASummary("test1", getMockIncidentEntity())
suite.NoError(err)
}

View File

@@ -38,7 +38,7 @@ func NewIncidentResolveProcessor(client *socketmode.Client, incidentService *inc
}
func (irp *ResolveIncidentAction) IncidentResolveProcess(callback slack.InteractionCallback, request *socketmode.Request) {
channelId := callback.Channel.ID
channelId := callback.View.PrivateMetadata
incidentEntity, err := irp.incidentService.FindIncidentByChannelId(channelId)
if err != nil {
logger.Error("incident not found",
@@ -47,40 +47,27 @@ func (irp *ResolveIncidentAction) IncidentResolveProcess(callback slack.Interact
}
incidentStatusEntity, _ := irp.incidentService.FindIncidentStatusByName(incident.Resolved)
tags, err := irp.tagService.FindTagsByTeamId(incidentEntity.TeamId)
// check if tags are required to be set
//check if active tags are set or else throw error
var flag = true
if tags != nil {
for _, t := range *tags {
if t.Optional == false {
incidentTag, err := irp.incidentService.GetIncidentTagByTagId(incidentEntity.ID, t.Id)
if err != nil {
logger.Error(fmt.Sprintf("failed to get the incident tag for incidentId: %v", incidentEntity.ID))
activeTags, err := irp.tagService.GetMandatoryActiveTags()
if err != nil {
logger.Error(fmt.Sprintf("failed to get mandatory active tags due to error : %s", err.Error()))
return
}
if len(*activeTags) > 0 {
for _, activeTag := range *activeTags {
incidentTag, _ := irp.incidentService.GetIncidentTagsByTagIds(incidentEntity.ID, []uint{activeTag.Id})
if incidentTag.TagValueIds == nil || len(incidentTag.TagValueIds) < 1 {
logger.Error(fmt.Sprintf(" %s for incidentId: %v is not set", activeTag.Label, incidentEntity.ID))
flag = false
msgOption := slack.MsgOptionText(fmt.Sprintf("`%s tags are not set`", activeTag.Label), false)
_, errMessage := irp.client.PostEphemeral(callback.Channel.ID, callback.User.ID, msgOption)
if errMessage != nil {
logger.Error("post response failed for tags not set message", zap.Error(errMessage))
return
}
if nil == incidentTag {
flag = false
break
}
if t.Type == "free_text" {
if incidentTag.FreeTextValue == nil || len(*incidentTag.FreeTextValue) == 0 {
flag = false
break
}
} else {
if incidentTag.TagValueIds == nil || len(incidentTag.TagValueIds) == 0 {
flag = false
break
}
}
}
}
} else {
logger.Error(fmt.Sprintf("Tags not required for team id: %v and incident id: %v", incidentEntity.TeamId, incidentEntity.ID))
}
// check if all tags are set
@@ -100,9 +87,9 @@ func (irp *ResolveIncidentAction) IncidentResolveProcess(callback slack.Interact
logger.Info("successfully resolved the incident",
zap.String("channel", channelId),
zap.String("user_id", callback.User.ID))
msgOption := slack.MsgOptionText(fmt.Sprintf("<@%s> *>* `houston set status to %s`", callback.User.ID,
msgOption := slack.MsgOptionText(fmt.Sprintf("<@%s> *>* `set status to %s`", callback.User.ID,
incident.Resolved), false)
_, _, errMessage := irp.client.PostMessage(callback.Channel.ID, msgOption)
_, _, errMessage := irp.client.PostMessage(channelId, msgOption)
if errMessage != nil {
logger.Error("post response failed for ResolveIncident", zap.Error(errMessage))
return
@@ -136,7 +123,7 @@ func (irp *ResolveIncidentAction) IncidentResolveProcess(callback slack.Interact
}()
} else {
msgOption := slack.MsgOptionText(fmt.Sprintf("`Please set tag value`"), false)
_, errMessage := irp.client.PostEphemeral(callback.Channel.ID, callback.User.ID, msgOption)
_, errMessage := irp.client.PostEphemeral(channelId, callback.User.ID, msgOption)
if errMessage != nil {
logger.Error("post response failed for ResolveIncident", zap.Error(errMessage))
return

View File

@@ -1,87 +0,0 @@
package action
import (
"fmt"
"houston/logger"
"houston/model/incident"
"houston/model/tag"
"github.com/slack-go/slack"
"github.com/slack-go/slack/socketmode"
"go.uber.org/zap"
)
type IncidentShowTagsAction struct {
client *socketmode.Client
incidentService *incident.Repository
tagService *tag.Repository
}
func NewIncidentShowTagsProcessor(client *socketmode.Client, incidentService *incident.Repository, tagService *tag.Repository) *IncidentShowTagsAction {
return &IncidentShowTagsAction{
client: client,
incidentService: incidentService,
tagService: tagService,
}
}
func (isp *IncidentShowTagsAction) IncidentShowTagsRequestProcess(callback slack.InteractionCallback, request *socketmode.Request) {
incidentEntity, err := isp.incidentService.FindIncidentByChannelId(callback.Channel.ID)
if err != nil || incidentEntity == nil {
logger.Error(fmt.Sprintf("failure while getting incident for channel: %v", callback.Channel.ID))
return
}
incidentTagsEntities, err := isp.incidentService.GetIncidentTagsByIncidentId(incidentEntity.ID)
if err != nil || incidentTagsEntities == nil {
logger.Error(fmt.Sprintf("failure while getting incident tags for incident id: %v", incidentEntity.ID))
return
}
var msgStrings []string
for _, incidentTagEntity := range *incidentTagsEntities {
tagEntity, err := isp.tagService.FindById(incidentTagEntity.TagId)
if err != nil || tagEntity == nil {
logger.Error(fmt.Sprintf("failure while getting tags for incident id: %v", incidentEntity.ID))
return
}
if incidentTagEntity.FreeTextValue != nil && *incidentTagEntity.FreeTextValue != "" {
msgStrings = append(msgStrings, fmt.Sprintf("\n\n *%v* : \n`%v`", tagEntity.Label, *incidentTagEntity.FreeTextValue))
} else if incidentTagEntity.TagValueIds != nil {
tagValues, err := isp.tagService.FindTagValuesByIds(incidentTagEntity.TagValueIds)
if err != nil {
logger.Error(fmt.Sprintf("failure while getting tag values for incident id: %v", incidentEntity.ID))
return
} else if tagValues == nil {
continue
}
var msg string
for _, tv := range *tagValues {
if msg == "" {
msg = tv.Value
} else {
msg = msg + "," + tv.Value
}
}
msgStrings = append(msgStrings, fmt.Sprintf("\n\n *%v* : \n `%v`", tagEntity.Label, msg))
}
}
var finalMsg string
for _, msg := range msgStrings {
finalMsg = finalMsg + msg
}
msgOption := slack.MsgOptionText(fmt.Sprintf(finalMsg), true)
_, errMessage := isp.client.PostEphemeral(callback.Channel.ID, callback.User.ID, msgOption)
if errMessage != nil {
logger.Error("post ephemeral message response failed for IncidentShowTagsRequestProcess", zap.Error(errMessage))
return
}
var payload interface{}
isp.client.Ack(*request, payload)
}

View File

@@ -0,0 +1,196 @@
package action
import (
"fmt"
"github.com/lib/pq"
"github.com/slack-go/slack"
"go.uber.org/zap"
"houston/internal/processor/action/view"
"houston/logger"
"houston/model/incident"
"houston/model/tag"
"reflect"
"strconv"
)
type IncidentTagsAction struct {
incidentRepository incident.IIncidentRepository
tagRepository tag.ITagRepository
}
func NewIncidentTagsAction(incidentService incident.IIncidentRepository, tagService tag.ITagRepository) *IncidentTagsAction {
return &IncidentTagsAction{
incidentRepository: incidentService,
tagRepository: tagService,
}
}
func (action *IncidentTagsAction) getIncidentTagValuesAsMap(incidentId uint) (map[string][]string, error) {
tagValuesMap := make(map[string][]string)
tags, err := action.getActiveTags()
if err != nil {
return nil, err
}
for _, tagDTO := range *tags {
incidentTag, err := action.getIncidentTagByTagId(incidentId, tagDTO.Id)
if err != nil {
return nil, err
}
tagValuesIds := incidentTag.TagValueIds
if len(tagValuesIds) > 0 {
tagValuesMap[tagDTO.Label], err = action.getTagValuesForTagIds(&tagValuesIds)
if err != nil {
return nil, err
}
}
}
return tagValuesMap, nil
}
func (action *IncidentTagsAction) getTagValuesForTagIds(tagValueIds *pq.Int32Array) ([]string, error) {
var tagValueIdsArr []uint
for _, tagValueId := range *tagValueIds {
tagValueIdsArr = append(tagValueIdsArr, uint(tagValueId))
}
tagValues, err := action.tagRepository.GetTagValuesByTagValueId(tagValueIdsArr)
if err != nil {
logger.Error(fmt.Sprintf("failure while getting tag values for tag value ids %v", tagValueIds))
return nil, err
}
return tagValues, nil
}
func (action *IncidentTagsAction) getActiveTags() (*[]tag.TagDTO, error) {
tags, err := action.tagRepository.GetActiveTags()
//validates if any active tags are available
if err != nil || tags == nil {
logger.Error(fmt.Sprintf("failure while getting active tags"))
return nil, err
}
return tags, nil
}
func (action *IncidentTagsAction) getIncidentTagByTagId(incidentId uint, tagId uint) (*incident.IncidentTagEntity, error) {
incidentTag, err := action.incidentRepository.GetIncidentTagByTagId(incidentId, tagId)
if err != nil {
logger.Error(fmt.Sprintf("failed to get the incident tag for incidentId: %v", incidentId))
return nil, err
}
return incidentTag, nil
}
func (action *IncidentTagsAction) getTagsBlock(incidentId uint) []slack.InputBlock {
tags, _ := action.getActiveTags()
var blocks []slack.InputBlock
for _, tagDTO := range *tags {
tagValues, err := action.tagRepository.FindTagValuesByTagId(tagDTO.Id)
if err != nil {
logger.Error(fmt.Sprintf("failed to get the tag values for tagId: %v", tagDTO.Id))
return nil
}
incidentTag, err := action.incidentRepository.GetIncidentTagByTagId(incidentId, tagDTO.Id)
if err != nil {
logger.Error(fmt.Sprintf("failed to get the incident tag for incidentId: %v", incidentId))
return nil
}
var block *slack.InputBlock
if incidentTag == nil {
incidentTag, err = action.incidentRepository.CreateIncidentTag(incidentId, tagDTO.Id)
if err != nil || incidentTag == nil {
logger.Error(fmt.Sprintf("failure while creating tag for incident id: %v", incidentId))
return nil
}
}
if tagDTO.Type == tag.FreeText {
var initialValue string
if incidentTag.FreeTextValue == nil {
initialValue = ""
} else {
initialValue = *incidentTag.FreeTextValue
}
block = view.CreateInputBlockForTag(tagDTO, nil, initialValue, tagDTO.Optional)
} else {
var initialTags []tag.TagValueEntity
if tagValues == nil {
logger.Error(fmt.Sprintf("no tag values are present for tag: %v", tagDTO.Id))
return nil
}
if incidentTag.TagValueIds != nil {
for _, tagValue := range *tagValues {
for _, it := range incidentTag.TagValueIds {
if tagValue.ID == uint(it) {
localTagValue := tagValue
initialTags = append(initialTags, localTagValue)
}
}
}
}
block = view.CreateInputBlockForTag(tagDTO, *tagValues, initialTags, tagDTO.Optional)
}
blocks = append(blocks, *block)
}
return blocks
}
func (action *IncidentTagsAction) updateTags(actions map[string]slack.BlockAction, callback slack.InteractionCallback, incidentEntity *incident.IncidentEntity) error {
incidentTagsEntity, err := action.incidentRepository.GetIncidentTagsByIncidentId(incidentEntity.ID)
if err != nil || incidentTagsEntity == nil {
logger.Error(fmt.Sprintf("failed to get the incicent tags for incident id: %v", incidentEntity.ID))
return err
}
for _, it := range *incidentTagsEntity {
tagEntity, err := action.tagRepository.FindById(it.TagId)
if err != nil || tagEntity == nil {
logger.Error(fmt.Sprintf("failed to get the tag for id: %v", it.TagId))
return err
}
var _, isValidTeamTag = actions[tagEntity.ActionId]
if isValidTeamTag == true {
if tagEntity.Type == tag.FreeText {
localValue := actions[tagEntity.ActionId].Value
if it.FreeTextValue != &localValue {
it.FreeTextValue = &localValue
}
} else if tagEntity.Type == tag.SingleValue {
localValue := actions[tagEntity.ActionId].SelectedOption.Value
value, err := strconv.Atoi(localValue)
if err != nil {
logger.Error(fmt.Sprintf("string to int conversion failed for incident: %v, tag value: %v",
incidentEntity.ID, localValue))
return err
}
if (it.TagValueIds == nil && localValue != "") || it.TagValueIds[0] != int32(value) {
it.TagValueIds = pq.Int32Array{int32(value)}
}
} else if tagEntity.Type == tag.MultiValue {
var valueArray pq.Int32Array
for _, o := range actions[tagEntity.ActionId].SelectedOptions {
localValue, err := strconv.Atoi(o.Value)
if err != nil {
logger.Error(fmt.Sprintf("string to int conversion failed for incident: %v, tag value: %v",
incidentEntity.ID, localValue))
return err
}
valueArray = append(valueArray, int32(localValue))
}
if (it.TagValueIds == nil && len(valueArray) > 0) || !reflect.DeepEqual(it.TagValueIds, valueArray) {
it.TagValueIds = valueArray
}
}
}
_, err = action.incidentRepository.SaveIncidentTag(it)
if err != nil {
logger.Error(fmt.Sprintf("Failed while saving incident tag values for incidentId: %v",
callback.View.PrivateMetadata), zap.Error(err))
return err
}
}
return nil
}

View File

@@ -0,0 +1,201 @@
package action
import (
"errors"
"github.com/gojuno/minimock/v3"
"github.com/lib/pq"
"github.com/slack-go/slack"
"github.com/stretchr/testify/suite"
"houston/logger"
"houston/mocks"
"houston/model/incident"
"houston/model/tag"
"testing"
)
type IncidentTagsActionSuite struct {
suite.Suite
}
var tagsMock = []tag.TagDTO{{Id: 1, Label: "test", Optional: false}, {Id: 2, Label: "test", Optional: true}}
var incidentTagDTOMock = incident.IncidentTagEntity{IncidentId: 1, TagId: 1, TagValueIds: []int32{1, 2}}
func TestTagsActions(t *testing.T) {
suite.Run(t, new(IncidentTagsActionSuite))
}
func (suite *IncidentTagsActionSuite) SetupSuite() {
logger.InitLogger()
}
func (suite *IncidentTagsActionSuite) Test_GetIncidentTagValuesAsMapFailureAtActiveTags() {
controller := minimock.NewController(suite.T())
suite.T().Cleanup(controller.Finish)
tagRepo := mocks.NewITagRepositoryMock(controller)
tagRepo.GetActiveTagsMock.Return(nil, errors.New("failure while getting tag value map"))
incidentRepo := mocks.NewIIncidentRepositoryMock(controller)
tagActions := NewIncidentTagsAction(incidentRepo, tagRepo)
_, err := tagActions.getIncidentTagValuesAsMap(1)
suite.EqualError(err, "failure while getting tag value map")
}
func (suite *IncidentTagsActionSuite) Test_GetIncidentTagValuesAsMapFailureAtIncidentTag() {
controller := minimock.NewController(suite.T())
suite.T().Cleanup(controller.Finish)
tagRepo := mocks.NewITagRepositoryMock(controller)
tagRepo.GetActiveTagsMock.Return(&tagsMock, nil)
incidentRepo := mocks.NewIIncidentRepositoryMock(controller)
incidentRepo.GetIncidentTagByTagIdMock.Return(nil, errors.New("failure while getting incident tag"))
tagActions := NewIncidentTagsAction(incidentRepo, tagRepo)
_, err := tagActions.getIncidentTagValuesAsMap(1)
suite.EqualError(err, "failure while getting incident tag")
}
func (suite *IncidentTagsActionSuite) Test_GetIncidentTagValuesAsMapFailureAtTagValue() {
controller := minimock.NewController(suite.T())
suite.T().Cleanup(controller.Finish)
tagRepo := mocks.NewITagRepositoryMock(controller)
tagRepo.GetActiveTagsMock.Return(&tagsMock, nil)
incidentRepo := mocks.NewIIncidentRepositoryMock(controller)
incidentRepo.GetIncidentTagByTagIdMock.Return(&incidentTagDTOMock, nil)
tagRepo.GetTagValuesByTagValueIdMock.Return(nil, errors.New("failure while getting tag values"))
tagActions := NewIncidentTagsAction(incidentRepo, tagRepo)
_, err := tagActions.getIncidentTagValuesAsMap(1)
suite.EqualError(err, "failure while getting tag values")
}
func (suite *IncidentTagsActionSuite) Test_GetIncidentTagValuesAsMapSuccess() {
controller := minimock.NewController(suite.T())
suite.T().Cleanup(controller.Finish)
tagRepo := mocks.NewITagRepositoryMock(controller)
tagRepo.GetActiveTagsMock.Return(&tagsMock, nil)
incidentRepo := mocks.NewIIncidentRepositoryMock(controller)
incidentRepo.GetIncidentTagByTagIdMock.Return(&incidentTagDTOMock, nil)
tagRepo.GetTagValuesByTagValueIdMock.Return([]string{"test"}, nil)
tagActions := NewIncidentTagsAction(incidentRepo, tagRepo)
tagValues, err := tagActions.getIncidentTagValuesAsMap(1)
suite.NoError(err)
suite.Equal(map[string][]string{"test": {"test"}}, tagValues)
}
func (suite *IncidentTagsActionSuite) Test_GetTagValuesForTagIdsFailure() {
controller := minimock.NewController(suite.T())
suite.T().Cleanup(controller.Finish)
tagRepo := mocks.NewITagRepositoryMock(controller)
tagRepo.GetTagValuesByTagValueIdMock.Return(nil, errors.New("failure while getting tag values"))
incidentRepo := mocks.NewIIncidentRepositoryMock(controller)
tagActions := NewIncidentTagsAction(incidentRepo, tagRepo)
_, err := tagActions.getTagValuesForTagIds(&pq.Int32Array{1, 2})
suite.EqualError(err, "failure while getting tag values")
}
func (suite *IncidentTagsActionSuite) Test_GetTagValuesForTagIdsSuccess() {
controller := minimock.NewController(suite.T())
suite.T().Cleanup(controller.Finish)
tagRepo := mocks.NewITagRepositoryMock(controller)
tagRepo.GetTagValuesByTagValueIdMock.Return([]string{"test"}, nil)
incidentRepo := mocks.NewIIncidentRepositoryMock(controller)
tagActions := NewIncidentTagsAction(incidentRepo, tagRepo)
tagValues, err := tagActions.getTagValuesForTagIds(&pq.Int32Array{1, 2})
suite.NoError(err)
suite.Equal([]string{"test"}, tagValues)
}
func (suite *IncidentTagsActionSuite) Test_GetActiveTagsFailure() {
controller := minimock.NewController(suite.T())
suite.T().Cleanup(controller.Finish)
tagRepo := mocks.NewITagRepositoryMock(controller)
tagRepo.GetActiveTagsMock.Return(nil, errors.New("failure while getting active tags"))
incidentRepo := mocks.NewIIncidentRepositoryMock(controller)
tagActions := NewIncidentTagsAction(incidentRepo, tagRepo)
_, err := tagActions.getActiveTags()
suite.EqualError(err, "failure while getting active tags")
}
func (suite *IncidentTagsActionSuite) Test_GetActiveTagsSuccess() {
controller := minimock.NewController(suite.T())
suite.T().Cleanup(controller.Finish)
tagRepo := mocks.NewITagRepositoryMock(controller)
tagRepo.GetActiveTagsMock.Return(&tagsMock, nil)
incidentRepo := mocks.NewIIncidentRepositoryMock(controller)
tagActions := NewIncidentTagsAction(incidentRepo, tagRepo)
tags, err := tagActions.getActiveTags()
suite.NoError(err)
suite.Equal(tagsMock, *tags)
}
func (suite *IncidentTagsActionSuite) Test_GetIncidentTagByTagIdFailure() {
controller := minimock.NewController(suite.T())
suite.T().Cleanup(controller.Finish)
tagRepo := mocks.NewITagRepositoryMock(controller)
incidentRepo := mocks.NewIIncidentRepositoryMock(controller)
incidentRepo.GetIncidentTagByTagIdMock.Return(nil, errors.New("failure while getting incident tag"))
tagActions := NewIncidentTagsAction(incidentRepo, tagRepo)
_, err := tagActions.getIncidentTagByTagId(1, 1)
suite.EqualError(err, "failure while getting incident tag")
}
func (suite *IncidentTagsActionSuite) Test_GetIncidentTagByTagIdSuccess() {
controller := minimock.NewController(suite.T())
suite.T().Cleanup(controller.Finish)
tagRepo := mocks.NewITagRepositoryMock(controller)
incidentRepo := mocks.NewIIncidentRepositoryMock(controller)
incidentRepo.GetIncidentTagByTagIdMock.Return(&incidentTagDTOMock, nil)
tagActions := NewIncidentTagsAction(incidentRepo, tagRepo)
incidentTag, err := tagActions.getIncidentTagByTagId(1, 1)
suite.NoError(err)
suite.Equal(incidentTagDTOMock, *incidentTag)
}
func (suite *IncidentTagsActionSuite) TestUpdateTagsFailureAtIncident() {
controller := minimock.NewController(suite.T())
suite.T().Cleanup(controller.Finish)
tagRepo := mocks.NewITagRepositoryMock(controller)
incidentRepo := mocks.NewIIncidentRepositoryMock(controller)
incidentRepo.GetIncidentTagsByIncidentIdMock.Return(nil, errors.New("failure while getting incident"))
tagActions := NewIncidentTagsAction(incidentRepo, tagRepo)
callBack := slack.InteractionCallback{}
err := tagActions.updateTags(nil, callBack, getMockIncidentEntity())
suite.EqualError(err, "failure while getting incident")
}
func (suite *IncidentTagsActionSuite) TestUpdateTagsFailureAtTagId() {
controller := minimock.NewController(suite.T())
suite.T().Cleanup(controller.Finish)
tagRepo := mocks.NewITagRepositoryMock(controller)
incidentRepo := mocks.NewIIncidentRepositoryMock(controller)
incidentRepo.GetIncidentTagsByIncidentIdMock.Return(&[]incident.IncidentTagEntity{{TagId: 1}}, nil)
tagRepo.FindByIdMock.Return(nil, errors.New("failure while getting tag"))
tagActions := NewIncidentTagsAction(incidentRepo, tagRepo)
callBack := slack.InteractionCallback{}
err := tagActions.updateTags(nil, callBack, getMockIncidentEntity())
suite.EqualError(err, "failure while getting tag")
}
func (suite *IncidentTagsActionSuite) TestUpdateTagsFailureAtSaveIncident() {
controller := minimock.NewController(suite.T())
suite.T().Cleanup(controller.Finish)
tagRepo := mocks.NewITagRepositoryMock(controller)
tagRepo.FindByIdMock.Return(&tag.TagEntity{Name: "test"}, nil)
incidentRepo := mocks.NewIIncidentRepositoryMock(controller)
incidentRepo.GetIncidentTagsByIncidentIdMock.Return(&[]incident.IncidentTagEntity{{TagId: 1}}, nil)
incidentRepo.SaveIncidentTagMock.Return(nil, errors.New("failure while saving incident"))
tagActions := NewIncidentTagsAction(incidentRepo, tagRepo)
callBack := slack.InteractionCallback{}
err := tagActions.updateTags(nil, callBack, getMockIncidentEntity())
suite.EqualError(err, "failure while saving incident")
}
func (suite *IncidentTagsActionSuite) TestUpdateTagsSuccess() {
controller := minimock.NewController(suite.T())
suite.T().Cleanup(controller.Finish)
tagRepo := mocks.NewITagRepositoryMock(controller)
tagRepo.FindByIdMock.Return(&tag.TagEntity{Name: "test"}, nil)
incidentRepo := mocks.NewIIncidentRepositoryMock(controller)
incidentRepo.GetIncidentTagsByIncidentIdMock.Return(&[]incident.IncidentTagEntity{{TagId: 1}}, nil)
incidentRepo.SaveIncidentTagMock.Return(nil, nil)
tagActions := NewIncidentTagsAction(incidentRepo, tagRepo)
callBack := slack.InteractionCallback{}
err := tagActions.updateTags(nil, callBack, getMockIncidentEntity())
suite.NoError(err)
}

View File

@@ -1,97 +0,0 @@
package action
import (
"fmt"
"github.com/slack-go/slack"
"github.com/slack-go/slack/socketmode"
"github.com/spf13/viper"
"houston/internal/processor/action/view"
"houston/logger"
"houston/model/incident"
incidentService "houston/service/incident"
slack2 "houston/service/slack"
"strings"
)
type IncidentUpdateJiraLinksAction struct {
client *socketmode.Client
incidentRepository *incident.Repository
incidentService *incidentService.IncidentServiceV2
slackService *slack2.SlackService
}
const (
logTag = "[IncidentUpdateJiraLinksAction]"
)
func NewIncidentUpdateJiraLinksAction(
client *socketmode.Client,
incidentRepository *incident.Repository,
incidentServiceV2 *incidentService.IncidentServiceV2,
slackService *slack2.SlackService,
) *IncidentUpdateJiraLinksAction {
return &IncidentUpdateJiraLinksAction{
client: client,
incidentRepository: incidentRepository,
incidentService: incidentServiceV2,
slackService: slackService,
}
}
func (action *IncidentUpdateJiraLinksAction) IncidentUpdateJiraLinksRequestProcess(callback slack.InteractionCallback, request *socketmode.Request) {
result, err := action.incidentRepository.FindIncidentByChannelId(callback.Channel.ID)
if err != nil || result == nil {
logger.Error(fmt.Sprintf("%s failed to find incident entity for: %s", logTag, callback.Channel.Name))
return
}
modalRequest := view.BuildJiraLinksModal(callback.Channel.ID, result.JiraLinks...)
_, err = action.client.OpenView(callback.TriggerID, modalRequest)
if err != nil {
logger.Error(fmt.Sprintf("%s failed to open view command for: %s", logTag, callback.Channel.Name))
return
}
var payload interface{}
action.client.Ack(*request, payload)
}
func (action *IncidentUpdateJiraLinksAction) IncidentUpdateJiraLinks(callback slack.InteractionCallback, request *socketmode.Request, channel slack.Channel, user slack.User) {
channelID := callback.View.PrivateMetadata
incidentEntity, err := action.incidentRepository.FindIncidentByChannelId(channelID)
if err != nil || incidentEntity == nil {
logger.Error(fmt.Sprintf("%s failed to find incident entity for: %s", logTag, channel.Name))
return
}
jiraLinks := strings.Split(
strings.ReplaceAll(strings.ReplaceAll(buildUpdateJiraLinksRequest(callback.View.State.Values), "\n", ""), " ", ""),
",",
)
for _, l := range jiraLinks {
if strings.HasPrefix(l, viper.GetString("navi.jira.base.url")) == false {
err := action.slackService.PostEphemeralByChannelID(fmt.Sprintf("%s is not a valid jira link", l), user.ID, false, channelID)
if err != nil {
logger.Debug(fmt.Sprintf("%s failed to post jira link validation failure ephemeral to slack channel: %s", logTag, incidentEntity.IncidentName))
}
return
}
}
err = action.incidentService.LinkJiraToIncident(incidentEntity.ID, user.ID, jiraLinks...)
var payload interface{}
action.client.Ack(*request, payload)
}
func buildUpdateJiraLinksRequest(blockActions map[string]map[string]slack.BlockAction) string {
var requestMap = make(map[string]string, 0)
for _, actions := range blockActions {
for actionID, a := range actions {
if a.Type == "plain_text_input" {
requestMap[actionID] = a.Value
}
}
}
return requestMap["jira"]
}

View File

@@ -1,100 +0,0 @@
package action
import (
"fmt"
"github.com/slack-go/slack"
"github.com/slack-go/slack/socketmode"
"go.uber.org/zap"
"houston/internal/processor/action/view"
"houston/logger"
"houston/model/incident"
"regexp"
)
type IncidentUpdateRcaAction struct {
client *socketmode.Client
logger *zap.Logger
incidentRepository *incident.Repository
}
func NewIncidentUpdateRcaAction(client *socketmode.Client, incidentRepository *incident.Repository) *IncidentUpdateRcaAction {
return &IncidentUpdateRcaAction{
client: client,
incidentRepository: incidentRepository,
}
}
func (idp *IncidentUpdateRcaAction) IncidentUpdateRcaRequestProcess(callback slack.InteractionCallback, request *socketmode.Request) {
result, err := idp.incidentRepository.FindIncidentByChannelId(callback.Channel.ID)
if err != nil {
logger.Error("FindIncidentByChannelId error ",
zap.String("incident_slack_channel_id", callback.Channel.ID), zap.String("channel", callback.Channel.Name),
zap.String("user_id", callback.User.ID), zap.Error(err))
return
} else if result == nil {
logger.Error("IncidentEntity not found ",
zap.String("incident_slack_channel_id", callback.Channel.ID), zap.String("channel", callback.Channel.Name),
zap.String("user_id", callback.User.ID), zap.Error(err))
return
}
modalRequest := view.BuildRcaModal(callback.Channel.ID, result.RCA)
_, err = idp.client.OpenView(callback.TriggerID, modalRequest)
if err != nil {
logger.Error("houston slackbot openview command for IncidentUpdateRcaRequestProcess failed.",
zap.String("trigger_id", callback.TriggerID), zap.String("channel_id", callback.Channel.ID), zap.Error(err))
return
}
var payload interface{}
idp.client.Ack(*request, payload)
}
func (itp *IncidentUpdateRcaAction) IncidentUpdateRca(callback slack.InteractionCallback, request *socketmode.Request, channel slack.Channel, user slack.User) {
incidentEntity, err := itp.incidentRepository.FindIncidentByChannelId(callback.View.PrivateMetadata)
if err != nil {
logger.Error("FindIncidentByChannelId error",
zap.String("incident_slack_channel_id", channel.ID), zap.String("channel", channel.Name),
zap.String("user_id", user.ID), zap.Error(err))
return
} else if incidentEntity == nil {
logger.Error("IncidentEntity not found ",
zap.String("incident_slack_channel_id", callback.Channel.ID), zap.String("channel", callback.Channel.Name),
zap.String("user_id", callback.User.ID), zap.Error(err))
return
}
incidentRca := buildUpdateRcaRequest(callback.View.State.Values)
incidentEntity.RCA = incidentRca
incidentEntity.UpdatedBy = user.ID
err = itp.incidentRepository.UpdateIncident(incidentEntity)
if err != nil {
logger.Error("IncidentUpdateRca error",
zap.String("incident_slack_channel_id", channel.ID), zap.String("channel", channel.Name),
zap.String("user_id", user.ID), zap.Error(err))
return
}
re := regexp.MustCompile(`\n+`)
msgOption := slack.MsgOptionText(fmt.Sprintf("<@%s> *>* `houston has updated RCA to \"%s\"`", user.ID, re.ReplaceAllString(incidentEntity.RCA, " ")), false)
_, _, errMessage := itp.client.PostMessage(callback.View.PrivateMetadata, msgOption)
if errMessage != nil {
logger.Error("post response failed for IncidentUpdateRca", zap.Error(errMessage))
return
}
var payload interface{}
itp.client.Ack(*request, payload)
}
func buildUpdateRcaRequest(blockActions map[string]map[string]slack.BlockAction) string {
var requestMap = make(map[string]string, 0)
for _, actions := range blockActions {
for actionID, a := range actions {
if a.Type == "plain_text_input" {
requestMap[actionID] = a.Value
}
}
}
return requestMap["rca"]
}

View File

@@ -87,7 +87,7 @@ func (isp *IncidentUpdateSevertityAction) IncidentUpdateSeverity(callback slack.
if viper.GetBool("UPDATE_INCIDENT_V2_ENABLED") {
teamEntity, _, incidentStatusEntity, incidentChannels, err := isp.incidentServiceV2.FetchAllEntitiesForIncident(incidentEntity)
if err != nil {
logger.Error(fmt.Sprintf("error in fetching entities for incident with id: %d %w", incidentEntity.ID, err))
logger.Error(fmt.Sprintf("error in fetching entities for incident with id: %d %v", incidentEntity.ID, err))
return
}
@@ -103,7 +103,7 @@ func (isp *IncidentUpdateSevertityAction) IncidentUpdateSeverity(callback slack.
incidentStatusEntity,
incidentChannels,
); err != nil {
logger.Error(fmt.Sprintf("error in updating severity: %w", err))
logger.Error(fmt.Sprintf("error in updating severity: %v", err))
}
var payload interface{}
isp.client.Ack(*request, payload)

View File

@@ -1,216 +0,0 @@
package action
import (
"fmt"
"houston/common/util"
"houston/internal/processor/action/view"
"houston/logger"
"houston/model/incident"
"houston/model/tag"
"houston/model/team"
"regexp"
"strconv"
"strings"
"github.com/lib/pq"
"github.com/slack-go/slack"
"github.com/slack-go/slack/socketmode"
"go.uber.org/zap"
)
type IncidentUpdateTagsAction struct {
client *socketmode.Client
incidentRepository *incident.Repository
teamRepository *team.Repository
tagRepository *tag.Repository
}
func NewIncidentUpdateTagsAction(client *socketmode.Client, incidentService *incident.Repository,
teamService *team.Repository, tagService *tag.Repository) *IncidentUpdateTagsAction {
return &IncidentUpdateTagsAction{
client: client,
incidentRepository: incidentService,
teamRepository: teamService,
tagRepository: tagService,
}
}
func (itp *IncidentUpdateTagsAction) IncidentUpdateTagsRequestProcess(callback slack.InteractionCallback, request *socketmode.Request) {
incidentEntity, err := itp.incidentRepository.FindIncidentByChannelId(callback.Channel.ID)
if err != nil || incidentEntity == nil {
logger.Error(fmt.Sprintf("failure while getting incident entity for channel: %v", callback.Channel.ID))
return
}
team, err := itp.teamRepository.FindTeamById(incidentEntity.TeamId)
if err != nil || team == nil {
logger.Error(fmt.Sprintf("failure while getting team for incident id: %v", incidentEntity.ID))
return
}
tags, err := itp.tagRepository.FindTagsByTeamId(team.ID)
if err != nil || tags == nil {
logger.Error(fmt.Sprintf("failure while getting tags for incident id: %v", incidentEntity.ID))
return
}
var blocks []slack.InputBlock
for _, t := range *tags {
tagValues, err := itp.tagRepository.FindTagValuesByTagId(t.Id)
if err != nil {
logger.Error(fmt.Sprintf("failed to get the tag values for tagId: %v", t.Id))
return
}
incidentTag, err := itp.incidentRepository.GetIncidentTagByTagId(incidentEntity.ID, t.Id)
if err != nil {
logger.Error(fmt.Sprintf("failed to get the incident tag for incidentId: %v", incidentEntity.ID))
return
}
var block *slack.InputBlock
if incidentTag == nil {
incidentTag, err = itp.incidentRepository.CreateIncidentTag(incidentEntity.ID, t.Id)
if err != nil || incidentTag == nil {
logger.Error(fmt.Sprintf("failure while creating tag for incident id: %v", incidentEntity.ID))
return
}
}
if t.Type == tag.FreeText {
var initialValue string
if incidentTag.FreeTextValue == nil {
initialValue = ""
} else {
initialValue = *incidentTag.FreeTextValue
}
block = view.CreateInputBlock(t, nil, initialValue, t.Optional)
} else {
var initialTags []tag.TagValueEntity
if tagValues == nil {
logger.Error(fmt.Sprintf("no tag values are present for tag: %v", t.Id))
return
}
if incidentTag.TagValueIds != nil {
for _, tv := range *tagValues {
for _, it := range incidentTag.TagValueIds {
if tv.ID == uint(it) {
ltv := tv
initialTags = append(initialTags, ltv)
}
}
}
}
block = view.CreateInputBlock(t, *tagValues, initialTags, t.Optional)
}
blocks = append(blocks, *block)
}
modalRequest := view.BuildIncidentUpdateTagModal(callback.Channel, blocks)
_, err = itp.client.OpenView(callback.TriggerID, modalRequest)
if err != nil {
logger.Error("houston slackbot openview command for IncidentUpdateTagsRequestProcess failed.",
zap.String("trigger_id", callback.TriggerID), zap.String("channel_id", callback.Channel.ID), zap.Error(err))
return
}
var payload interface{}
itp.client.Ack(*request, payload)
}
func (itp *IncidentUpdateTagsAction) IncidentUpdateTags(callback slack.InteractionCallback, request *socketmode.Request) {
incidentEntity, err := itp.incidentRepository.FindIncidentByChannelId(callback.View.PrivateMetadata)
if err != nil || incidentEntity == nil {
logger.Error(fmt.Sprintf("failed to get the incicent for channel id: %v", callback.View.PrivateMetadata))
return
}
incidentTagsEntity, err := itp.incidentRepository.GetIncidentTagsByIncidentId(incidentEntity.ID)
if err != nil || incidentTagsEntity == nil {
logger.Error(fmt.Sprintf("failed to get the incicent tags for incident id: %v", incidentEntity.ID))
return
}
blockActions := callback.View.State.Values
actions := make(map[string]slack.BlockAction, 0)
for _, a := range blockActions {
for key, value := range a {
actions[key] = value
}
}
//update resolution text
rca := actions[util.SetRCA].Value
if strings.TrimSpace(rca) != "" {
incidentEntity.RCA = rca
err = itp.incidentRepository.UpdateIncident(incidentEntity)
if err != nil {
logger.Error("IncidentUpdateRca error",
zap.String("incident_slack_channel_id", callback.Channel.ID), zap.String("channel", callback.Channel.Name),
zap.String("user_id", callback.User.ID), zap.Error(err))
return
}
re := regexp.MustCompile(`\n+`)
msgOption := slack.MsgOptionText(fmt.Sprintf("<@%s> *>* `houston has updated RCA to \"%s\"`", callback.User.ID, re.ReplaceAllString(incidentEntity.RCA, " ")), false)
_, _, errMessage := itp.client.PostMessage(callback.View.PrivateMetadata, msgOption)
if errMessage != nil {
logger.Error("post response failed for IncidentUpdateRca", zap.Error(errMessage))
return
}
}
// build request to update the tag values
for _, it := range *incidentTagsEntity {
tagEntity, err := itp.tagRepository.FindById(it.TagId)
if err != nil || tagEntity == nil {
logger.Error(fmt.Sprintf("failed to get the tag for id: %v", it.TagId))
return
}
var _, isValidTeamTag = actions[tagEntity.ActionId]
if isValidTeamTag == true {
if tagEntity.Type == tag.FreeText {
localValue := actions[tagEntity.ActionId].Value
it.FreeTextValue = &localValue
} else if tagEntity.Type == tag.SingleValue {
localValue := actions[tagEntity.ActionId].SelectedOption.Value
value, err := strconv.Atoi(localValue)
if err != nil {
logger.Error(fmt.Sprintf("string to int conversion failed for incident: %v, tag value: %v",
incidentEntity.ID, localValue))
return
}
it.TagValueIds = pq.Int32Array{int32(value)}
} else if tagEntity.Type == tag.MultiValue {
var valueArray pq.Int32Array
for _, o := range actions[tagEntity.ActionId].SelectedOptions {
localValue, err := strconv.Atoi(o.Value)
if err != nil {
logger.Error(fmt.Sprintf("string to int conversion failed for incident: %v, tag value: %v",
incidentEntity.ID, localValue))
return
}
valueArray = append(valueArray, int32(localValue))
}
it.TagValueIds = valueArray
}
}
_, err = itp.incidentRepository.SaveIncidentTag(it)
if err != nil {
logger.Error(fmt.Sprintf("Failed while saving incident tag values for incidentId: %v",
callback.View.PrivateMetadata), zap.Error(err))
return
}
}
var payload interface{}
itp.client.Ack(*request, payload)
}

View File

@@ -6,9 +6,10 @@ import (
"github.com/slack-go/slack/socketmode"
"go.uber.org/zap"
"houston/appcontext"
"houston/internal/processor/action/view"
"houston/common/util"
"houston/logger"
"houston/pkg/slackbot"
"houston/service/rca"
)
const openSetRCAViewModalActionLogTag = "[open_set_rca_view_modal_command_action]"
@@ -16,15 +17,18 @@ const openSetRCAViewModalActionLogTag = "[open_set_rca_view_modal_command_action
type OpenFillRCAViewModalCommandAction struct {
socketModeClient *socketmode.Client
slackBot *slackbot.Client
rcaService *rca.RcaService
}
func NewOpenFillRCAViewModalCommandAction(
socketModeClient *socketmode.Client,
slackBot *slackbot.Client,
rcaService *rca.RcaService,
) *OpenFillRCAViewModalCommandAction {
return &OpenFillRCAViewModalCommandAction{
socketModeClient: socketModeClient,
slackBot: slackBot,
rcaService: rcaService,
}
}
@@ -36,30 +40,15 @@ func (action *OpenFillRCAViewModalCommandAction) PerformAction(evt *socketmode.E
return
}
err := action.openFillRCAViewModal(cmd)
tagsAction := NewIncidentTagsAction(appcontext.GetIncidentRepo(), appcontext.GetTagRepo())
rcaSummaryAction := NewIncidentRCASummaryAction(appcontext.GetIncidentRepo())
jiraLinksAction := NewIncidentJiraLinksAction(appcontext.GetIncidentService(), appcontext.GetSlackService())
rcaSectionAction := NewIncidentRCASectionAction(action.socketModeClient, appcontext.GetIncidentRepo(), appcontext.GetTeamRepo(), appcontext.GetTagRepo(), appcontext.GetSeverityRepo(), tagsAction, rcaSummaryAction, jiraLinksAction, action.rcaService)
err := rcaSectionAction.ProcessIncidentRCAActionRequestForSlashCommand(cmd.ChannelID, cmd.TriggerID, evt.Request, util.SetIncidentRCADetailsSubmit)
if err != nil {
err := appcontext.GetSlackService().PostEphemeralByChannelID(err.Error(), cmd.UserID, false, cmd.ChannelID)
if err != nil {
logger.Error(fmt.Sprintf("%s failed to post ephemeral for create incident error. %+v", openSetRCAViewModalActionLogTag, err))
}
}
action.socketModeClient.Ack(*evt.Request)
}
func (action *OpenFillRCAViewModalCommandAction) openFillRCAViewModal(cmd slack.SlashCommand) error {
logger.Info(fmt.Sprintf("%s received request to resolve the incident", openSetRCAViewModalActionLogTag))
return executeForHoustonChannel(cmd, func() error {
incidentEntity, err := appcontext.GetIncidentService().GetIncidentByChannelID(cmd.ChannelID)
if err != nil {
logger.Error(fmt.Sprintf("%s failed to fetch incident by channel ID: %s. %+v", openSetRCAViewModalActionLogTag, cmd.ChannelID, err))
return genericBackendError
}
_, err = action.socketModeClient.OpenView(cmd.TriggerID, view.BuildRcaModal(cmd.ChannelID, incidentEntity.RCA))
if err != nil {
return fmt.Errorf("failed to open set RCA view modal")
}
return nil
})
}

View File

@@ -4,15 +4,12 @@ import (
"fmt"
"github.com/slack-go/slack"
"github.com/slack-go/slack/socketmode"
"github.com/spf13/viper"
"go.uber.org/zap"
"houston/appcontext"
"houston/common/util"
"houston/logger"
"houston/model/incident"
"houston/pkg/slackbot"
"houston/service/rca"
"time"
)
const resolveIncidentActionLogTag = "[slash_command_action]"
@@ -42,135 +39,15 @@ func (action *ResolveIncidentCommandAction) PerformAction(evt *socketmode.Event)
logger.Error("event data to slash command conversion failed", zap.Any("data", evt))
return
}
err := action.resolveIncident(cmd)
tagsAction := NewIncidentTagsAction(appcontext.GetIncidentRepo(), appcontext.GetTagRepo())
rcaSummaryAction := NewIncidentRCASummaryAction(appcontext.GetIncidentRepo())
jiraLinksAction := NewIncidentJiraLinksAction(appcontext.GetIncidentService(), appcontext.GetSlackService())
rcaSectionAction := NewIncidentRCASectionAction(action.socketModeClient, appcontext.GetIncidentRepo(), appcontext.GetTeamRepo(), appcontext.GetTagRepo(), appcontext.GetSeverityRepo(), tagsAction, rcaSummaryAction, jiraLinksAction, action.rcaService)
err := rcaSectionAction.ProcessIncidentRCAActionRequestForSlashCommand(cmd.ChannelID, cmd.TriggerID, evt.Request, util.IncidentResolveSubmit)
if err != nil {
err := appcontext.GetSlackService().PostEphemeralByChannelID(err.Error(), cmd.UserID, false, cmd.ChannelID)
if err != nil {
logger.Error(fmt.Sprintf("%s failed to post ephemeral for create incident error. %+v", resolveIncidentActionLogTag, err))
}
}
action.socketModeClient.Ack(*evt.Request)
}
func (action *ResolveIncidentCommandAction) resolveIncident(cmd slack.SlashCommand) error {
logger.Info(fmt.Sprintf("%s received request to resolve the incident", resolveIncidentActionLogTag))
return executeForHoustonChannel(cmd, func() error {
incidentEntity, err := appcontext.GetIncidentService().GetIncidentByChannelID(cmd.ChannelID)
if err != nil {
logger.Error(fmt.Sprintf("%s failed to fetch incident entity with channel ID: %s. %+v", resolveIncidentActionLogTag, cmd.ChannelID, err))
return fmt.Errorf("failed to fetch incident entity with channel ID: %s", cmd.ChannelID)
}
if incidentEntity == nil {
logger.Error(fmt.Sprintf("%s no entry found for incident with channel ID: %s in DB", resolveIncidentActionLogTag, cmd.ChannelID))
return genericBackendError
}
incidentStatusEntity, _ := appcontext.GetIncidentRepo().FindIncidentStatusByName(incident.Resolved)
tags, err := appcontext.GetTagRepo().FindTagsByTeamId(incidentEntity.TeamId)
//check if tags are required to be set
var flag = true
if tags != nil {
for _, t := range *tags {
if t.Optional == false {
incidentTag, err := appcontext.GetIncidentRepo().GetIncidentTagByTagId(incidentEntity.ID, t.Id)
if err != nil {
logger.Error(fmt.Sprintf("%s failed to get the incident tag for incidentId: %d", resolveIncidentActionLogTag, incidentEntity.ID))
return genericBackendError
}
if nil == incidentTag {
flag = false
break
}
if t.Type == "free_text" {
if incidentTag.FreeTextValue == nil || len(*incidentTag.FreeTextValue) == 0 {
flag = false
break
}
} else {
if incidentTag.TagValueIds == nil || len(incidentTag.TagValueIds) == 0 {
flag = false
break
}
}
}
}
} else {
logger.Info(fmt.Sprintf("%s tags not required for team id: %v and incident id: %v", resolveIncidentActionLogTag, incidentEntity.TeamId, incidentEntity.ID))
}
//check if all tags are set
if flag == true {
now := time.Now()
incidentEntity.Status = incidentStatusEntity.ID
incidentEntity.EndTime = &now
err = appcontext.GetIncidentRepo().UpdateIncident(incidentEntity)
if err != nil {
logger.Error("failed to update incident to resolve state",
zap.String("channel", cmd.ChannelID),
zap.String("user_id", cmd.UserID), zap.Error(err))
return fmt.Errorf("failed to update incident status")
}
logger.Info("successfully resolved the incident",
zap.String("channel", cmd.ChannelID),
zap.String("user_id", cmd.UserID))
msgOption := slack.MsgOptionText(fmt.Sprintf("<@%s> *>* `houston set status to %s`", cmd.UserID,
incident.Resolved), false)
_, _, errMessage := action.socketModeClient.PostMessage(cmd.ChannelID, msgOption)
if errMessage != nil {
logger.Error("post response failed for ResolveIncident", zap.Error(errMessage))
return fmt.Errorf("incident is resolved but failed to post response to slack channel")
}
msgUpdate := NewIncidentChannelMessageUpdateAction(
action.socketModeClient,
appcontext.GetIncidentRepo(),
appcontext.GetTeamRepo(),
appcontext.GetSeverityRepo(),
)
msgUpdate.ProcessAction(incidentEntity.SlackChannel)
//ToDo() Delete Conference event if exists and if incident is resolved
go func() {
if incidentEntity.SeverityId != incident.Sev0Id && incidentEntity.SeverityId != incident.Sev1Id {
postErr := util.PostArchivingTimeToIncidentChannel(cmd.ChannelID, incident.Resolved, action.socketModeClient)
if postErr != nil {
logger.Error("failed to post archiving time to incident channel", zap.String("channel id", cmd.ChannelID), zap.Error(err))
}
}
if viper.GetBool("RCA_GENERATION_ENABLED") {
err = action.rcaService.SendConversationDataForGeneratingRCA(incidentEntity.ID, cmd.ChannelID)
if err != nil {
logger.Error(fmt.Sprintf("failed to generate rca for incident id: %d of channel id: %s", incidentEntity.ID, cmd.ChannelID), zap.Error(err))
_, _, errMessage := action.socketModeClient.PostMessage(cmd.ChannelID, slack.MsgOptionText("`Some issue occurred while generating RCA`", false))
if errMessage != nil {
logger.Error("post response failed for rca failure message", zap.Error(errMessage))
}
} else {
_, _, errMessage := action.socketModeClient.PostMessage(cmd.ChannelID, slack.MsgOptionText("System RCA generation is in progress and might take 2 to 4 minutes.", false))
if errMessage != nil {
logger.Error("post response failed for rca generated message", zap.Error(errMessage))
}
}
}
}()
} else {
msgOption := slack.MsgOptionText(fmt.Sprintf("`Please set tag value`"), false)
_, errMessage := action.socketModeClient.PostEphemeral(cmd.ChannelID, cmd.UserID, msgOption)
if errMessage != nil {
logger.Error("post response failed for ResolveIncident", zap.Error(errMessage))
return fmt.Errorf("`Please set tag value`")
}
}
return nil
})
}

View File

@@ -0,0 +1,155 @@
package view
import (
"fmt"
"github.com/slack-go/slack"
"houston/common/util"
"houston/model/incident"
"houston/model/tag"
"strconv"
"strings"
)
func BuildIncidentRCASectionModal(channelId string, inputBlocks []slack.InputBlock, requesterType util.ViewSubmissionType) slack.ModalViewRequest {
titleText := slack.NewTextBlockObject(slack.PlainTextType, "RCA", false, false)
closeText := slack.NewTextBlockObject(slack.PlainTextType, "Close", false, false)
submitText := slack.NewTextBlockObject(slack.PlainTextType, "Submit", false, false)
var localBlocks []slack.Block
for _, block := range inputBlocks {
localBlocks = append(localBlocks, block)
}
blocks := slack.Blocks{
BlockSet: localBlocks,
}
return slack.ModalViewRequest{
Type: slack.VTModal,
Title: titleText,
Close: closeText,
Submit: submitText,
Blocks: blocks,
PrivateMetadata: channelId,
CallbackID: string(requesterType),
}
}
func ConstructShowRCADetailsBlock(incident *incident.IncidentEntity, tagValuesMap map[string][]string) slack.Blocks {
var blockSet []slack.Block
blockSet = append(blockSet, buildHeaderBlock(util.RCADetailsLabel))
tagsBlockSet := buildTagsBlock(tagValuesMap)
for _, tagBlock := range tagsBlockSet {
blockSet = append(blockSet, *tagBlock)
}
blockSet = append(blockSet, buildRCASummaryBlock(incident), buildJiraLinksBlock(incident))
return slack.Blocks{
BlockSet: blockSet,
}
}
func buildHeaderBlock(header string) *slack.HeaderBlock {
block := slack.NewHeaderBlock(&slack.TextBlockObject{
Type: util.PlainTextType,
Text: header,
})
return block
}
func buildTagsBlock(tagValuesMap map[string][]string) []*slack.SectionBlock {
var tagsBlocks []*slack.SectionBlock
for key := range tagValuesMap {
tagsBlocks = append(tagsBlocks, slack.NewSectionBlock(&slack.TextBlockObject{Type: util.MarkDownElementType, Text: fmt.Sprintf("*%s* : %s", key, strings.Join(tagValuesMap[key], ", "))}, nil, nil))
}
return tagsBlocks
}
func buildRCASummaryBlock(entity *incident.IncidentEntity) *slack.SectionBlock {
field := slack.TextBlockObject{Type: util.MarkDownElementType, Text: fmt.Sprintf("*%s* : %s", util.RCASummaryLabel, entity.RCA), Verbatim: false}
block := slack.NewSectionBlock(&field, nil, nil)
return block
}
func buildJiraLinksBlock(entity *incident.IncidentEntity) *slack.SectionBlock {
var hyperLinks []string
for _, jiraLink := range entity.JiraLinks {
if jiraLink != "" {
hyperLinks = append(hyperLinks, fmt.Sprintf("<%s|%s>", jiraLink, strings.Split(jiraLink, util.JiraIdSeparator)[1]))
}
}
field := slack.TextBlockObject{Type: util.MarkDownElementType, Text: fmt.Sprintf("*%s* : %s", util.JiraLinksLabel, strings.Join(hyperLinks, ", "))}
block := slack.NewSectionBlock(&field, nil, nil)
return block
}
func CreateInputBlockForTag(tagEntity tag.TagDTO, tagValues []tag.TagValueEntity, initialTagValues interface{}, isOptional bool) *slack.InputBlock {
text := slack.NewTextBlockObject(slack.PlainTextType, tagEntity.Label, false, false)
var element slack.BlockElement
switch tagEntity.Type {
case tag.FreeText:
{
element = createPlainTextInputBlockElementForTag(tagEntity, initialTagValues)
}
case tag.SingleValue:
{
element = createOptionsSelectBlockElementForTag(tagEntity, tagValues, initialTagValues.([]tag.TagValueEntity))
}
case tag.MultiValue:
{
element = createMultiOptionsSelectBlockElementForTag(tagEntity, tagValues, initialTagValues.([]tag.TagValueEntity))
}
default:
{
return nil
}
}
block := slack.NewInputBlock(tagEntity.Name, text, nil, element)
block.Optional = isOptional
return block
}
func createPlainTextInputBlockElementForTag(tagEntity tag.TagDTO, value interface{}) *slack.PlainTextInputBlockElement {
placeholder := slack.NewTextBlockObject(slack.PlainTextType, tagEntity.PlaceHolder, false, false)
element := slack.NewPlainTextInputBlockElement(placeholder, tagEntity.ActionId)
if value != nil {
element.InitialValue = fmt.Sprintf("%v", value)
} else {
element.InitialValue = ""
}
return element
}
func createOptionsSelectBlockElementForTag(tagEntity tag.TagDTO, tagValues []tag.TagValueEntity, initialTagValues []tag.TagValueEntity) *slack.SelectBlockElement {
blockOptions := createTagOptions(tagValues)
placeholder := slack.NewTextBlockObject(slack.PlainTextType, tagEntity.PlaceHolder, false, false)
element := slack.NewOptionsSelectBlockElement(slack.OptTypeStatic, placeholder, tagEntity.ActionId, blockOptions...)
if initialTagValues != nil {
element.InitialOption = createTagOptions(initialTagValues)[0]
}
return element
}
func createMultiOptionsSelectBlockElementForTag(tagEntity tag.TagDTO, tagValues []tag.TagValueEntity, initialTagValues []tag.TagValueEntity) *slack.MultiSelectBlockElement {
blockOptions := createTagOptions(tagValues)
placeholder := slack.NewTextBlockObject(slack.PlainTextType, tagEntity.PlaceHolder, false, false)
element := slack.NewOptionsMultiSelectBlockElement(slack.MultiOptTypeStatic, placeholder, tagEntity.ActionId, blockOptions...)
if initialTagValues != nil {
element.InitialOptions = createTagOptions(initialTagValues)
}
return element
}
func createTagOptions(tagValues []tag.TagValueEntity) []*slack.OptionBlockObject {
optionBlockObjects := make([]*slack.OptionBlockObject, 0, len(tagValues))
for _, o := range tagValues {
txt := fmt.Sprintf("%s", o.Value)
optionText := slack.NewTextBlockObject(slack.PlainTextType, txt, false, false)
optionBlockObjects = append(optionBlockObjects, slack.NewOptionBlockObject(strconv.FormatUint(uint64(o.ID), 10), optionText, nil))
}
return optionBlockObjects
}

View File

@@ -1,42 +0,0 @@
package view
import (
"github.com/slack-go/slack"
"houston/common/util"
)
func BuildRcaModal(channelID string, description string) slack.ModalViewRequest {
titleText := slack.NewTextBlockObject(slack.PlainTextType, "Incident RCA", false, false)
closeText := slack.NewTextBlockObject(slack.PlainTextType, "Close", false, false)
submitText := slack.NewTextBlockObject(slack.PlainTextType, "Submit", false, false)
//headerText := slack.NewTextBlockObject("mrkdwn", "", false, false)
//headerSection := slack.NewSectionBlock(headerText, nil, nil)
rcaText := slack.NewTextBlockObject(slack.PlainTextType, " ", false, false)
rcaPlaceholder := slack.NewTextBlockObject(slack.PlainTextType, "Write RCA here...", false, false)
rcaElement := slack.NewPlainTextInputBlockElement(rcaPlaceholder, "rca")
rcaElement.Multiline = true
rcaElement.InitialValue = description
rcaElement.MaxLength = 3000
rca := slack.NewInputBlock("RCA", rcaText, nil, rcaElement)
rca.Optional = false
blocks := slack.Blocks{
BlockSet: []slack.Block{
//headerSection,
rca,
},
}
return slack.ModalViewRequest{
Type: slack.VTModal,
Title: titleText,
Close: closeText,
Submit: submitText,
Blocks: blocks,
PrivateMetadata: channelID,
CallbackID: util.SetIncidentRcaSubmit,
}
}

View File

@@ -26,14 +26,6 @@ func NewIncidentBlock() map[string]interface{} {
Text: "Show incidents",
},
),
slack.NewButtonBlockElement(
"show_on_call_button",
"show_on_call_button_value",
&slack.TextBlockObject{
Type: slack.PlainTextType,
Text: "Show on-call",
},
),
slack.NewButtonBlockElement(
util.HelpCommand,
"help_commands_button_value",
@@ -63,9 +55,7 @@ func ExistingIncidentOptionsBlock() map[string]interface{} {
return map[string]interface{}{
"blocks": []slack.Block{
incidentSectionBlock(),
tagsSectionBlock(),
rcaSectionBlock(),
jiraLinksSectionBlock(),
},
}
}
@@ -146,213 +136,15 @@ func incidentSectionBlock() *slack.SectionBlock {
return slack.NewSectionBlock(textBlock, nil, accessoryOption, slack.SectionBlockOptionBlockID("incident"))
}
func taskSectionBlock() *slack.SectionBlock {
textBlock := &slack.TextBlockObject{
Type: "mrkdwn",
Text: "Task",
func rcaSectionBlock() *slack.ActionBlock {
buttonElement := &slack.ButtonBlockElement{
Type: "button",
ActionID: util.SetRCADetails,
Text: &slack.TextBlockObject{
Type: "plain_text",
Text: "Fill RCA details",
},
Value: util.SetRCADetails,
}
optionBlockObjects := []*slack.OptionBlockObject{
{
Text: &slack.TextBlockObject{
Type: "plain_text",
Text: "Add task",
},
Value: "addTask",
},
{
Text: &slack.TextBlockObject{
Type: "plain_text",
Text: "Assign task",
},
Value: "assignTask",
},
{
Text: &slack.TextBlockObject{
Type: "plain_text",
Text: "View your tasks",
},
Value: "viewYourTasks",
},
{
Text: &slack.TextBlockObject{
Type: "plain_text",
Text: "View all tasks",
},
Value: "viewAllTasks",
},
}
accessoryOption := &slack.Accessory{
SelectElement: &slack.SelectBlockElement{
Type: "static_select",
ActionID: "task",
Options: optionBlockObjects,
Placeholder: slack.NewTextBlockObject("plain_text", "Select command", false, false),
},
}
return slack.NewSectionBlock(textBlock, nil, accessoryOption, slack.SectionBlockOptionBlockID("task"))
}
func tagsSectionBlock() *slack.SectionBlock {
textBlock := &slack.TextBlockObject{
Type: "mrkdwn",
Text: "Tags",
}
optionBlockObjects := []*slack.OptionBlockObject{
{
Text: &slack.TextBlockObject{
Type: "plain_text",
Text: "Add tag(s)",
},
Value: util.AddTags,
},
{
Text: &slack.TextBlockObject{
Type: "plain_text",
Text: "Show tags",
},
Value: util.ShowTags,
},
{
Text: &slack.TextBlockObject{
Type: "plain_text",
Text: "Remove tag",
},
Value: util.RemoveTag,
},
}
accessoryOption := &slack.Accessory{
SelectElement: &slack.SelectBlockElement{
Type: "static_select",
ActionID: "tags",
Options: optionBlockObjects,
Placeholder: slack.NewTextBlockObject("plain_text", "Select command", false, false),
},
}
return slack.NewSectionBlock(textBlock, nil, accessoryOption, slack.SectionBlockOptionBlockID("tags"))
}
func onCallSectionBlock() *slack.SectionBlock {
textBlock := &slack.TextBlockObject{
Type: "mrkdwn",
Text: "On-call",
}
optionBlockObjects := []*slack.OptionBlockObject{
{
Text: &slack.TextBlockObject{
Type: "plain_text",
Text: "Show on-call",
},
Value: "showOncall",
},
{
Text: &slack.TextBlockObject{
Type: "plain_text",
Text: "Show escalation policy",
},
Value: "showEscalationPolicy",
},
{
Text: &slack.TextBlockObject{
Type: "plain_text",
Text: "Trigger alert",
},
Value: "triggerAlert",
},
}
accessoryOption := &slack.Accessory{
SelectElement: &slack.SelectBlockElement{
Type: "static_select",
ActionID: "on-call",
Options: optionBlockObjects,
Placeholder: slack.NewTextBlockObject("plain_text", "Select command", false, false),
},
}
return slack.NewSectionBlock(textBlock, nil, accessoryOption, slack.SectionBlockOptionBlockID("on-call"))
}
func botHelpSectionBlock() *slack.SectionBlock {
textBlock := &slack.TextBlockObject{
Type: "mrkdwn",
Text: "Bot help",
}
optionBlockObjects := []*slack.OptionBlockObject{
{
Text: &slack.TextBlockObject{
Type: "plain_text",
Text: "Help - commands",
},
Value: "helpCommands",
},
}
accessoryOption := &slack.Accessory{
SelectElement: &slack.SelectBlockElement{
Type: "static_select",
ActionID: "bot-helps",
Options: optionBlockObjects,
Placeholder: slack.NewTextBlockObject("plain_text", "Select command", false, false),
},
}
return slack.NewSectionBlock(textBlock, nil, accessoryOption, slack.SectionBlockOptionBlockID("bot-helps"))
}
func rcaSectionBlock() *slack.SectionBlock {
textBlock := &slack.TextBlockObject{
Type: "mrkdwn",
Text: "RCA",
}
optionBlockObjects := []*slack.OptionBlockObject{
{
Text: &slack.TextBlockObject{
Type: "plain_text",
Text: "Write RCA",
},
Value: util.SetRCA,
},
}
accessoryOption := &slack.Accessory{
SelectElement: &slack.SelectBlockElement{
Type: "static_select",
ActionID: util.SetRCA,
Options: optionBlockObjects,
Placeholder: slack.NewTextBlockObject("plain_text", "Select command", false, false),
},
}
return slack.NewSectionBlock(textBlock, nil, accessoryOption, slack.SectionBlockOptionBlockID("set_rca"))
}
func jiraLinksSectionBlock() *slack.SectionBlock {
textBlock := &slack.TextBlockObject{
Type: "mrkdwn",
Text: "Jira links",
}
optionBlockObjects := []*slack.OptionBlockObject{
{
Text: &slack.TextBlockObject{
Type: "plain_text",
Text: "Add Jira link(s)",
},
Value: util.SetJiraLinks,
},
}
accessoryOption := &slack.Accessory{
SelectElement: &slack.SelectBlockElement{
Type: "static_select",
ActionID: util.SetJiraLinks,
Options: optionBlockObjects,
Placeholder: slack.NewTextBlockObject("plain_text", "Select command", false, false),
},
}
return slack.NewSectionBlock(textBlock, nil, accessoryOption, slack.SectionBlockOptionBlockID(util.SetJiraLinks))
return slack.NewActionBlock(util.SetRCADetails, buttonElement)
}

View File

@@ -1,116 +0,0 @@
package view
import (
"fmt"
"houston/common/util"
"houston/model/tag"
"strconv"
"github.com/slack-go/slack"
)
func BuildIncidentUpdateTagModal(channel slack.Channel, inputBlocks []slack.InputBlock) slack.ModalViewRequest {
rcaObject := slack.NewTextBlockObject(slack.PlainTextType, "RCA", false, false)
rcaPlaceholder := slack.NewTextBlockObject(slack.PlainTextType, "Write RCA here...", false, false)
rcaElement := slack.NewPlainTextInputBlockElement(rcaPlaceholder, util.SetRCA)
rcaElement.Multiline = true
rcaBlock := slack.NewInputBlock("RCA", rcaObject, nil, rcaElement)
rcaBlock.Optional = true
titleText := slack.NewTextBlockObject(slack.PlainTextType, "Edit tags", false, false)
closeText := slack.NewTextBlockObject(slack.PlainTextType, "Close", false, false)
submitText := slack.NewTextBlockObject(slack.PlainTextType, "Submit", false, false)
var localBlocks []slack.Block
for _, block := range inputBlocks {
localBlocks = append(localBlocks, block)
}
blocks := slack.Blocks{
BlockSet: append(localBlocks, rcaBlock),
}
return slack.ModalViewRequest{
Type: slack.VTModal,
Title: titleText,
Close: closeText,
Submit: submitText,
Blocks: blocks,
PrivateMetadata: channel.ID,
CallbackID: util.UpdateTagSubmit,
}
}
func CreateInputBlock(tagEntity tag.TagDTO, tagValues []tag.TagValueEntity, initialTagValues interface{}, isOptional bool) *slack.InputBlock {
text := slack.NewTextBlockObject(slack.PlainTextType, tagEntity.Label, false, false)
var element slack.BlockElement
switch tagEntity.Type {
case tag.FreeText:
{
element = createPlainTextInputBlockElement(tagEntity, initialTagValues)
}
case tag.SingleValue:
{
element = createOptionsSelectBlockElement(tagEntity, tagValues, initialTagValues.([]tag.TagValueEntity))
}
case tag.MultiValue:
{
element = createMultiOptionsSelectBlockElement(tagEntity, tagValues, initialTagValues.([]tag.TagValueEntity))
}
default:
{
return nil
}
}
block := slack.NewInputBlock(tagEntity.Name, text, nil, element)
block.Optional = isOptional
return block
}
func createPlainTextInputBlockElement(tagEntity tag.TagDTO, value interface{}) *slack.PlainTextInputBlockElement {
placeholder := slack.NewTextBlockObject(slack.PlainTextType, tagEntity.PlaceHolder, false, false)
element := slack.NewPlainTextInputBlockElement(placeholder, tagEntity.ActionId)
if value != nil {
element.InitialValue = fmt.Sprintf("%v", value)
} else {
element.InitialValue = ""
}
return element
}
func createOptionsSelectBlockElement(tagEntity tag.TagDTO, tagValues []tag.TagValueEntity, initialTagValues []tag.TagValueEntity) *slack.SelectBlockElement {
blockOptions := createTagOptions(tagValues)
placeholder := slack.NewTextBlockObject(slack.PlainTextType, tagEntity.PlaceHolder, false, false)
element := slack.NewOptionsSelectBlockElement(slack.OptTypeStatic, placeholder, tagEntity.ActionId, blockOptions...)
if initialTagValues != nil {
element.InitialOption = createTagOptions(initialTagValues)[0]
}
return element
}
func createMultiOptionsSelectBlockElement(tagEntity tag.TagDTO, tagValues []tag.TagValueEntity, initialTagValues []tag.TagValueEntity) *slack.MultiSelectBlockElement {
blockOptions := createTagOptions(tagValues)
placeholder := slack.NewTextBlockObject(slack.PlainTextType, tagEntity.PlaceHolder, false, false)
element := slack.NewOptionsMultiSelectBlockElement(slack.MultiOptTypeStatic, placeholder, tagEntity.ActionId, blockOptions...)
if initialTagValues != nil {
element.InitialOptions = createTagOptions(initialTagValues)
}
return element
}
func createTagOptions(tagValues []tag.TagValueEntity) []*slack.OptionBlockObject {
optionBlockObjects := make([]*slack.OptionBlockObject, 0, len(tagValues))
for _, o := range tagValues {
txt := fmt.Sprintf("%s", o.Value)
optionText := slack.NewTextBlockObject(slack.PlainTextType, txt, false, false)
optionBlockObjects = append(optionBlockObjects, slack.NewOptionBlockObject(strconv.FormatUint(uint64(o.ID), 10), optionText, nil))
}
return optionBlockObjects
}

View File

@@ -0,0 +1,36 @@
package view
import (
"github.com/slack-go/slack"
)
type PlainInputBlockElementData struct {
BasicData BasicInputElementData
}
type BasicInputElementData struct {
Header string
PlaceHolder string
ActionId string
Optional bool
MultiLine bool
MaxLength int
}
type SimpleInputBlockElementData struct {
BasicData BasicInputElementData
InitialValue string
}
func CreatePlainTextInputBlock(inputData SimpleInputBlockElementData) *slack.InputBlock {
basicInputData := inputData.BasicData
title := slack.NewTextBlockObject(slack.PlainTextType, basicInputData.Header, false, false)
placeholder := slack.NewTextBlockObject(slack.PlainTextType, basicInputData.PlaceHolder, false, false)
slackElement := slack.NewPlainTextInputBlockElement(placeholder, basicInputData.ActionId)
slackElement.InitialValue = inputData.InitialValue
slackElement.Multiline = basicInputData.MultiLine
slackElement.MaxLength = basicInputData.MaxLength
slackBlock := slack.NewInputBlock(basicInputData.Header, title, nil, slackElement)
slackBlock.Optional = basicInputData.Optional
return slackBlock
}

View File

@@ -37,13 +37,10 @@ type BlockActionProcessor struct {
incidentUpdateSeverityAction *action.IncidentUpdateSevertityAction
incidentUpdateTitleAction *action.IncidentUpdateTitleAction
incidentUpdateDescriptionAction *action.IncidentUpdateDescriptionAction
incidentUpdateTagsAction *action.IncidentUpdateTagsAction
incidentShowTagsAction *action.IncidentShowTagsAction
incidentUpdateRcaAction *action.IncidentUpdateRcaAction
incidentUpdateJiraLinksAction *action.IncidentUpdateJiraLinksAction
incidentDuplicateAction *action.DuplicateIncidentAction
incidentServiceV2 *incidentService.IncidentServiceV2
slackService *slack2.SlackService
incidentRCASectionAction *action.IncidentRCASectionAction
}
func NewBlockActionProcessor(
@@ -76,15 +73,9 @@ func NewBlockActionProcessor(
incidentUpdateTitleAction: action.NewIncidentUpdateTitleAction(socketModeClient, incidentRepository,
teamService, severityService, slackbotClient),
incidentUpdateDescriptionAction: action.NewIncidentUpdateDescriptionAction(socketModeClient, incidentRepository),
incidentUpdateTagsAction: action.NewIncidentUpdateTagsAction(socketModeClient, incidentRepository,
teamService, tagService),
incidentShowTagsAction: action.NewIncidentShowTagsProcessor(socketModeClient, incidentRepository,
tagService),
incidentUpdateRcaAction: action.NewIncidentUpdateRcaAction(socketModeClient, incidentRepository),
incidentUpdateJiraLinksAction: action.NewIncidentUpdateJiraLinksAction(socketModeClient, incidentRepository,
incidentServiceV2, slackService),
incidentDuplicateAction: action.NewDuplicateIncidentProcessor(socketModeClient, incidentRepository,
tagService, teamService, severityService),
incidentRCASectionAction: action.NewIncidentRCASectionAction(socketModeClient, incidentRepository, teamService, tagService, severityService, action.NewIncidentTagsAction(incidentRepository, tagService), action.NewIncidentRCASummaryAction(incidentRepository), action.NewIncidentJiraLinksAction(incidentServiceV2, slackService), rcaService),
}
}
@@ -120,17 +111,9 @@ func (bap *BlockActionProcessor) ProcessCommand(callback slack.InteractionCallba
bap.processIncidentCommands(callback, request)
}
case util.Tags:
case util.SetRCADetails:
{
bap.processTagsCommands(callback, request)
}
case util.SetRCA:
{
bap.processRcaCommands(callback, request)
}
case util.SetJiraLinks:
{
bap.processJiraLinkCommands(callback, request)
bap.processRCASectionCommands(callback, request)
}
default:
{
@@ -157,7 +140,7 @@ func (bap *BlockActionProcessor) processIncidentCommands(callback slack.Interact
}
case util.ResolveIncident:
{
bap.incidentResolveAction.IncidentResolveProcess(callback, request)
bap.incidentRCASectionAction.ProcessIncidentRCAActionRequest(callback, request, util.IncidentResolveSubmit)
}
case util.SetIncidentStatus:
{
@@ -186,40 +169,16 @@ func (bap *BlockActionProcessor) processIncidentCommands(callback slack.Interact
}
}
func (bap *BlockActionProcessor) processRcaCommands(callback slack.InteractionCallback, request *socketmode.Request) {
action1 := util.BlockActionType(callback.ActionCallback.BlockActions[0].SelectedOption.Value)
func (bap *BlockActionProcessor) processRCASectionCommands(callback slack.InteractionCallback, request *socketmode.Request) {
action1 := util.BlockActionType(callback.ActionCallback.BlockActions[0].ActionID)
switch action1 {
case util.SetRCA:
case util.SetRCADetails:
{
bap.incidentUpdateRcaAction.IncidentUpdateRcaRequestProcess(callback, request)
bap.incidentRCASectionAction.ProcessIncidentRCAActionRequest(callback, request, util.SetIncidentRCADetailsSubmit)
}
}
}
func (bap *BlockActionProcessor) processJiraLinkCommands(callback slack.InteractionCallback, request *socketmode.Request) {
action1 := util.BlockActionType(callback.ActionCallback.BlockActions[0].SelectedOption.Value)
switch action1 {
case util.SetJiraLinks:
case util.ShowRCADetails:
{
bap.incidentUpdateJiraLinksAction.IncidentUpdateJiraLinksRequestProcess(callback, request)
}
}
}
func (bap *BlockActionProcessor) processTagsCommands(callback slack.InteractionCallback, request *socketmode.Request) {
action1 := util.BlockActionType(callback.ActionCallback.BlockActions[0].SelectedOption.Value)
switch action1 {
case util.AddTags:
{
bap.incidentUpdateTagsAction.IncidentUpdateTagsRequestProcess(callback, request)
}
case util.ShowTags:
{
bap.incidentShowTagsAction.IncidentShowTagsRequestProcess(callback, request)
}
case util.RemoveTag:
{
bap.incidentUpdateTagsAction.IncidentUpdateTagsRequestProcess(callback, request)
bap.incidentRCASectionAction.PerformShowRCADetailsAction(callback, request)
}
}
}
@@ -234,12 +193,11 @@ type ViewSubmissionProcessor struct {
incidentUpdateDescriptionAction *action.IncidentUpdateDescriptionAction
incidentUpdateSeverityAction *action.IncidentUpdateSevertityAction
incidentUpdateTypeAction *action.IncidentUpdateTypeAction
incidentUpdateTagsAction *action.IncidentUpdateTagsAction
incidentUpdateRca *action.IncidentUpdateRcaAction
incidentUpdateJiraLinks *action.IncidentUpdateJiraLinksAction
incidentAdditionalAction *action.IncidentRCASectionAction
showIncidentSubmitAction *action.ShowIncidentsSubmitAction
incidentDuplicateAction *action.DuplicateIncidentAction
db *gorm.DB
incidentRCAAction *action.IncidentRCASectionAction
}
func NewViewSubmissionProcessor(
@@ -251,6 +209,7 @@ func NewViewSubmissionProcessor(
teamRepository *team.Repository,
slackbotClient *slackbot.Client,
db *gorm.DB,
rcaService *rca.RcaService,
incidentServiceV2 *incidentService.IncidentServiceV2,
) *ViewSubmissionProcessor {
slackService := slack2.NewSlackService()
@@ -270,15 +229,11 @@ func NewViewSubmissionProcessor(
severityService, teamService, slackbotClient, incidentServiceV2),
incidentUpdateTypeAction: action.NewIncidentUpdateTypeAction(socketModeClient, incidentRepository,
teamService, severityService, slackbotClient, incidentServiceV2),
incidentUpdateTagsAction: action.NewIncidentUpdateTagsAction(socketModeClient, incidentRepository,
teamService, tagService),
incidentUpdateRca: action.NewIncidentUpdateRcaAction(socketModeClient, incidentRepository),
incidentUpdateJiraLinks: action.NewIncidentUpdateJiraLinksAction(socketModeClient, incidentRepository,
incidentServiceV2, slackService),
showIncidentSubmitAction: action.NewShowIncidentsSubmitAction(socketModeClient, incidentRepository,
teamRepository),
incidentDuplicateAction: action.NewDuplicateIncidentProcessor(socketModeClient, incidentRepository,
tagService, teamRepository, severityService),
incidentRCAAction: action.NewIncidentRCASectionAction(socketModeClient, incidentRepository, teamService, tagService, severityService, action.NewIncidentTagsAction(incidentRepository, tagService), action.NewIncidentRCASummaryAction(incidentRepository), action.NewIncidentJiraLinksAction(incidentServiceV2, slackService), rcaService),
}
}
@@ -324,17 +279,13 @@ func (vsp *ViewSubmissionProcessor) ProcessCommand(callback slack.InteractionCal
{
vsp.incidentUpdateTypeAction.IncidentUpdateType(callback, request, callback.Channel, callback.User)
}
case util.UpdateTagSubmit:
case util.IncidentResolveSubmit:
{
vsp.incidentUpdateTagsAction.IncidentUpdateTags(callback, request)
vsp.incidentRCAAction.PerformSetIncidentRCADetailsAction(callback, request, util.IncidentResolveSubmit)
}
case util.SetIncidentRcaSubmit:
case util.SetIncidentRCADetailsSubmit:
{
vsp.incidentUpdateRca.IncidentUpdateRca(callback, request, callback.Channel, callback.User)
}
case util.SetIncidentJiraLinksSubmit:
{
vsp.incidentUpdateJiraLinks.IncidentUpdateJiraLinks(callback, request, callback.Channel, callback.User)
vsp.incidentRCAAction.PerformSetIncidentRCADetailsAction(callback, request, util.SetIncidentRCADetailsSubmit)
}
case util.ShowIncidentSubmit:
{

View File

@@ -6,6 +6,7 @@ import (
"houston/internal/processor/action"
"houston/logger"
"houston/pkg/slackbot"
"houston/service/rca"
)
type OpenFillRCAViewModalCommandProcessor struct {
@@ -18,10 +19,12 @@ const openSetRCAViewModalCommandProcessorLogTag = "[open_set_rca_view_modal_comm
func NewOpenFillRCAViewModalCommandProcessor(
socketModeClient *socketmode.Client,
slackBot *slackbot.Client,
rcaService *rca.RcaService,
) *OpenFillRCAViewModalCommandProcessor {
return &OpenFillRCAViewModalCommandProcessor{
socketModeClient: socketModeClient,
openSetRCAViewModalCommandAction: action.NewOpenFillRCAViewModalCommandAction(socketModeClient, slackBot),
openSetRCAViewModalCommandAction: action.NewOpenFillRCAViewModalCommandAction(socketModeClient, slackBot, rcaService),
}
}

View File

@@ -117,6 +117,7 @@ func (resolver *HoustonCommandResolver) Resolve(evt *socketmode.Event) processor
commandProcessor = processor.NewOpenFillRCAViewModalCommandProcessor(
resolver.socketModeClient,
resolver.slackBotClient,
resolver.rcaService,
)
case strings.HasPrefix(params, internal.HelpParam):

View File

@@ -567,6 +567,19 @@ func (r *Repository) GetIncidentTagByTagId(incidentId uint, tagId uint) (*Incide
return &incidentTag, nil
}
func (r *Repository) GetIncidentTagsByTagIds(incidentId uint, tagIds []uint) (*IncidentTagEntity, error) {
var incidentTag IncidentTagEntity
result := r.gormClient.Find(&incidentTag, "incident_id = ? and tag_id in ?", incidentId, tagIds)
if result.Error != nil {
return nil, result.Error
} else if result.RowsAffected == 0 {
return nil, nil
}
return &incidentTag, nil
}
func (r *Repository) SaveIncidentTag(entity IncidentTagEntity) (*IncidentTagEntity, error) {
tx := r.gormClient.Save(&entity)
if tx.Error != nil {

View File

@@ -12,11 +12,14 @@ const (
type TagEntity struct {
gorm.Model
Name string `gorm:"column:name"`
Label string `gorm:"column:label"`
PlaceHolder string `gorm:"column:place_holder"`
ActionId string `gorm:"column:action_id"`
Type Type `gorm:"column:type"`
Name string `gorm:"column:name"`
Label string `gorm:"column:label"`
PlaceHolder string `gorm:"column:place_holder"`
ActionId string `gorm:"column:action_id"`
Type Type `gorm:"column:type"`
Optional bool `gorm:"column:optional"`
Active bool `gorm:"column:active"`
DisplayOrder int8 `gorm:"column:display_order"`
}
func (TagEntity) TableName() string {
@@ -25,8 +28,9 @@ func (TagEntity) TableName() string {
type TagValueEntity struct {
gorm.Model
TagId uint `gorm:"column:tag_id"`
Value string `gorm:"column:value"`
TagId uint `gorm:"column:tag_id"`
Value string `gorm:"column:value"`
Active bool `gorm:"column:active"`
}
func (TagValueEntity) TableName() string {

View File

@@ -4,6 +4,15 @@ import (
"gorm.io/gorm"
)
type ITagRepository interface {
FindById(id uint) (*TagEntity, error)
FindTagValuesByTagId(id uint) (*[]TagValueEntity, error)
FindTagValuesByIds(ids []int32) (*[]TagValueEntity, error)
FindTagsByTeamId(teamId uint) (*[]TagDTO, error)
GetActiveTags() (*[]TagDTO, error)
GetTagValuesByTagValueId(tagValueIds []uint) ([]string, error)
}
type Repository struct {
gormClient *gorm.DB
}
@@ -30,7 +39,7 @@ func (r *Repository) FindById(id uint) (*TagEntity, error) {
func (r *Repository) FindTagValuesByTagId(id uint) (*[]TagValueEntity, error) {
var tagValues []TagValueEntity
tx := r.gormClient.Raw("select tv.* from tag_value tv inner join tag t on t.id = tv.tag_id where t.id = ?", id).Scan(&tagValues)
tx := r.gormClient.Raw("select tv.* from tag_value tv inner join tag t on t.id = tv.tag_id where t.id = ? and tv.active=?", id, true).Scan(&tagValues)
if tx.Error != nil {
return nil, tx.Error
@@ -66,3 +75,40 @@ func (r *Repository) FindTagsByTeamId(teamId uint) (*[]TagDTO, error) {
}
return &tags, nil
}
func (r *Repository) GetActiveTags() (*[]TagDTO, error) {
var tags []TagDTO
tx := r.gormClient.Table(TagEntity{}.TableName()).Where("active = ?", true).Order("display_order").Find(&tags)
if tx.Error != nil {
return nil, tx.Error
}
if tx.RowsAffected == 0 {
return nil, nil
}
return &tags, nil
}
func (r *Repository) GetMandatoryActiveTags() (*[]TagDTO, error) {
var tags []TagDTO
tx := r.gormClient.Table(TagEntity{}.TableName()).Where("active = ? and optional = ?", true, false).Order("display_order").Find(&tags)
if tx.Error != nil {
return nil, tx.Error
}
if tx.RowsAffected == 0 {
return nil, nil
}
return &tags, nil
}
func (r *Repository) GetTagValuesByTagValueId(tagValueIds []uint) ([]string, error) {
var tagValues []string
tx := r.gormClient.Model(TagValueEntity{}).Where("id in ?", tagValueIds).Where("active = ?", true).Pluck("value", &tagValues)
if tx.Error != nil {
return nil, tx.Error
}
if tx.RowsAffected == 0 {
return nil, nil
}
return tagValues, nil
}

View File

@@ -254,6 +254,21 @@ const (
UnLinkJira = "unLink"
)
func (i *IncidentServiceV2) UpdateIncidentJiraLinksEntity(entity *incident.IncidentEntity, user string, jiraToBeUpdated []string) error {
slackUser, err := i.slackService.GetUserByEmailOrID(user)
entity.JiraLinks = jiraToBeUpdated
entity.UpdatedBy = slackUser.ID
entity.UpdatedAt = time.Now()
err = i.incidentRepository.UpdateIncident(entity)
if err != nil {
errorMessage := fmt.Sprintf("%s failed to update JIRA IDs for incident %s", logTag, entity.IncidentName)
logger.Error(errorMessage)
return fmt.Errorf(errorMessage, err)
}
return nil
}
func (i *IncidentServiceV2) updateJiraIDs(entity *incident.IncidentEntity, user, logTag, action string, jiraLinks ...string) error {
slackUser, err := i.slackService.GetUserByEmailOrID(user)
var updatedBy string

View File

@@ -13,5 +13,6 @@ type IIncidentService interface {
UnLinkJiraFromIncident(incidentId uint, unLinkedBy, jiraLink string) error
GetAllOpenIncidents() ([]incident.IncidentEntity, int, error)
GetIncidentRoleByIncidentIdAndRole(incidentId uint, role string) (*incident.IncidentRoleEntity, error)
UpdateIncidentJiraLinksEntity(incidentEntity *incident.IncidentEntity, updatedBy string, jiraLinks []string) error
IsHoustonChannel(channelID string) (bool, error)
}

View File

@@ -503,6 +503,7 @@ func splitUsers(users []string, chunkSize int) [][]string {
}
return result
}
func convertToConversationResponse(message slack.Message) service.ConversationResponse {
return service.ConversationResponse{
UserName: message.User,
@@ -511,6 +512,20 @@ func convertToConversationResponse(message slack.Message) service.ConversationRe
}
}
func (s *SlackService) AckRequest(request socketmode.Request) {
var payload interface{}
s.SocketModeClient.Ack(request, payload)
}
func (s *SlackService) OpenView(callback slack.InteractionCallback, modalRequest slack.ModalViewRequest, action string) {
_, err := s.SocketModeClient.OpenView(callback.TriggerID, modalRequest)
if err != nil {
logger.Error(fmt.Sprintf("houston slackbot open view command for %s failed.", action),
zap.String("trigger_id", callback.TriggerID), zap.String("channel_id", callback.Channel.ID), zap.Error(err))
return
}
}
func (s *SlackService) GetConversationInfo(channelId string) (*slack.Channel, error) {
channel, err := s.SocketModeClient.GetConversationInfo(&slack.GetConversationInfoInput{
ChannelID: channelId,