diff --git a/Makefile b/Makefile index 5ffaf0e..af9833a 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/cmd/app/handler/slack_handler.go b/cmd/app/handler/slack_handler.go index f1bc8f8..be500c1 100644 --- a/cmd/app/handler/slack_handler.go +++ b/cmd/app/handler/slack_handler.go @@ -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, ), diff --git a/common/util/common_util.go b/common/util/common_util.go index c084e3c..b1f6d75 100644 --- a/common/util/common_util.go +++ b/common/util/common_util.go @@ -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 == "" diff --git a/common/util/constant.go b/common/util/constant.go index 142c963..d9eb787 100644 --- a/common/util/constant.go +++ b/common/util/constant.go @@ -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" ) diff --git a/db/migration/000008_add_tag_tagvalues_columns.up.sql b/db/migration/000008_add_tag_tagvalues_columns.up.sql new file mode 100644 index 0000000..6bf28f0 --- /dev/null +++ b/db/migration/000008_add_tag_tagvalues_columns.up.sql @@ -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); diff --git a/internal/processor/action/incident_jira_links_action.go b/internal/processor/action/incident_jira_links_action.go new file mode 100644 index 0000000..67bc345 --- /dev/null +++ b/internal/processor/action/incident_jira_links_action.go @@ -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 +} diff --git a/internal/processor/action/incident_jira_links_action_test.go b/internal/processor/action/incident_jira_links_action_test.go new file mode 100644 index 0000000..6e3518c --- /dev/null +++ b/internal/processor/action/incident_jira_links_action_test.go @@ -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) +} diff --git a/internal/processor/action/incident_rca_details_action.go b/internal/processor/action/incident_rca_details_action.go new file mode 100644 index 0000000..97e1dfc --- /dev/null +++ b/internal/processor/action/incident_rca_details_action.go @@ -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 + } +} diff --git a/internal/processor/action/incident_rca_summary_action.go b/internal/processor/action/incident_rca_summary_action.go new file mode 100644 index 0000000..f4df42b --- /dev/null +++ b/internal/processor/action/incident_rca_summary_action.go @@ -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 +} diff --git a/internal/processor/action/incident_rca_summary_action_test.go b/internal/processor/action/incident_rca_summary_action_test.go new file mode 100644 index 0000000..af113e0 --- /dev/null +++ b/internal/processor/action/incident_rca_summary_action_test.go @@ -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) +} diff --git a/internal/processor/action/incident_resolve_action.go b/internal/processor/action/incident_resolve_action.go index 1a21cc0..18a746b 100644 --- a/internal/processor/action/incident_resolve_action.go +++ b/internal/processor/action/incident_resolve_action.go @@ -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 diff --git a/internal/processor/action/incident_show_tags_action.go b/internal/processor/action/incident_show_tags_action.go deleted file mode 100644 index dfdee0f..0000000 --- a/internal/processor/action/incident_show_tags_action.go +++ /dev/null @@ -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) -} diff --git a/internal/processor/action/incident_tags_action.go b/internal/processor/action/incident_tags_action.go new file mode 100644 index 0000000..e7520fe --- /dev/null +++ b/internal/processor/action/incident_tags_action.go @@ -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 +} diff --git a/internal/processor/action/incident_tags_action_test.go b/internal/processor/action/incident_tags_action_test.go new file mode 100644 index 0000000..4b94117 --- /dev/null +++ b/internal/processor/action/incident_tags_action_test.go @@ -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) +} diff --git a/internal/processor/action/incident_update_jira-links_action.go b/internal/processor/action/incident_update_jira-links_action.go deleted file mode 100644 index 62e2de1..0000000 --- a/internal/processor/action/incident_update_jira-links_action.go +++ /dev/null @@ -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"] -} diff --git a/internal/processor/action/incident_update_resolution_text_action.go b/internal/processor/action/incident_update_resolution_text_action.go deleted file mode 100644 index 20a4cab..0000000 --- a/internal/processor/action/incident_update_resolution_text_action.go +++ /dev/null @@ -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"] -} diff --git a/internal/processor/action/incident_update_severity_action.go b/internal/processor/action/incident_update_severity_action.go index 879939f..0e24e3b 100644 --- a/internal/processor/action/incident_update_severity_action.go +++ b/internal/processor/action/incident_update_severity_action.go @@ -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) diff --git a/internal/processor/action/incident_update_tags_action.go b/internal/processor/action/incident_update_tags_action.go deleted file mode 100644 index 35453c0..0000000 --- a/internal/processor/action/incident_update_tags_action.go +++ /dev/null @@ -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) -} diff --git a/internal/processor/action/open_set_rca_view_modal_command_action.go b/internal/processor/action/open_set_rca_view_modal_command_action.go index 6a1d10c..3ea4c59 100644 --- a/internal/processor/action/open_set_rca_view_modal_command_action.go +++ b/internal/processor/action/open_set_rca_view_modal_command_action.go @@ -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 - }) } diff --git a/internal/processor/action/resolve_incident_command_action.go b/internal/processor/action/resolve_incident_command_action.go index a1718f2..7d534d3 100644 --- a/internal/processor/action/resolve_incident_command_action.go +++ b/internal/processor/action/resolve_incident_command_action.go @@ -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 - }) } diff --git a/internal/processor/action/view/incident_rca_details_section.go b/internal/processor/action/view/incident_rca_details_section.go new file mode 100644 index 0000000..faf5f6f --- /dev/null +++ b/internal/processor/action/view/incident_rca_details_section.go @@ -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 +} diff --git a/internal/processor/action/view/incident_resolution_text.go b/internal/processor/action/view/incident_resolution_text.go deleted file mode 100644 index 02fd33e..0000000 --- a/internal/processor/action/view/incident_resolution_text.go +++ /dev/null @@ -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, - } - -} diff --git a/internal/processor/action/view/incident_section.go b/internal/processor/action/view/incident_section.go index 7b42465..0354a3f 100644 --- a/internal/processor/action/view/incident_section.go +++ b/internal/processor/action/view/incident_section.go @@ -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) } diff --git a/internal/processor/action/view/incident_update_tags.go b/internal/processor/action/view/incident_update_tags.go deleted file mode 100644 index e14cca6..0000000 --- a/internal/processor/action/view/incident_update_tags.go +++ /dev/null @@ -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 -} diff --git a/internal/processor/action/view/slack_modal_helpers.go b/internal/processor/action/view/slack_modal_helpers.go new file mode 100644 index 0000000..18cbe0a --- /dev/null +++ b/internal/processor/action/view/slack_modal_helpers.go @@ -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 +} diff --git a/internal/processor/event_type_interactive_processor.go b/internal/processor/event_type_interactive_processor.go index f5c234c..4d99721 100644 --- a/internal/processor/event_type_interactive_processor.go +++ b/internal/processor/event_type_interactive_processor.go @@ -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: { diff --git a/internal/processor/open_set_rca_view_modal_command_processor.go b/internal/processor/open_set_rca_view_modal_command_processor.go index d007ebe..caeed19 100644 --- a/internal/processor/open_set_rca_view_modal_command_processor.go +++ b/internal/processor/open_set_rca_view_modal_command_processor.go @@ -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), } } diff --git a/internal/resolver/houston_command_resolver.go b/internal/resolver/houston_command_resolver.go index 4a7fe60..4d1c941 100644 --- a/internal/resolver/houston_command_resolver.go +++ b/internal/resolver/houston_command_resolver.go @@ -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): diff --git a/model/incident/incident.go b/model/incident/incident.go index 7c54cbb..d59881e 100644 --- a/model/incident/incident.go +++ b/model/incident/incident.go @@ -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 { diff --git a/model/tag/entity.go b/model/tag/entity.go index 294dcec..1a80d1b 100644 --- a/model/tag/entity.go +++ b/model/tag/entity.go @@ -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 { diff --git a/model/tag/tag.go b/model/tag/tag.go index 4e5afcf..df5e183 100644 --- a/model/tag/tag.go +++ b/model/tag/tag.go @@ -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 +} diff --git a/service/incident/incident_service_v2.go b/service/incident/incident_service_v2.go index 7d25598..a8fd574 100644 --- a/service/incident/incident_service_v2.go +++ b/service/incident/incident_service_v2.go @@ -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 diff --git a/service/incident/incident_service_v2_interface.go b/service/incident/incident_service_v2_interface.go index 971e869..d01cb1c 100644 --- a/service/incident/incident_service_v2_interface.go +++ b/service/incident/incident_service_v2_interface.go @@ -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) } diff --git a/service/slack/slack_service.go b/service/slack/slack_service.go index 0ff398e..859afc5 100644 --- a/service/slack/slack_service.go +++ b/service/slack/slack_service.go @@ -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,