Skip to content

Commit

Permalink
ux(slack): improve case report modal type to assignee and oncall reso…
Browse files Browse the repository at this point in the history
…lution (#5197)

* ux(slack): improve case report modal type to assignee and oncall resolution

* ux(slack): improve case report modal type to assignee and oncall resolution

* fix: assigne_email can never be None
  • Loading branch information
wssheldon authored Sep 16, 2024
1 parent c8ecf87 commit 07b5396
Show file tree
Hide file tree
Showing 4 changed files with 199 additions and 4 deletions.
15 changes: 15 additions & 0 deletions src/dispatch/plugins/dispatch_pagerduty/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
:copyright: (c) 2019 by Netflix Inc., see AUTHORS for more
:license: Apache, see LICENSE for more details.
"""

from pdpyras import APISession
from pydantic import Field, SecretStr, EmailStr
from typing import Optional
Expand Down Expand Up @@ -116,6 +117,20 @@ def get_schedule_id_from_service_id(self, service_id: str) -> Optional[str]:
log.error("Error trying to retrieve schedule_id from service_id")
log.exception(e)

def get_service_url(self, service_id: str) -> Optional[str]:
if not service_id:
return None

client = APISession(self.configuration.api_key.get_secret_value())
client.url = self.configuration.pagerduty_api_url
try:
service = get_service(client, service_id)
return service.get("html_url")
except Exception as e:
log.error(f"Error retrieving service URL for service_id {service_id}")
log.exception(e)
return None

def get_next_oncall(self, service_id: str) -> Optional[str]:
schedule_id = self.get_schedule_id_from_service_id(service_id)

Expand Down
2 changes: 2 additions & 0 deletions src/dispatch/plugins/dispatch_slack/case/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ class CaseEscalateActions(DispatchEnum):
class CaseReportActions(DispatchEnum):
submit = "case-report-submit"
project_select = "case-report-project-select"
case_type_select = "ccase-report-case-type-select"
assignee_select = "case-report-assignee-select"


class CaseShortcutCallbacks(DispatchEnum):
Expand Down
184 changes: 181 additions & 3 deletions src/dispatch/plugins/dispatch_slack/case/interactive.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
from dispatch.case import service as case_service
from dispatch.case.enums import CaseStatus, CaseResolutionReason
from dispatch.case.models import Case, CaseCreate, CaseRead, CaseUpdate
from dispatch.case.type import service as case_type_service
from dispatch.conversation import flows as conversation_flows
from dispatch.entity import service as entity_service
from dispatch.participant_role import service as participant_role_service
Expand Down Expand Up @@ -100,6 +101,7 @@
)
from dispatch.project import service as project_service
from dispatch.search.utils import create_filter_expression
from dispatch.service import flows as service_flows
from dispatch.signal import service as signal_service
from dispatch.signal.enums import SignalEngagementStatus
from dispatch.signal.models import (
Expand Down Expand Up @@ -1651,8 +1653,12 @@ def report_issue(

@app.action(CaseReportActions.project_select, middleware=[db_middleware, action_context_middleware])
def handle_report_project_select_action(
ack: Ack, body: dict, db_session: Session, context: BoltContext, client: WebClient
):
ack: Ack,
body: dict,
db_session: Session,
context: BoltContext,
client: WebClient,
) -> None:
ack()
values = body["view"]["state"]["values"]

Expand Down Expand Up @@ -1681,7 +1687,20 @@ def handle_report_project_select_action(
action_id=CaseReportActions.project_select,
dispatch_action=True,
),
case_type_select(db_session=db_session, initial_option=None, project_id=project.id),
case_type_select(
db_session=db_session,
initial_option=None,
project_id=project.id,
action_id=CaseReportActions.case_type_select,
dispatch_action=True,
),
Context(
elements=[
MarkdownText(
text="💡 Case Types determine the initial assignee based on their configured on-call schedule."
)
]
),
case_priority_select(
db_session=db_session,
project_id=project.id,
Expand All @@ -1707,6 +1726,160 @@ def handle_report_project_select_action(
)


@app.action(
CaseReportActions.case_type_select, middleware=[db_middleware, action_context_middleware]
)
def handle_report_case_type_select_action(
ack: Ack,
body: dict,
db_session: Session,
context: BoltContext,
client: WebClient,
) -> None:
ack()
values = body["view"]["state"]["values"]

project_id = values[DefaultBlockIds.project_select][CaseReportActions.project_select][
"selected_option"
]["value"]

case_type_id = values[DefaultBlockIds.case_type_select][CaseReportActions.case_type_select][
"selected_option"
]["value"]

project = project_service.get(
db_session=db_session,
project_id=project_id,
)

case_type = case_type_service.get(
db_session=db_session,
case_type_id=case_type_id,
)

assignee_email = None
assignee_slack_id = None
oncall_service_name = None
service_url = None

# Resolve the assignee based on the case type
if case_type.oncall_service:
assignee_email = service_flows.resolve_oncall(
service=case_type.oncall_service, db_session=db_session
)
oncall_service_name = case_type.oncall_service.name

oncall_plugin = plugin_service.get_active_instance(
db_session=db_session, project_id=project.id, plugin_type="oncall"
)
if not oncall_plugin:
log.debug("Unable to send email since oncall plugin is not active.")
else:
service_url = oncall_plugin.instance.get_service_url(
case_type.oncall_service.external_id
)

if assignee_email:
# Get the Slack user ID for the assignee
try:
assignee_slack_id = client.users_lookupByEmail(email=assignee_email)["user"]["id"]
except SlackApiError:
assignee_slack_id = None

blocks = [
Context(
elements=[
MarkdownText(
text="Cases are meant to triage events that do not raise to the level of incidents, but can be escalated to incidents if necessary. If you suspect a security issue and need help, please fill out this form to the best of your abilities."
)
]
),
title_input(),
description_input(),
project_select(
db_session=db_session,
initial_option={"text": project.name, "value": project.id},
action_id=CaseReportActions.project_select,
dispatch_action=True,
),
case_type_select(
db_session=db_session,
initial_option={"text": case_type.name, "value": case_type.id},
project_id=project.id,
action_id=CaseReportActions.case_type_select,
dispatch_action=True,
),
Context(
elements=[
MarkdownText(
text="💡 Case Types determine the initial assignee based on their configured on-call schedule."
)
]
),
case_priority_select(
db_session=db_session,
project_id=project.id,
initial_option=None,
optional=True,
block_id=None, # ensures state is reset
),
assignee_select(
initial_user=assignee_slack_id if assignee_slack_id else None,
action_id=CaseReportActions.assignee_select,
),
]

# Conditionally add context blocks
if oncall_service_name and assignee_email:
if service_url:
oncall_text = (
f"👩‍🚒 {assignee_email} is on-call for <{service_url}|{oncall_service_name}>"
)
else:
oncall_text = f"👩‍🚒 {assignee_email} is on-call for {oncall_service_name}"

blocks.extend(
[
Context(elements=[MarkdownText(text=oncall_text)]),
Divider(),
Context(
elements=[
MarkdownText(
text="Not who you're looking for? You can override the assignee for this case."
)
]
),
]
)
else:
blocks.extend(
[
Context(
elements=[
MarkdownText(
text="There is no on-call service associated with this case type."
)
]
),
Context(elements=[MarkdownText(text="Please select an assignee for this case.")]),
]
)

modal = Modal(
title="Open a Case",
blocks=blocks,
submit="Report",
close="Close",
callback_id=CaseReportActions.submit,
private_metadata=context["subject"].json(),
).build()

client.views_update(
view_id=body["view"]["id"],
view=modal,
)


def ack_report_case_submission_event(ack: Ack) -> None:
"""Handles the report case submission event acknowledgment."""
modal = Modal(
Expand Down Expand Up @@ -1740,6 +1913,10 @@ def handle_report_submission_event(
if form_data.get(DefaultBlockIds.case_type_select):
case_type = {"name": form_data[DefaultBlockIds.case_type_select]["name"]}

assignee_email = client.users_info(
user=form_data[DefaultBlockIds.case_assignee_select]["value"]
)["user"]["profile"]["email"]

case_in = CaseCreate(
title=form_data[DefaultBlockIds.title_input],
description=form_data[DefaultBlockIds.description_input],
Expand All @@ -1748,6 +1925,7 @@ def handle_report_submission_event(
case_type=case_type,
dedicated_channel=True,
reporter=ParticipantUpdate(individual=IndividualContactRead(email=user.email)),
assignee=ParticipantUpdate(individual=IndividualContactRead(email=assignee_email)),
)

case = case_service.create(db_session=db_session, case_in=case_in, current_user=user)
Expand Down
2 changes: 1 addition & 1 deletion src/dispatch/plugins/dispatch_slack/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -598,7 +598,7 @@ def case_type_select(
action_id: str = DefaultActionIds.case_type_select,
block_id: str = DefaultBlockIds.case_type_select,
label: str = "Case Type",
initial_option: dict = None,
initial_option: dict | None = None,
project_id: int = None,
**kwargs,
):
Expand Down

0 comments on commit 07b5396

Please sign in to comment.