diff --git a/CHANGELOG.md b/CHANGELOG.md index 4cb0465a..3ea91a87 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Fixed + +- Fix Slack direct paging issue when there are more than 100 schedules by @vadimkerr ([#2594](https://github.com/grafana/oncall/pull/2594)) + ## v1.3.15 (2023-07-19) ### Changed diff --git a/engine/apps/slack/scenarios/paging.py b/engine/apps/slack/scenarios/paging.py index 09aec8ed..41a12bf4 100644 --- a/engine/apps/slack/scenarios/paging.py +++ b/engine/apps/slack/scenarios/paging.py @@ -43,6 +43,9 @@ ITEM_ACTIONS = ( SCHEDULES_DATA_KEY = "schedules" USERS_DATA_KEY = "users" +# https://api.slack.com/reference/block-kit/block-elements#static_select +MAX_STATIC_SELECT_OPTIONS = 100 + def add_or_update_item(payload, key, item_pk, policy): metadata = json.loads(payload["view"]["private_metadata"]) @@ -639,7 +642,7 @@ def _get_additional_responders_blocks( return blocks -def _get_users_select(organization, input_id_prefix, action_id): +def _get_users_select(organization, input_id_prefix, action_id, max_options_per_group=MAX_STATIC_SELECT_OPTIONS): users = organization.users.all() user_options = [ @@ -667,27 +670,16 @@ def _get_users_select(organization, input_id_prefix, action_id): "action_id": action_id, }, } - MAX_STATIC_SELECT_OPTIONS = 100 - if len(user_options) > MAX_STATIC_SELECT_OPTIONS: - # paginate user options in groups - max_length = MAX_STATIC_SELECT_OPTIONS - chunks = [user_options[x : x + max_length] for x in range(0, len(user_options), max_length)] - option_groups = [ - { - "label": {"type": "plain_text", "text": f"({(i * max_length)+1}-{(i * max_length)+max_length})"}, - "options": group, - } - for i, group in enumerate(chunks) - ] - user_select["accessory"]["option_groups"] = option_groups + if len(user_options) > max_options_per_group: + user_select["accessory"]["option_groups"] = _get_option_groups(user_options, max_options_per_group) else: user_select["accessory"]["options"] = user_options return user_select -def _get_schedules_select(organization, input_id_prefix, action_id): +def _get_schedules_select(organization, input_id_prefix, action_id, max_options_per_group=MAX_STATIC_SELECT_OPTIONS): schedules = organization.oncall_schedules.all() schedule_options = [ @@ -701,23 +693,46 @@ def _get_schedules_select(organization, input_id_prefix, action_id): } for schedule in schedules ] + if not schedule_options: - schedule_select = {"type": "context", "elements": [{"type": "mrkdwn", "text": "No schedules available"}]} + return {"type": "context", "elements": [{"type": "mrkdwn", "text": "No schedules available"}]} + + schedule_select = { + "type": "section", + "text": {"type": "mrkdwn", "text": "Notify schedule"}, + "block_id": input_id_prefix + DIRECT_PAGING_SCHEDULE_SELECT_ID, + "accessory": { + "type": "static_select", + "placeholder": {"type": "plain_text", "text": "Select schedule", "emoji": True}, + "action_id": action_id, + }, + } + + if len(schedule_options) > max_options_per_group: + schedule_select["accessory"]["option_groups"] = _get_option_groups(schedule_options, max_options_per_group) else: - schedule_select = { - "type": "section", - "text": {"type": "mrkdwn", "text": "Notify schedule"}, - "block_id": input_id_prefix + DIRECT_PAGING_SCHEDULE_SELECT_ID, - "accessory": { - "type": "static_select", - "placeholder": {"type": "plain_text", "text": "Select schedule", "emoji": True}, - "options": schedule_options, - "action_id": action_id, - }, - } + schedule_select["accessory"]["options"] = schedule_options + return schedule_select +def _get_option_groups(options, max_options_per_group): + chunks = [options[x : x + max_options_per_group] for x in range(0, len(options), max_options_per_group)] + + option_groups = [] + for idx, group in enumerate(chunks): + start = idx * max_options_per_group + 1 + end = idx * max_options_per_group + max_options_per_group + option_groups.append( + { + "label": {"type": "plain_text", "text": f"({start}-{end})"}, + "options": group, + } + ) + + return option_groups + + def _get_selected_entries_list(input_id_prefix, key, entries): current_entries = [] for entry, policy in entries: diff --git a/engine/apps/slack/tests/test_scenario_steps/test_manage_responders.py b/engine/apps/slack/tests/test_scenario_steps/test_manage_responders.py index 81ba6e58..4f4d74ff 100644 --- a/engine/apps/slack/tests/test_scenario_steps/test_manage_responders.py +++ b/engine/apps/slack/tests/test_scenario_steps/test_manage_responders.py @@ -16,6 +16,7 @@ from apps.slack.scenarios.manage_responders import ( ManageRespondersUserChange, StartManageResponders, ) +from apps.slack.scenarios.paging import _get_schedules_select, _get_users_select ORGANIZATION_ID = 12 ALERT_GROUP_ID = 42 @@ -205,3 +206,41 @@ def test_remove_user(manage_responders_setup): assert mock_slack_api_call.call_args.args == ("views.update",) # check there's no list of users in the view assert mock_slack_api_call.call_args.kwargs["view"]["blocks"][0]["accessory"]["type"] != "button" + + +@pytest.mark.django_db +def test_get_users_select(make_organization, make_user): + organization = make_organization() + for _ in range(3): + make_user(organization=organization) + + select_options = _get_users_select(organization=organization, input_id_prefix="test", action_id="test") + assert len(select_options["accessory"]["options"]) == 3 + assert "option_groups" not in select_options["accessory"] + + select_option_groups = _get_users_select( + organization=organization, input_id_prefix="test", action_id="test", max_options_per_group=2 + ) + assert len(select_option_groups["accessory"]["option_groups"]) == 2 + assert len(select_option_groups["accessory"]["option_groups"][0]["options"]) == 2 + assert len(select_option_groups["accessory"]["option_groups"][1]["options"]) == 1 + assert "options" not in select_option_groups["accessory"] + + +@pytest.mark.django_db +def test_get_schedules_select(make_organization, make_schedule): + organization = make_organization() + for _ in range(3): + make_schedule(organization, schedule_class=OnCallScheduleWeb) + + select_options = _get_schedules_select(organization=organization, input_id_prefix="test", action_id="test") + assert len(select_options["accessory"]["options"]) == 3 + assert "option_groups" not in select_options["accessory"] + + select_option_groups = _get_schedules_select( + organization=organization, input_id_prefix="test", action_id="test", max_options_per_group=2 + ) + assert len(select_option_groups["accessory"]["option_groups"]) == 2 + assert len(select_option_groups["accessory"]["option_groups"][0]["options"]) == 2 + assert len(select_option_groups["accessory"]["option_groups"][1]["options"]) == 1 + assert "options" not in select_option_groups["accessory"]