diff --git a/docs/sources/user-and-team-management/_index.md b/docs/sources/user-and-team-management/_index.md
index 8dd416a5..40ee6923 100644
--- a/docs/sources/user-and-team-management/_index.md
+++ b/docs/sources/user-and-team-management/_index.md
@@ -53,6 +53,7 @@ To learn more about RBAC for Grafana OnCall, refer to the following documentatio
- [Manage RBAC roles](https://grafana.com/docs/grafana/latest/administration/roles-and-permissions/access-control/manage-rbac-roles/#update-basic-role-permissions)
- [RBAC permissions, actions, and scopes](https://grafana.com/docs/grafana/latest/administration/roles-and-permissions/access-control/custom-role-actions-scopes/)
+- [RBAC role definitions](https://grafana.com/docs/grafana/latest/administration/roles-and-permissions/access-control/rbac-fixed-basic-role-definitions/#grafana-oncall-roles-beta)
#### Available Grafana OnCall RBAC roles + granted actions
diff --git a/engine/apps/alerts/paging.py b/engine/apps/alerts/paging.py
index d17f6f03..710ac486 100644
--- a/engine/apps/alerts/paging.py
+++ b/engine/apps/alerts/paging.py
@@ -42,7 +42,7 @@ def _trigger_alert(
deleted_at=None,
defaults={
"author": from_user,
- "verbal_name": "Direct paging",
+ "verbal_name": f"Direct paging ({team.name if team else 'No'} team)",
},
)
if alert_receive_channel.default_channel_filter is None:
@@ -90,7 +90,7 @@ def _trigger_alert(
return alert.group
-def check_user_availability(user: User, team: Team) -> list[dict[str, Any]]:
+def check_user_availability(user: User) -> list[dict[str, Any]]:
"""Check user availability to be paged.
Return a warnings list indicating `error` and any additional related `data`.
@@ -108,7 +108,6 @@ def check_user_availability(user: User, team: Team) -> list[dict[str, Any]]:
schedules = OnCallSchedule.objects.filter(
Q(cached_ical_file_primary__contains=user.username) | Q(cached_ical_file_primary__contains=user.email),
organization=user.organization,
- team=team,
)
schedules_data = {}
for s in schedules:
diff --git a/engine/apps/alerts/tasks/check_escalation_finished.py b/engine/apps/alerts/tasks/check_escalation_finished.py
index 343696c9..b0cfee9e 100644
--- a/engine/apps/alerts/tasks/check_escalation_finished.py
+++ b/engine/apps/alerts/tasks/check_escalation_finished.py
@@ -34,10 +34,19 @@ def audit_alert_group_escalation(alert_group: "AlertGroup") -> None:
alert_group_id = alert_group.id
base_msg = f"Alert group {alert_group_id}"
- if not escalation_snapshot:
- raise AlertGroupEscalationPolicyExecutionAuditException(
- f"{base_msg} does not have an escalation snapshot associated with it, this should never occur"
+ if not alert_group.escalation_chain_exists:
+ task_logger.info(
+ f"{base_msg} does not have an escalation chain associated with it, and therefore it is expected "
+ "that it will not have an escalation snapshot, skipping further validation"
)
+ return
+
+ if not escalation_snapshot:
+ msg = f"{base_msg} does not have an escalation snapshot associated with it, this should never occur"
+
+ task_logger.warning(msg)
+ raise AlertGroupEscalationPolicyExecutionAuditException(msg)
+
task_logger.info(f"{base_msg} has an escalation snapshot associated with it, auditing if it executed properly")
escalation_policies_snapshots = escalation_snapshot.escalation_policies_snapshots
@@ -118,8 +127,11 @@ def check_escalation_finished_task() -> None:
started_at__range=(two_days_ago, now),
)
- if not alert_groups.exists():
- task_logger.info("There are no alert groups to audit, everything is good :)")
+ task_logger.info(
+ f"There are {len(alert_groups)} alert group(s) to audit"
+ if alert_groups.exists()
+ else "There are no alert groups to audit, everything is good :)"
+ )
alert_group_ids_that_failed_audit: typing.List[str] = []
@@ -130,8 +142,10 @@ def check_escalation_finished_task() -> None:
alert_group_ids_that_failed_audit.append(str(alert_group.id))
if alert_group_ids_that_failed_audit:
- raise AlertGroupEscalationPolicyExecutionAuditException(
- f"The following alert group id(s) failed auditing: {', '.join(alert_group_ids_that_failed_audit)}"
- )
+ msg = f"The following alert group id(s) failed auditing: {', '.join(alert_group_ids_that_failed_audit)}"
+ task_logger.warning(msg)
+ raise AlertGroupEscalationPolicyExecutionAuditException(msg)
+
+ task_logger.info("There were no alert groups that failed auditing")
send_alert_group_escalation_auditor_task_heartbeat()
diff --git a/engine/apps/alerts/tests/test_check_escalation_finished_task.py b/engine/apps/alerts/tests/test_check_escalation_finished_task.py
index ab0cfaaf..8e947210 100644
--- a/engine/apps/alerts/tests/test_check_escalation_finished_task.py
+++ b/engine/apps/alerts/tests/test_check_escalation_finished_task.py
@@ -80,6 +80,27 @@ def test_send_alert_group_escalation_auditor_task_heartbeat_raises_an_exception_
mock_requests.get.return_value.raise_for_status.assert_called_once_with()
+@pytest.mark.django_db
+def test_audit_alert_group_escalation_skips_validation_if_the_alert_group_does_not_have_an_escalation_chain(
+ make_organization_and_user,
+ make_alert_receive_channel,
+ make_alert_group,
+):
+ organization, _ = make_organization_and_user()
+ alert_receive_channel = make_alert_receive_channel(organization)
+ alert_group = make_alert_group(alert_receive_channel)
+
+ alert_group.escalation_snapshot = None
+ alert_group.save()
+
+ assert alert_group.escalation_chain_exists is False
+
+ try:
+ audit_alert_group_escalation(alert_group)
+ except AlertGroupEscalationPolicyExecutionAuditException:
+ pytest.fail()
+
+
@pytest.mark.django_db
def test_audit_alert_group_escalation_raises_exception_if_the_alert_group_does_not_have_an_escalation_snapshot(
escalation_snapshot_test_setup,
diff --git a/engine/apps/alerts/tests/test_paging.py b/engine/apps/alerts/tests/test_paging.py
index daa73a6a..901f655d 100644
--- a/engine/apps/alerts/tests/test_paging.py
+++ b/engine/apps/alerts/tests/test_paging.py
@@ -70,7 +70,7 @@ def test_check_user_availability_no_policies(make_organization, make_user_for_or
organization = make_organization()
user = make_user_for_organization(organization)
- warnings = check_user_availability(user, None)
+ warnings = check_user_availability(user)
assert warnings == [
{"data": {}, "error": USER_HAS_NO_NOTIFICATION_POLICY},
{"data": {"schedules": {}}, "error": USER_IS_NOT_ON_CALL},
@@ -95,40 +95,12 @@ def test_check_user_availability_not_on_call(
make_schedule, make_on_call_shift, organization, None, other_user, extra_users=[user]
)
- warnings = check_user_availability(user, None)
+ warnings = check_user_availability(user)
assert warnings == [
{"data": {"schedules": {schedule.name: {other_user.public_primary_key}}}, "error": USER_IS_NOT_ON_CALL},
]
-@pytest.mark.django_db
-def test_check_user_availability_on_call_different_team(
- make_organization,
- make_team,
- make_user_for_organization,
- make_user_notification_policy,
- make_schedule,
- make_on_call_shift,
-):
- organization = make_organization()
- some_team = make_team(organization)
- user = make_user_for_organization(organization)
- make_user_notification_policy(
- user=user,
- step=UserNotificationPolicy.Step.NOTIFY,
- notify_by=UserNotificationPolicy.NotificationChannel.SMS,
- )
-
- # setup on call schedule
- # user is on call, but on a different team
- setup_always_on_call_schedule(make_schedule, make_on_call_shift, organization, some_team, user)
-
- warnings = check_user_availability(user, None)
- assert warnings == [
- {"data": {"schedules": {}}, "error": USER_IS_NOT_ON_CALL},
- ]
-
-
@pytest.mark.django_db
def test_check_user_availability_on_call(
make_organization,
@@ -150,7 +122,7 @@ def test_check_user_availability_on_call(
# setup on call schedule
setup_always_on_call_schedule(make_schedule, make_on_call_shift, organization, some_team, user)
- warnings = check_user_availability(user, some_team)
+ warnings = check_user_availability(user)
assert warnings == []
diff --git a/engine/apps/api/serializers/paging.py b/engine/apps/api/serializers/paging.py
index b764f7ef..1f69b66a 100644
--- a/engine/apps/api/serializers/paging.py
+++ b/engine/apps/api/serializers/paging.py
@@ -49,7 +49,7 @@ class DirectPagingSerializer(serializers.Serializer):
alert_group = serializers.HiddenField(default=None) # set in DirectPagingSerializer.validate
title = serializers.CharField(required=False, default=None)
- message = serializers.CharField(required=False, default=None)
+ message = serializers.CharField(required=False, default=None, allow_null=True)
team = TeamPrimaryKeyRelatedField(allow_null=True, default=CurrentTeamDefault())
diff --git a/engine/apps/api/views/user.py b/engine/apps/api/views/user.py
index 01f6fc37..629990a6 100644
--- a/engine/apps/api/views/user.py
+++ b/engine/apps/api/views/user.py
@@ -647,7 +647,7 @@ class UserView(
@action(detail=True, methods=["get"])
def check_availability(self, request, pk) -> Response:
user = self.get_object()
- warnings = check_user_availability(user=user, team=request.user.current_team)
+ warnings = check_user_availability(user=user)
return Response(data={"warnings": warnings}, status=status.HTTP_200_OK)
diff --git a/engine/apps/phone_notifications/phone_provider.py b/engine/apps/phone_notifications/phone_provider.py
index ad72924a..1bff1397 100644
--- a/engine/apps/phone_notifications/phone_provider.py
+++ b/engine/apps/phone_notifications/phone_provider.py
@@ -181,4 +181,8 @@ def get_phone_provider() -> PhoneProvider:
if len(_providers) == 0:
for provider_alias, importpath in settings.PHONE_PROVIDERS.items():
_providers[provider_alias] = import_string(importpath)()
+
+ if live_settings.PHONE_PROVIDER not in settings.PHONE_PROVIDERS.keys():
+ return _providers[settings.DEFAULT_PHONE_PROVIDER]
+
return _providers[live_settings.PHONE_PROVIDER]
diff --git a/engine/apps/slack/scenarios/manual_incident.py b/engine/apps/slack/scenarios/manual_incident.py
index bca16d62..20a5c340 100644
--- a/engine/apps/slack/scenarios/manual_incident.py
+++ b/engine/apps/slack/scenarios/manual_incident.py
@@ -20,6 +20,7 @@ DEFAULT_TEAM_VALUE = "default_team"
class StartCreateIncidentFromSlashCommand(scenario_step.ScenarioStep):
"""
StartCreateIncidentFromSlashCommand triggers creation of a manual incident from the slack message via slash command
+ THIS FEATURE IS DEPRECATED AND WILL BE REMOVED IN A FUTURE RELEASE
"""
command_name = [settings.SLACK_SLASH_COMMAND_NAME]
@@ -232,6 +233,18 @@ class OnRouteChange(scenario_step.ScenarioStep):
def _get_manual_incident_form_view(routing_uid, blocks, private_metatada):
+ deprecation_blocks = [
+ {
+ "type": "header",
+ "text": {
+ "type": "plain_text",
+ "text": f":no_entry: This command is deprecated and will be removed soon. Please use {settings.SLACK_DIRECT_PAGING_SLASH_COMMAND} command instead :no_entry:",
+ "emoji": True,
+ },
+ },
+ {"type": "divider"},
+ ]
+
view = {
"type": "modal",
"callback_id": routing_uid,
@@ -248,7 +261,7 @@ def _get_manual_incident_form_view(routing_uid, blocks, private_metatada):
"type": "plain_text",
"text": "Submit",
},
- "blocks": blocks,
+ "blocks": deprecation_blocks + blocks,
"private_metadata": private_metatada,
}
diff --git a/engine/apps/slack/scenarios/paging.py b/engine/apps/slack/scenarios/paging.py
index ce9891dc..ba495db6 100644
--- a/engine/apps/slack/scenarios/paging.py
+++ b/engine/apps/slack/scenarios/paging.py
@@ -4,6 +4,7 @@ from uuid import uuid4
from django.apps import apps
from django.conf import settings
+from apps.alerts.models import AlertReceiveChannel, EscalationChain
from apps.alerts.paging import (
USER_HAS_NO_NOTIFICATION_POLICY,
USER_IS_NOT_ON_CALL,
@@ -11,19 +12,17 @@ from apps.alerts.paging import (
direct_paging,
)
from apps.slack.constants import PRIVATE_METADATA_MAX_LENGTH
-from apps.slack.models import SlackChannel
from apps.slack.scenarios import scenario_step
from apps.slack.slack_client.exceptions import SlackAPIException
DIRECT_PAGING_TEAM_SELECT_ID = "paging_team_select"
DIRECT_PAGING_ORG_SELECT_ID = "paging_org_select"
-DIRECT_PAGING_ESCALATION_SELECT_ID = "paging_escalation_select"
DIRECT_PAGING_USER_SELECT_ID = "paging_user_select"
DIRECT_PAGING_SCHEDULE_SELECT_ID = "paging_schedule_select"
DIRECT_PAGING_TITLE_INPUT_ID = "paging_title_input"
DIRECT_PAGING_MESSAGE_INPUT_ID = "paging_message_input"
+DIRECT_PAGING_ADDITIONAL_RESPONDERS_INPUT_ID = "paging_additional_responders_input"
-DEFAULT_NO_ESCALATION_VALUE = "default_no_escalation"
DEFAULT_TEAM_VALUE = "default_team"
@@ -121,22 +120,27 @@ class FinishDirectPaging(scenario_step.ScenarioStep):
private_metadata = json.loads(payload["view"]["private_metadata"])
channel_id = private_metadata["channel_id"]
input_id_prefix = private_metadata["input_id_prefix"]
- selected_organization = _get_selected_org_from_payload(payload, input_id_prefix)
- selected_team = _get_selected_team_from_payload(payload, input_id_prefix)
- selected_escalation = _get_selected_escalation_from_payload(payload, input_id_prefix)
+ selected_organization = _get_selected_org_from_payload(
+ payload, input_id_prefix, slack_team_identity, slack_user_identity
+ )
+ _, selected_team = _get_selected_team_from_payload(payload, input_id_prefix)
user = slack_user_identity.get_user(selected_organization)
- selected_users = [
- (u, p == IMPORTANT_POLICY)
- for u, p in get_current_items(payload, USERS_DATA_KEY, selected_organization.users)
- ]
- selected_schedules = [
- (s, p == IMPORTANT_POLICY)
- for s, p in get_current_items(payload, SCHEDULES_DATA_KEY, selected_organization.oncall_schedules)
- ]
+ # Only pass users/schedules if additional responders checkbox is checked
+ selected_users, selected_schedules = None, None
+ is_additional_responders_checked = _get_additional_responders_checked_from_payload(payload, input_id_prefix)
+ if is_additional_responders_checked:
+ selected_users = [
+ (u, p == IMPORTANT_POLICY)
+ for u, p in get_current_items(payload, USERS_DATA_KEY, selected_organization.users)
+ ]
+ selected_schedules = [
+ (s, p == IMPORTANT_POLICY)
+ for s, p in get_current_items(payload, SCHEDULES_DATA_KEY, selected_organization.oncall_schedules)
+ ]
- # trigger direct paging to selected users/schedules/escalation
- direct_paging(
+ # trigger direct paging to selected team + users/schedules
+ alert_group = direct_paging(
selected_organization,
selected_team,
user,
@@ -144,15 +148,16 @@ class FinishDirectPaging(scenario_step.ScenarioStep):
message,
selected_users,
selected_schedules,
- selected_escalation,
)
+ text = ":white_check_mark: Alert group *{}* created: {}".format(title, alert_group.web_link)
+
try:
self._slack_client.api_call(
"chat.postEphemeral",
channel=channel_id,
user=slack_user_identity.slack_id,
- text=":white_check_mark: Alert *{}* successfully submitted".format(title),
+ text=text,
)
except SlackAPIException as e:
if e.response["error"] == "channel_not_found":
@@ -160,7 +165,7 @@ class FinishDirectPaging(scenario_step.ScenarioStep):
"chat.postEphemeral",
channel=slack_user_identity.im_channel_id,
user=slack_user_identity.slack_id,
- text=":white_check_mark: Alert *{}* successfully submitted".format(title),
+ text=text,
)
else:
raise e
@@ -183,12 +188,21 @@ class OnPagingOrgChange(scenario_step.ScenarioStep):
)
-class OnPagingTeamChange(OnPagingOrgChange):
- """Reload form with updated team."""
+class OnPagingTeamChange(scenario_step.ScenarioStep):
+ """Set team."""
+
+ def process_scenario(self, slack_user_identity, slack_team_identity, payload):
+ view = render_dialog(slack_user_identity, slack_team_identity, payload)
+ self._slack_client.api_call(
+ "views.update",
+ trigger_id=payload["trigger_id"],
+ view=view,
+ view_id=payload["view"]["id"],
+ )
-class OnPagingEscalationChange(scenario_step.ScenarioStep):
- """Set escalation chain."""
+class OnPagingCheckAdditionalResponders(OnPagingOrgChange):
+ """Check/uncheck additional responders checkbox."""
class OnPagingUserChange(scenario_step.ScenarioStep):
@@ -199,14 +213,15 @@ class OnPagingUserChange(scenario_step.ScenarioStep):
def process_scenario(self, slack_user_identity, slack_team_identity, payload):
private_metadata = json.loads(payload["view"]["private_metadata"])
- selected_organization = _get_selected_org_from_payload(payload, private_metadata["input_id_prefix"])
- selected_team = _get_selected_team_from_payload(payload, private_metadata["input_id_prefix"])
+ selected_organization = _get_selected_org_from_payload(
+ payload, private_metadata["input_id_prefix"], slack_team_identity, slack_user_identity
+ )
selected_user = _get_selected_user_from_payload(payload, private_metadata["input_id_prefix"])
if selected_user is None:
return
# check availability
- availability_warnings = check_user_availability(selected_user, selected_team)
+ availability_warnings = check_user_availability(selected_user)
if availability_warnings:
# display warnings and require additional confirmation
view = _display_availability_warnings(payload, availability_warnings, selected_organization, selected_user)
@@ -335,93 +350,62 @@ DIVIDER_BLOCK = {"type": "divider"}
def render_dialog(slack_user_identity, slack_team_identity, payload, initial=False, error_msg=None):
private_metadata = json.loads(payload["view"]["private_metadata"])
submit_routing_uid = private_metadata.get("submit_routing_uid")
+
+ # Get organizations available to user
+ available_organizations = _get_available_organizations(slack_team_identity, slack_user_identity)
+
if initial:
# setup initial form
new_input_id_prefix = _generate_input_id_prefix()
new_private_metadata = private_metadata
new_private_metadata["input_id_prefix"] = new_input_id_prefix
- selected_organization = (
- slack_team_identity.organizations.filter(users__slack_user_identity=slack_user_identity)
- .order_by("pk")
- .distinct()
- .first()
- )
- selected_team = None
- selected_escalation = None
+ selected_organization = available_organizations.first()
+ is_team_selected, selected_team = False, None
+ is_additional_responders_checked = False
else:
# setup form using data/state
old_input_id_prefix, new_input_id_prefix, new_private_metadata = _get_and_change_input_id_prefix_from_metadata(
private_metadata
)
- selected_organization = _get_selected_org_from_payload(payload, old_input_id_prefix)
- selected_team = _get_selected_team_from_payload(payload, old_input_id_prefix)
- selected_escalation = _get_selected_escalation_from_payload(payload, old_input_id_prefix)
+ selected_organization = _get_selected_org_from_payload(
+ payload, old_input_id_prefix, slack_team_identity, slack_user_identity
+ )
+ is_team_selected, selected_team = _get_selected_team_from_payload(payload, old_input_id_prefix)
+ is_additional_responders_checked = _get_additional_responders_checked_from_payload(payload, old_input_id_prefix)
# widgets
- organization_select = _get_organization_select(
- slack_team_identity, slack_user_identity, selected_organization, new_input_id_prefix
+ team_select_blocks = _get_team_select_blocks(
+ slack_user_identity, selected_organization, is_team_selected, selected_team, new_input_id_prefix
)
- team_select = _get_team_select(slack_user_identity, selected_organization, selected_team, new_input_id_prefix)
- escalation_select = _get_escalation_select(
- selected_organization, selected_team, selected_escalation, new_input_id_prefix
+ additional_responders_blocks = _get_additional_responders_blocks(
+ payload, selected_organization, new_input_id_prefix, is_additional_responders_checked, error_msg
)
- users_select = _get_users_select(selected_organization, selected_team, new_input_id_prefix)
- schedules_select = _get_schedules_select(selected_organization, selected_team, new_input_id_prefix)
- # blocks
- blocks = [organization_select, team_select, escalation_select, users_select, schedules_select]
+ # Add title and message inputs
+ blocks = [_get_title_input(payload), _get_message_input(payload)]
- if error_msg:
- blocks += [
- {
- "type": "section",
- "block_id": "error_message",
- "text": {
- "type": "mrkdwn",
- "text": f":warning: {error_msg}",
- },
- }
- ]
+ # Add organization select if more than one organization available for user
+ if len(available_organizations) > 1:
+ organization_select = _get_organization_select(
+ available_organizations, selected_organization, new_input_id_prefix
+ )
+ blocks.append(organization_select)
- # selected items
- selected_users = get_current_items(payload, USERS_DATA_KEY, selected_organization.users)
- selected_schedules = get_current_items(payload, SCHEDULES_DATA_KEY, selected_organization.oncall_schedules)
+ # Add team select and additional responders blocks
+ blocks += team_select_blocks
+ blocks += additional_responders_blocks
- if selected_users or selected_schedules:
- blocks += [DIVIDER_BLOCK]
- blocks.extend(_get_selected_entries_list(new_input_id_prefix, USERS_DATA_KEY, selected_users))
- blocks.extend(_get_selected_entries_list(new_input_id_prefix, SCHEDULES_DATA_KEY, selected_schedules))
- blocks += [DIVIDER_BLOCK]
-
- blocks.extend([_get_title_input(payload), _get_message_input(payload)])
-
- view = _get_form_view(submit_routing_uid, blocks, json.dumps(new_private_metadata), selected_organization)
+ view = _get_form_view(submit_routing_uid, blocks, json.dumps(new_private_metadata))
return view
-def _get_form_view(routing_uid, blocks, private_metatada, organization):
- try:
- channel = organization.slack_team_identity.get_cached_channels().get(
- slack_id=organization.general_log_channel_id
- )
- additional_info = f":information_source: The alert group will be posted to the #{channel.name} Slack channel"
- except SlackChannel.DoesNotExist:
- additional_info = (
- ":information_source: The alert group will be posted to the default Slack channel if there is one setup"
- )
-
- blocks += [
- {
- "type": "context",
- "elements": [{"type": "mrkdwn", "text": additional_info}],
- }
- ]
+def _get_form_view(routing_uid, blocks, private_metadata):
view = {
"type": "modal",
"callback_id": routing_uid,
"title": {
"type": "plain_text",
- "text": "Create alert group",
+ "text": "Create Alert Group",
},
"close": {
"type": "plain_text",
@@ -430,19 +414,16 @@ def _get_form_view(routing_uid, blocks, private_metatada, organization):
},
"submit": {
"type": "plain_text",
- "text": "Submit",
+ "text": "Create",
},
"blocks": blocks,
- "private_metadata": private_metatada,
+ "private_metadata": private_metadata,
}
return view
-def _get_organization_select(slack_team_identity, slack_user_identity, value, input_id_prefix):
- organizations = slack_team_identity.organizations.filter(
- users__slack_user_identity=slack_user_identity,
- ).distinct()
+def _get_organization_select(organizations, value, input_id_prefix):
organizations_options = []
initial_option_idx = 0
for idx, org in enumerate(organizations):
@@ -452,7 +433,7 @@ def _get_organization_select(slack_team_identity, slack_user_identity, value, in
{
"text": {
"type": "plain_text",
- "text": f"{org.stack_slug}",
+ "text": f"{org.org_title}",
"emoji": True,
},
"value": f"{org.pk}",
@@ -460,41 +441,50 @@ def _get_organization_select(slack_team_identity, slack_user_identity, value, in
)
organization_select = {
- "type": "section",
- "text": {"type": "mrkdwn", "text": "Select an organization"},
+ "type": "input",
"block_id": input_id_prefix + DIRECT_PAGING_ORG_SELECT_ID,
- "accessory": {
+ "label": {
+ "type": "plain_text",
+ "text": "Organization",
+ },
+ "element": {
"type": "static_select",
- "placeholder": {"type": "plain_text", "text": "Select an organization", "emoji": True},
+ "placeholder": {"type": "plain_text", "text": "Organization", "emoji": True},
"options": organizations_options,
"action_id": OnPagingOrgChange.routing_uid(),
"initial_option": organizations_options[initial_option_idx],
},
+ "dispatch_action": True,
}
return organization_select
def _get_select_field_value(payload, prefix_id, routing_uid, field_id):
- field = payload["view"]["state"]["values"][prefix_id + field_id][routing_uid]["selected_option"]
+ try:
+ field = payload["view"]["state"]["values"][prefix_id + field_id][routing_uid]["selected_option"]
+ except KeyError:
+ return None
+
if field:
return field["value"]
-def _get_selected_org_from_payload(payload, input_id_prefix):
+def _get_selected_org_from_payload(payload, input_id_prefix, slack_team_identity, slack_user_identity):
Organization = apps.get_model("user_management", "Organization")
selected_org_id = _get_select_field_value(
payload, input_id_prefix, OnPagingOrgChange.routing_uid(), DIRECT_PAGING_ORG_SELECT_ID
)
- if selected_org_id is not None:
+ if selected_org_id is None:
+ return _get_available_organizations(slack_team_identity, slack_user_identity).first()
+ else:
org = Organization.objects.filter(pk=selected_org_id).first()
return org
-def _get_team_select(slack_user_identity, organization, value, input_id_prefix):
- teams = organization.teams.filter(
- users__slack_user_identity=slack_user_identity,
- ).distinct()
+def _get_team_select_blocks(slack_user_identity, organization, is_selected, value, input_id_prefix):
+ user = slack_user_identity.get_user(organization) # TODO: handle None
+ teams = user.available_teams
team_options = []
# Adding pseudo option for default team
@@ -503,7 +493,7 @@ def _get_team_select(slack_user_identity, organization, value, input_id_prefix):
{
"text": {
"type": "plain_text",
- "text": f"General",
+ "text": f"No team",
"emoji": True,
},
"value": DEFAULT_TEAM_VALUE,
@@ -524,73 +514,133 @@ def _get_team_select(slack_user_identity, organization, value, input_id_prefix):
)
team_select = {
- "type": "section",
- "text": {"type": "mrkdwn", "text": "Select a team"},
+ "type": "input",
"block_id": input_id_prefix + DIRECT_PAGING_TEAM_SELECT_ID,
- "accessory": {
+ "label": {
+ "type": "plain_text",
+ "text": "Team to notify",
+ },
+ "element": {
"type": "static_select",
- "placeholder": {"type": "plain_text", "text": "Select a team", "emoji": True},
- "options": team_options,
"action_id": OnPagingTeamChange.routing_uid(),
- "initial_option": team_options[initial_option_idx],
+ "placeholder": {"type": "plain_text", "text": "Select team", "emoji": True},
+ "options": team_options,
+ },
+ "dispatch_action": True,
+ }
+
+ # No context block if no team selected
+ if not is_selected:
+ return [team_select]
+
+ team_select["element"]["initial_option"] = team_options[initial_option_idx]
+ return [team_select, _get_team_select_context(organization, value)]
+
+
+def _get_team_select_context(organization, team):
+ team_name = team.name if team else "No team"
+ alert_receive_channel = AlertReceiveChannel.objects.filter(
+ organization=organization,
+ team=team,
+ integration=AlertReceiveChannel.INTEGRATION_DIRECT_PAGING,
+ ).first()
+
+ escalation_chains_exist = EscalationChain.objects.filter(
+ channel_filters__alert_receive_channel=alert_receive_channel
+ ).exists()
+
+ if not alert_receive_channel:
+ context_text = (
+ ":warning: *Direct paging integration missing*\n"
+ "The selected team doesn't have a direct paging integration configured and will not be notified. "
+ "If you proceed with the alert group, an empty direct paging integration will be created automatically for the team. "
+ "