Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 4 commits into from
Sep 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)),
wssheldon marked this conversation as resolved.
Show resolved Hide resolved
)

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
Loading