diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 00000000..8ce70308 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +20.16.0 diff --git a/docs/sources/manage/notify/ms-teams/index.md b/docs/sources/manage/notify/ms-teams/index.md index dd5dfd9d..e6fd9bd7 100644 --- a/docs/sources/manage/notify/ms-teams/index.md +++ b/docs/sources/manage/notify/ms-teams/index.md @@ -38,7 +38,7 @@ The following is required to connect to Microsoft Teams to Grafana OnCall: - You must have Admin permissions in your Grafana Cloud instance. - You must have Owner permissions in Microsoft Teams. -- Install the Grafana OnCall app from the [Microsoft Marketplace](https://appsource.microsoft.com/en-us/product/office/WA200004307). +- Install the Grafana IRM app from the [Microsoft Marketplace](https://appsource.microsoft.com/en-us/product/office/WA200004307). ## Install Microsoft Teams integration for Grafana OnCall diff --git a/engine/apps/public_api/tests/test_alert_groups.py b/engine/apps/public_api/tests/test_alert_groups.py index 51d07421..9c88fec8 100644 --- a/engine/apps/public_api/tests/test_alert_groups.py +++ b/engine/apps/public_api/tests/test_alert_groups.py @@ -447,10 +447,9 @@ def test_pagination(settings, alert_group_public_api_setup): token, alert_groups, _, _ = alert_group_public_api_setup client = APIClient() - url = reverse("api-public:alert_groups-list") + url = "{}?perpage=1".format(reverse("api-public:alert_groups-list")) - with patch("common.api_helpers.paginators.PathPrefixedPagePagination.get_page_size", return_value=1): - response = client.get(url, HTTP_AUTHORIZATION=token) + response = client.get(url, HTTP_AUTHORIZATION=token) assert response.status_code == status.HTTP_200_OK result = response.json() diff --git a/engine/apps/telegram/client.py b/engine/apps/telegram/client.py index e469e5e9..4271ae5f 100644 --- a/engine/apps/telegram/client.py +++ b/engine/apps/telegram/client.py @@ -3,7 +3,7 @@ from typing import Optional, Tuple, Union from django.conf import settings from telegram import Bot, InlineKeyboardMarkup, Message, ParseMode -from telegram.error import BadRequest, InvalidToken, Unauthorized +from telegram.error import BadRequest, InvalidToken, TelegramError, Unauthorized from telegram.utils.request import Request from apps.alerts.models import AlertGroup @@ -27,6 +27,18 @@ class TelegramClient: if self.token is None: raise InvalidToken() + class BadRequestMessage: + CHAT_NOT_FOUND = "Chat not found" + MESSAGE_IS_NOT_MODIFIED = "Message is not modified" + MESSAGE_TO_EDIT_NOT_FOUND = "Message to edit not found" + NEED_ADMIN_RIGHTS_IN_THE_CHANNEL = "Need administrator rights in the channel chat" + MESSAGE_TO_BE_REPLIED_NOT_FOUND = "Message to be replied not found" + + class UnauthorizedMessage: + BOT_WAS_BLOCKED_BY_USER = "Forbidden: bot was blocked by the user" + INVALID_TOKEN = "Invalid token" + USER_IS_DEACTIVATED = "Forbidden: user is deactivated" + @property def api_client(self) -> Bot: return Bot(self.token, request=Request(read_timeout=15)) @@ -96,7 +108,7 @@ class TelegramClient: disable_web_page_preview=False, ) except BadRequest as e: - logger.warning("Telegram BadRequest: {}".format(e.message)) + logger.warning(f"Telegram BadRequest: {e.message}") raise return message @@ -171,3 +183,7 @@ class TelegramClient: raise Exception(f"_get_message_and_keyboard with type {message_type} is not implemented") return text, keyboard + + @staticmethod + def error_message_is(error: TelegramError, messages: list[str]) -> bool: + return error.message in messages diff --git a/engine/apps/telegram/decorators.py b/engine/apps/telegram/decorators.py index bfc29a79..655e7e4f 100644 --- a/engine/apps/telegram/decorators.py +++ b/engine/apps/telegram/decorators.py @@ -42,7 +42,7 @@ def ignore_message_unchanged(f): try: return f(*args, **kwargs) except error.BadRequest as e: - if "Message is not modified" in e.message: + if TelegramClient.error_message_is(e, [TelegramClient.BadRequestMessage.MESSAGE_IS_NOT_MODIFIED]): logger.warning( f"Tried to change Telegram message, but update is identical to original message. " f"args: {args}, kwargs: {kwargs}" @@ -59,7 +59,7 @@ def ignore_message_to_edit_deleted(f): try: return f(*args, **kwargs) except error.BadRequest as e: - if "Message to edit not found" in e.message: + if TelegramClient.error_message_is(e, [TelegramClient.BadRequestMessage.MESSAGE_TO_EDIT_NOT_FOUND]): logger.warning( f"Tried to edit Telegram message, but message was deleted. args: {args}, kwargs: {kwargs}" ) @@ -75,7 +75,7 @@ def ignore_reply_to_message_deleted(f): try: return f(*args, **kwargs) except error.BadRequest as e: - if "Replied message not found" in e.message: + if TelegramClient.error_message_is(e, [TelegramClient.BadRequestMessage.MESSAGE_TO_BE_REPLIED_NOT_FOUND]): logger.warning( f"Tried to reply to Telegram message, but message was deleted. args: {args}, kwargs: {kwargs}" ) diff --git a/engine/apps/telegram/models/connectors/channel.py b/engine/apps/telegram/models/connectors/channel.py index 49168808..49b1191a 100644 --- a/engine/apps/telegram/models/connectors/channel.py +++ b/engine/apps/telegram/models/connectors/channel.py @@ -123,15 +123,16 @@ class TelegramToOrganizationConnector(models.Model): chat_id=self.channel_chat_id, message_type=TelegramMessage.ALERT_GROUP_MESSAGE, alert_group=alert_group ) except error.BadRequest as e: - if e.message == "Need administrator rights in the channel chat": + if TelegramClient.error_message_is( + e, + [ + TelegramClient.BadRequestMessage.NEED_ADMIN_RIGHTS_IN_THE_CHANNEL, + TelegramClient.BadRequestMessage.CHAT_NOT_FOUND, + ], + ): logger.warning( f"Could not send alert group to Telegram channel with id {self.channel_chat_id} " - f"due to lack of admin rights. alert_group {alert_group.pk}" - ) - elif e.message == "Chat not found": - logger.warning( - f"Could not send alert group to Telegram channel with id {self.channel_chat_id} " - f"due to 'Chat not found'. alert_group {alert_group.pk}" + f"due to {e.message}. alert_group {alert_group.pk}" ) else: telegram_client.send_message( diff --git a/engine/apps/telegram/models/connectors/personal.py b/engine/apps/telegram/models/connectors/personal.py index ae006ed4..9726cb3e 100644 --- a/engine/apps/telegram/models/connectors/personal.py +++ b/engine/apps/telegram/models/connectors/personal.py @@ -25,6 +25,14 @@ class TelegramToUserConnector(models.Model): class Meta: unique_together = (("user", "telegram_chat_id"),) + class NotificationErrorReason: + USER_WAS_DISABLED = "Telegram user was disabled" + INVALID_TOKEN = "Invalid token" + BOT_BLOCKED_BY_USER = "Bot was blocked by the user" + FORMATTING_ERROR_IN_RENDERED_TEMPLATE = ( + "Notification sent but there was a formatting error in the rendered template" + ) + @classmethod def notify_user(cls, user: User, alert_group: AlertGroup, notification_policy: UserNotificationPolicy) -> None: try: @@ -98,7 +106,7 @@ class TelegramToUserConnector(models.Model): self.user, notification_policy, UserNotificationPolicyLogRecord.ERROR_NOTIFICATION_TELEGRAM_TOKEN_ERROR, - reason="Invalid token", + reason=self.NotificationErrorReason.INVALID_TOKEN, ) return @@ -123,7 +131,7 @@ class TelegramToUserConnector(models.Model): self.user, notification_policy, UserNotificationPolicyLogRecord.ERROR_NOTIFICATION_FORMATTING_ERROR, - reason="Notification sent but there was a formatting error in the rendered template", + reason=self.NotificationErrorReason.FORMATTING_ERROR_IN_RENDERED_TEMPLATE, ) telegram_client.send_message( chat_id=self.telegram_chat_id, @@ -131,29 +139,29 @@ class TelegramToUserConnector(models.Model): alert_group=alert_group, ) except error.Unauthorized as e: - if e.message == "Forbidden: bot was blocked by the user": + if TelegramClient.error_message_is(e, [TelegramClient.UnauthorizedMessage.BOT_WAS_BLOCKED_BY_USER]): TelegramToUserConnector.create_telegram_notification_error( alert_group, self.user, notification_policy, UserNotificationPolicyLogRecord.ERROR_NOTIFICATION_TELEGRAM_BOT_IS_DELETED, - reason="Bot was blocked by the user", + reason=self.NotificationErrorReason.BOT_BLOCKED_BY_USER, ) - elif e.message == "Invalid token": + elif TelegramClient.error_message_is(e, [TelegramClient.UnauthorizedMessage.INVALID_TOKEN]): TelegramToUserConnector.create_telegram_notification_error( alert_group, self.user, notification_policy, UserNotificationPolicyLogRecord.ERROR_NOTIFICATION_TELEGRAM_TOKEN_ERROR, - reason="Invalid token", + reason=self.NotificationErrorReason.INVALID_TOKEN, ) - elif e.message == "Forbidden: user is deactivated": + elif TelegramClient.error_message_is(e, [TelegramClient.UnauthorizedMessage.USER_IS_DEACTIVATED]): TelegramToUserConnector.create_telegram_notification_error( alert_group, self.user, notification_policy, UserNotificationPolicyLogRecord.ERROR_NOTIFICATION_TELEGRAM_USER_IS_DEACTIVATED, - reason="Telegram user was disabled", + reason=self.NotificationErrorReason.USER_WAS_DISABLED, ) else: raise e @@ -175,7 +183,7 @@ class TelegramToUserConnector(models.Model): self.user, notification_policy, UserNotificationPolicyLogRecord.ERROR_NOTIFICATION_TELEGRAM_TOKEN_ERROR, - reason="Invalid token", + reason=self.NotificationErrorReason.INVALID_TOKEN, ) return @@ -193,29 +201,29 @@ class TelegramToUserConnector(models.Model): alert_group=alert_group, ) except error.Unauthorized as e: - if e.message == "Forbidden: bot was blocked by the user": + if TelegramClient.error_message_is(e, [TelegramClient.UnauthorizedMessage.BOT_WAS_BLOCKED_BY_USER]): TelegramToUserConnector.create_telegram_notification_error( alert_group, self.user, notification_policy, UserNotificationPolicyLogRecord.ERROR_NOTIFICATION_TELEGRAM_BOT_IS_DELETED, - reason="Bot was blocked by the user", + reason=self.NotificationErrorReason.BOT_BLOCKED_BY_USER, ) - elif e.message == "Invalid token": + elif TelegramClient.error_message_is(e, [TelegramClient.UnauthorizedMessage.INVALID_TOKEN]): TelegramToUserConnector.create_telegram_notification_error( alert_group, self.user, notification_policy, UserNotificationPolicyLogRecord.ERROR_NOTIFICATION_TELEGRAM_TOKEN_ERROR, - reason="Invalid token", + reason=self.NotificationErrorReason.INVALID_TOKEN, ) - elif e.message == "Forbidden: user is deactivated": + elif TelegramClient.error_message_is(e, [TelegramClient.UnauthorizedMessage.USER_IS_DEACTIVATED]): TelegramToUserConnector.create_telegram_notification_error( alert_group, self.user, notification_policy, UserNotificationPolicyLogRecord.ERROR_NOTIFICATION_TELEGRAM_USER_IS_DEACTIVATED, - reason="Telegram user was disabled", + reason=self.NotificationErrorReason.USER_WAS_DISABLED, ) else: raise e diff --git a/engine/apps/telegram/tasks.py b/engine/apps/telegram/tasks.py index 5aeb9948..93e7e4f1 100644 --- a/engine/apps/telegram/tasks.py +++ b/engine/apps/telegram/tasks.py @@ -66,7 +66,7 @@ def edit_message(self, message_pk): try: telegram_client.edit_message(message=message) except error.BadRequest as e: - if "Message is not modified" in e.message: + if TelegramClient.error_message_is(e, [TelegramClient.BadRequestMessage.MESSAGE_IS_NOT_MODIFIED]): pass except (error.RetryAfter, error.TimedOut) as e: countdown = getattr(e, "retry_after", 3) @@ -165,20 +165,19 @@ def send_log_and_actions_message(self, channel_chat_id, group_chat_id, channel_m reply_to_message_id=reply_to_message_id, ) except error.BadRequest as e: - if e.message == "Chat not found": + if TelegramClient.error_message_is( + e, + [ + TelegramClient.BadRequestMessage.CHAT_NOT_FOUND, + TelegramClient.BadRequestMessage.MESSAGE_TO_BE_REPLIED_NOT_FOUND, + ], + ): logger.warning( f"Could not send log and actions messages to Telegram group with id {group_chat_id} " - f"due to 'Chat not found'. alert_group {alert_group.pk}" + f"due to '{e.message}'. alert_group {alert_group.pk}" ) return - elif e.message == "Message to reply not found": - logger.warning( - f"Could not send log and actions messages to Telegram group with id {group_chat_id} " - f"due to 'Message to reply not found'. alert_group {alert_group.pk}" - ) - return - else: - raise + raise @shared_dedicated_queue_retry_task( diff --git a/engine/apps/telegram/tests/test_personal_connector.py b/engine/apps/telegram/tests/test_personal_connector.py index 00a2dd7d..e9d4d193 100644 --- a/engine/apps/telegram/tests/test_personal_connector.py +++ b/engine/apps/telegram/tests/test_personal_connector.py @@ -8,7 +8,11 @@ from apps.telegram.client import TelegramClient from apps.telegram.models import TelegramMessage, TelegramToUserConnector -@patch.object(TelegramClient, "send_raw_message", side_effect=error.BadRequest("Replied message not found")) +@patch.object( + TelegramClient, + "send_raw_message", + side_effect=error.BadRequest(TelegramClient.BadRequestMessage.MESSAGE_TO_BE_REPLIED_NOT_FOUND), +) @pytest.mark.django_db def test_personal_connector_replied_message_not_found( mock_send_message, @@ -53,19 +57,19 @@ def test_personal_connector_replied_message_not_found( "side_effect,notification_error_code,reason", [ ( - error.Unauthorized("Forbidden: bot was blocked by the user"), + error.Unauthorized(TelegramClient.UnauthorizedMessage.BOT_WAS_BLOCKED_BY_USER), UserNotificationPolicyLogRecord.ERROR_NOTIFICATION_TELEGRAM_BOT_IS_DELETED, - "Bot was blocked by the user", + TelegramToUserConnector.NotificationErrorReason.BOT_BLOCKED_BY_USER, ), ( - error.Unauthorized("Invalid token"), + error.Unauthorized(TelegramClient.UnauthorizedMessage.INVALID_TOKEN), UserNotificationPolicyLogRecord.ERROR_NOTIFICATION_TELEGRAM_TOKEN_ERROR, - "Invalid token", + TelegramToUserConnector.NotificationErrorReason.INVALID_TOKEN, ), ( - error.Unauthorized("Forbidden: user is deactivated"), + error.Unauthorized(TelegramClient.UnauthorizedMessage.USER_IS_DEACTIVATED), UserNotificationPolicyLogRecord.ERROR_NOTIFICATION_TELEGRAM_USER_IS_DEACTIVATED, - "Telegram user was disabled", + TelegramToUserConnector.NotificationErrorReason.USER_WAS_DISABLED, ), ], ) diff --git a/engine/apps/telegram/tests/test_tasks.py b/engine/apps/telegram/tests/test_tasks.py index a3a7c2f8..fab84068 100644 --- a/engine/apps/telegram/tests/test_tasks.py +++ b/engine/apps/telegram/tests/test_tasks.py @@ -7,8 +7,10 @@ from apps.telegram.client import TelegramClient from apps.telegram.models import TelegramMessage from apps.telegram.tasks import send_log_and_actions_message +bad_request_error_msg = TelegramClient.BadRequestMessage.MESSAGE_TO_BE_REPLIED_NOT_FOUND -@patch.object(TelegramClient, "send_raw_message", side_effect=error.BadRequest("Message to reply not found")) + +@patch.object(TelegramClient, "send_raw_message", side_effect=error.BadRequest(bad_request_error_msg)) @pytest.mark.django_db def test_send_log_and_actions_replied_message_not_found( mock_send_message, @@ -42,6 +44,6 @@ def test_send_log_and_actions_replied_message_not_found( expected_msg = ( f"Could not send log and actions messages to Telegram group with id group_chat_id " - f"due to 'Message to reply not found'. alert_group {alert_group.pk}" + f"due to '{bad_request_error_msg}'. alert_group {alert_group.pk}" ) assert expected_msg in caplog.text diff --git a/engine/common/api_helpers/paginators.py b/engine/common/api_helpers/paginators.py index e85ed62a..5c5b508c 100644 --- a/engine/common/api_helpers/paginators.py +++ b/engine/common/api_helpers/paginators.py @@ -1,5 +1,6 @@ import typing +from django.core.paginator import EmptyPage from rest_framework.pagination import BasePagination, CursorPagination, PageNumberPagination from rest_framework.response import Response @@ -54,6 +55,44 @@ class PathPrefixedPagePagination(BasePathPrefixedPagination, PageNumberPaginatio ) return paginated_schema + def paginate_queryset(self, queryset, request, view=None): + request.build_absolute_uri = lambda: create_engine_url(request.get_full_path()) + per_page = request.query_params.get(self.page_size_query_param, self.page_size) + try: + per_page = int(per_page) + except ValueError: + per_page = self.page_size + + if per_page < 1: + per_page = self.page_size + + paginator = self.django_paginator_class(queryset, per_page) + page_number = request.query_params.get(self.page_query_param, 1) + try: + page_number = int(page_number) + except ValueError: + page_number = 1 + + if page_number < 1: + page_number = 1 + + try: + self.page = self.get_page(page_number, paginator) + except EmptyPage: + self.page = paginator.page(paginator.num_pages) + + if paginator.num_pages > 1 and self.template is not None: + self.display_page_controls = True + + self.request = request + return list(self.page) + + def get_page(self, page_number, paginator): + try: + return paginator.page(page_number) + except EmptyPage: + return paginator.page(paginator.num_pages) + class PathPrefixedCursorPagination(BasePathPrefixedPagination, CursorPagination): def get_paginated_response(self, data: PaginatedData) -> Response: diff --git a/grafana-plugin/.nvmrc b/grafana-plugin/.nvmrc deleted file mode 100644 index 922f10a1..00000000 --- a/grafana-plugin/.nvmrc +++ /dev/null @@ -1 +0,0 @@ -20.x diff --git a/grafana-plugin/package.json b/grafana-plugin/package.json index 84cbf8e3..73df3a5e 100644 --- a/grafana-plugin/package.json +++ b/grafana-plugin/package.json @@ -126,9 +126,6 @@ "webpack-cli": "^5.1.4", "webpack-livereload-plugin": "^3.0.2" }, - "engines": { - "node": "20.x" - }, "dependencies": { "@dnd-kit/core": "^6.0.8", "@dnd-kit/modifiers": "^7.0.0", diff --git a/grafana-plugin/src/containers/MSTeams/MSTeamsInstructions.tsx b/grafana-plugin/src/containers/MSTeams/MSTeamsInstructions.tsx index 1f188cf6..de29a379 100644 --- a/grafana-plugin/src/containers/MSTeams/MSTeamsInstructions.tsx +++ b/grafana-plugin/src/containers/MSTeams/MSTeamsInstructions.tsx @@ -79,20 +79,20 @@ export const MSTeamsInstructions: FC = observer((props MS Teams marketplace {' '} - and add Grafana OnCall app to your MS Teams org workspace.{' '} + and add Grafana IRM app to your MS Teams org workspace.{' '} )} {!onCallisAdded ? 2 : 1}.{' '} {personalSettings ? ( - Send a direct message to the Grafana OnCall bot using ⁠linkUser command with + Send a direct message to the Grafana IRM bot using ⁠linkUser command with following code: ) : ( - Add OnCall bot to your team channel and send this code by{' '} - @Grafana OnCall linkTeam command + Add IRM bot to your team channel and send this code by @Grafana IRM linkTeam{' '} + command )} diff --git a/grafana-plugin/src/pages/integrations/Integrations.tsx b/grafana-plugin/src/pages/integrations/Integrations.tsx index fed1b4c3..1df6f1cc 100644 --- a/grafana-plugin/src/pages/integrations/Integrations.tsx +++ b/grafana-plugin/src/pages/integrations/Integrations.tsx @@ -660,6 +660,7 @@ class _IntegrationsPage extends React.Component { const { store } = this.props; + return requestedPage !== store.filtersStore.currentTablePageNum[PAGE.Integrations]; }; @@ -696,6 +697,10 @@ class _IntegrationsPage extends React.Component { + store.filtersStore.currentTablePageNum[PAGE.Integrations] = newPage; + }); + await alertReceiveChannelStore.fetchPaginatedItems({ filters: this.getFiltersBasedOnCurrentTab(), page: newPage, @@ -703,9 +708,6 @@ class _IntegrationsPage extends React.Component this.invalidateRequestFn(newPage), }); - runInAction(() => { - store.filtersStore.currentTablePageNum[PAGE.Integrations] = newPage; - }); LocationHelper.update({ p: newPage }, 'partial'); };