# What this PR does Refactors the "invite" functionality in Slack to use direct paging and be more consistent with the web UI and `/escalate` Slack command. ## Screenshots ### Alert group buttons Before: <img width="609" alt="Screenshot 2023-07-17 at 22 40 47" src="https://github.com/grafana/oncall/assets/20116910/68fad5a4-5011-4d74-b1c7-362bdb4f8cf0"> After (replace "Invite..." dropdown with "Responders" button, swap it with the silence button): <img width="587" alt="Screenshot 2023-07-17 at 22 37 19" src="https://github.com/grafana/oncall/assets/20116910/50b42057-f46b-4558-ab1c-56c34a15af5e"> ### What happens when clicking on "Responders" The following modal opens up with a list of currently paged users and inputs to page more users/schedules: <img width="514" alt="Screenshot 2023-07-17 at 22 37 52" src="https://github.com/grafana/oncall/assets/20116910/70bd2853-d459-4343-8b25-8519ac0098f7"> This is supposed to be the Slack equivalent of this part of the web UI: <img width="601" alt="Screenshot 2023-07-17 at 22 47 17" src="https://github.com/grafana/oncall/assets/20116910/101e1229-a5c4-404f-8388-eceee3e4820f"> ## Which issue(s) this PR fixes https://github.com/grafana/oncall/issues/2336 ## Checklist - [x] Unit, integration, and e2e (if applicable) tests updated - [x] Documentation added (or `pr:no public docs` PR label added if not required) - [x] `CHANGELOG.md` updated (or `pr:no changelog` PR label added if not required)
265 lines
9.5 KiB
Python
265 lines
9.5 KiB
Python
import json
|
|
|
|
from django.apps import apps
|
|
|
|
from apps.alerts.paging import check_user_availability, direct_paging, unpage_user
|
|
from apps.slack.scenarios import scenario_step
|
|
from apps.slack.scenarios.paging import (
|
|
DIRECT_PAGING_SCHEDULE_SELECT_ID,
|
|
DIRECT_PAGING_USER_SELECT_ID,
|
|
DIVIDER_BLOCK,
|
|
_generate_input_id_prefix,
|
|
_get_availability_warnings_view,
|
|
_get_schedules_select,
|
|
_get_select_field_value,
|
|
_get_users_select,
|
|
)
|
|
from apps.slack.scenarios.step_mixins import AlertGroupActionsMixin
|
|
|
|
MANAGE_RESPONDERS_USER_SELECT_ID = "responders_user_select"
|
|
MANAGE_RESPONDERS_SCHEDULE_SELECT_ID = "responders_schedule_select"
|
|
|
|
USER_DATA_KEY = "user"
|
|
ALERT_GROUP_DATA_KEY = "alert_group_pk"
|
|
|
|
# Slack scenario steps
|
|
|
|
|
|
class StartManageResponders(AlertGroupActionsMixin, scenario_step.ScenarioStep):
|
|
"""Handle "Responders" button click."""
|
|
|
|
def process_scenario(self, slack_user_identity, slack_team_identity, payload):
|
|
alert_group = self.get_alert_group(slack_team_identity, payload)
|
|
if not self.is_authorized(alert_group):
|
|
self.open_unauthorized_warning(payload)
|
|
return
|
|
|
|
view = render_dialog(alert_group)
|
|
self._slack_client.api_call(
|
|
"views.open",
|
|
trigger_id=payload["trigger_id"],
|
|
view=view,
|
|
)
|
|
|
|
|
|
class ManageRespondersUserChange(scenario_step.ScenarioStep):
|
|
"""Handle user selection in responders modal."""
|
|
|
|
def process_scenario(self, slack_user_identity, slack_team_identity, payload):
|
|
alert_group = _get_alert_group_from_payload(payload)
|
|
selected_user = _get_selected_user_from_payload(payload)
|
|
organization = alert_group.channel.organization
|
|
|
|
# check availability
|
|
availability_warnings = check_user_availability(selected_user)
|
|
if availability_warnings:
|
|
# display warnings and require additional confirmation
|
|
view = _get_availability_warnings_view(
|
|
availability_warnings,
|
|
organization,
|
|
selected_user,
|
|
ManageRespondersConfirmUserChange.routing_uid(),
|
|
json.dumps({USER_DATA_KEY: selected_user.id, ALERT_GROUP_DATA_KEY: alert_group.pk}),
|
|
)
|
|
self._slack_client.api_call(
|
|
"views.push",
|
|
trigger_id=payload["trigger_id"],
|
|
view=view,
|
|
)
|
|
else:
|
|
# no warnings, proceed with paging
|
|
direct_paging(
|
|
organization=organization,
|
|
team=alert_group.channel.team,
|
|
from_user=slack_user_identity.get_user(organization),
|
|
users=[(selected_user, False)],
|
|
alert_group=alert_group,
|
|
)
|
|
view = render_dialog(alert_group)
|
|
self._slack_client.api_call(
|
|
"views.update",
|
|
trigger_id=payload["trigger_id"],
|
|
view=view,
|
|
view_id=payload["view"]["id"],
|
|
)
|
|
|
|
|
|
class ManageRespondersConfirmUserChange(scenario_step.ScenarioStep):
|
|
"""Handle user confirmation on availability warnings modal."""
|
|
|
|
def process_scenario(self, slack_user_identity, slack_team_identity, payload):
|
|
alert_group = _get_alert_group_from_payload(payload)
|
|
selected_user = _get_selected_user_from_payload(payload)
|
|
organization = alert_group.channel.organization
|
|
|
|
direct_paging(
|
|
organization=organization,
|
|
team=alert_group.channel.team,
|
|
from_user=slack_user_identity.get_user(organization),
|
|
users=[(selected_user, False)],
|
|
alert_group=alert_group,
|
|
)
|
|
view = render_dialog(alert_group)
|
|
self._slack_client.api_call(
|
|
"views.update",
|
|
trigger_id=payload["trigger_id"],
|
|
view=view,
|
|
view_id=payload["view"]["previous_view_id"],
|
|
)
|
|
|
|
|
|
class ManageRespondersScheduleChange(scenario_step.ScenarioStep):
|
|
"""Handle schedule selection in responders modal."""
|
|
|
|
def process_scenario(self, slack_user_identity, slack_team_identity, payload, action=None):
|
|
alert_group = _get_alert_group_from_payload(payload)
|
|
selected_schedule = _get_selected_schedule_from_payload(payload)
|
|
organization = alert_group.channel.organization
|
|
|
|
direct_paging(
|
|
organization=organization,
|
|
team=alert_group.channel.team,
|
|
from_user=slack_user_identity.get_user(organization),
|
|
schedules=[(selected_schedule, False)],
|
|
alert_group=alert_group,
|
|
)
|
|
self._slack_client.api_call(
|
|
"views.update",
|
|
trigger_id=payload["trigger_id"],
|
|
view=render_dialog(alert_group),
|
|
view_id=payload["view"]["id"],
|
|
)
|
|
|
|
|
|
class ManageRespondersRemoveUser(scenario_step.ScenarioStep):
|
|
"""Handle user removal in responders modal."""
|
|
|
|
def process_scenario(self, slack_user_identity, slack_team_identity, payload, action=None):
|
|
alert_group = _get_alert_group_from_payload(payload)
|
|
selected_user = _get_selected_user_from_payload(payload)
|
|
from_user = slack_user_identity.get_user(alert_group.channel.organization)
|
|
|
|
unpage_user(alert_group, selected_user, from_user)
|
|
view = render_dialog(alert_group)
|
|
self._slack_client.api_call(
|
|
"views.update",
|
|
trigger_id=payload["trigger_id"],
|
|
view=view,
|
|
view_id=payload["view"]["id"],
|
|
)
|
|
|
|
|
|
# slack view/blocks rendering helpers
|
|
|
|
|
|
def render_dialog(alert_group):
|
|
blocks = []
|
|
|
|
# Show list of users that are currently paged
|
|
paged_users = alert_group.get_paged_users()
|
|
for user in alert_group.get_paged_users():
|
|
blocks += [
|
|
{
|
|
"type": "section",
|
|
"text": {"type": "mrkdwn", "text": f":bust_in_silhouette: *{user.name or user.username}*"},
|
|
"accessory": {
|
|
"type": "button",
|
|
"text": {"type": "plain_text", "text": "Remove", "emoji": True},
|
|
"action_id": ManageRespondersRemoveUser.routing_uid(),
|
|
"value": str(user.pk),
|
|
},
|
|
}
|
|
]
|
|
if paged_users:
|
|
blocks += [DIVIDER_BLOCK]
|
|
|
|
# Show user and schedule dropdowns
|
|
input_id_prefix = _generate_input_id_prefix()
|
|
blocks += [
|
|
_get_users_select(alert_group.channel.organization, input_id_prefix, ManageRespondersUserChange.routing_uid())
|
|
]
|
|
blocks += [
|
|
_get_schedules_select(
|
|
alert_group.channel.organization, input_id_prefix, ManageRespondersScheduleChange.routing_uid()
|
|
)
|
|
]
|
|
|
|
view = {
|
|
"type": "modal",
|
|
"title": {
|
|
"type": "plain_text",
|
|
"text": "Additional responders",
|
|
},
|
|
"blocks": blocks,
|
|
"private_metadata": json.dumps({ALERT_GROUP_DATA_KEY: alert_group.pk, "input_id_prefix": input_id_prefix}),
|
|
}
|
|
return view
|
|
|
|
|
|
def _get_selected_user_from_payload(payload):
|
|
User = apps.get_model("user_management", "User")
|
|
|
|
try:
|
|
selected_user_id = payload["actions"][0]["value"] # "remove" button
|
|
except KeyError:
|
|
try:
|
|
# "confirm" button on availability warnings modal
|
|
selected_user_id = json.loads(payload["view"]["private_metadata"])[USER_DATA_KEY]
|
|
except KeyError:
|
|
# user select dropdown
|
|
input_id_prefix = json.loads(payload["view"]["private_metadata"])["input_id_prefix"]
|
|
selected_user_id = _get_select_field_value(
|
|
payload, input_id_prefix, ManageRespondersUserChange.routing_uid(), DIRECT_PAGING_USER_SELECT_ID
|
|
)
|
|
|
|
return User.objects.get(pk=selected_user_id)
|
|
|
|
|
|
def _get_selected_schedule_from_payload(payload):
|
|
OnCallSchedule = apps.get_model("schedules", "OnCallSchedule")
|
|
|
|
input_id_prefix = json.loads(payload["view"]["private_metadata"])["input_id_prefix"]
|
|
selected_schedule_id = _get_select_field_value(
|
|
payload, input_id_prefix, ManageRespondersScheduleChange.routing_uid(), DIRECT_PAGING_SCHEDULE_SELECT_ID
|
|
)
|
|
|
|
return OnCallSchedule.objects.get(pk=selected_schedule_id)
|
|
|
|
|
|
def _get_alert_group_from_payload(payload):
|
|
AlertGroup = apps.get_model("alerts", "AlertGroup")
|
|
alert_group_pk = json.loads(payload["view"]["private_metadata"])[ALERT_GROUP_DATA_KEY]
|
|
return AlertGroup.all_objects.get(pk=alert_group_pk)
|
|
|
|
|
|
STEPS_ROUTING = [
|
|
{
|
|
"payload_type": scenario_step.PAYLOAD_TYPE_BLOCK_ACTIONS,
|
|
"block_action_type": scenario_step.BLOCK_ACTION_TYPE_STATIC_SELECT,
|
|
"block_action_id": ManageRespondersUserChange.routing_uid(),
|
|
"step": ManageRespondersUserChange,
|
|
},
|
|
{
|
|
"payload_type": scenario_step.PAYLOAD_TYPE_VIEW_SUBMISSION,
|
|
"view_callback_id": ManageRespondersConfirmUserChange.routing_uid(),
|
|
"step": ManageRespondersConfirmUserChange,
|
|
},
|
|
{
|
|
"payload_type": scenario_step.PAYLOAD_TYPE_BLOCK_ACTIONS,
|
|
"block_action_type": scenario_step.BLOCK_ACTION_TYPE_STATIC_SELECT,
|
|
"block_action_id": ManageRespondersScheduleChange.routing_uid(),
|
|
"step": ManageRespondersScheduleChange,
|
|
},
|
|
{
|
|
"payload_type": scenario_step.PAYLOAD_TYPE_BLOCK_ACTIONS,
|
|
"block_action_type": scenario_step.BLOCK_ACTION_TYPE_BUTTON,
|
|
"block_action_id": ManageRespondersRemoveUser.routing_uid(),
|
|
"step": ManageRespondersRemoveUser,
|
|
},
|
|
{
|
|
"payload_type": scenario_step.PAYLOAD_TYPE_BLOCK_ACTIONS,
|
|
"block_action_type": scenario_step.BLOCK_ACTION_TYPE_BUTTON,
|
|
"block_action_id": StartManageResponders.routing_uid(),
|
|
"step": StartManageResponders,
|
|
},
|
|
]
|