From 8325c662584e6349e7b8d2635845bcfc769f6b00 Mon Sep 17 00:00:00 2001 From: Joey Orlando Date: Fri, 7 Jul 2023 20:02:39 +0200 Subject: [PATCH 01/10] augment API response pagination attributes --- engine/apps/api/tests/test_oncall_shift.py | 9 +++ engine/apps/api/tests/test_schedules.py | 12 ++++ engine/apps/api/tests/test_user.py | 3 + engine/apps/public_api/tests/test_alerts.py | 3 + .../public_api/tests/test_custom_actions.py | 16 +++++- .../public_api/tests/test_escalation_chain.py | 3 + .../tests/test_escalation_policies.py | 3 + .../apps/public_api/tests/test_incidents.py | 13 ++++- .../public_api/tests/test_integrations.py | 3 + .../tests/test_personal_notification_rules.py | 9 +++ .../public_api/tests/test_resolution_notes.py | 3 + engine/apps/public_api/tests/test_routes.py | 6 ++ .../apps/public_api/tests/test_schedules.py | 3 + .../public_api/tests/test_slack_channels.py | 3 + engine/apps/public_api/tests/test_teams.py | 3 + .../apps/public_api/tests/test_user_groups.py | 16 +++++- engine/apps/public_api/tests/test_users.py | 9 +++ engine/common/api_helpers/paginators.py | 55 ++++++++++++------- 18 files changed, 147 insertions(+), 25 deletions(-) diff --git a/engine/apps/api/tests/test_oncall_shift.py b/engine/apps/api/tests/test_oncall_shift.py index 7e708daf67..e8b435ed07 100644 --- a/engine/apps/api/tests/test_oncall_shift.py +++ b/engine/apps/api/tests/test_oncall_shift.py @@ -222,6 +222,9 @@ def test_list_on_call_shift( "updated_shift": None, } ], + "current_page_number": 1, + "page_size": 50, + "total_pages": 1, } assert response.status_code == status.HTTP_200_OK @@ -280,6 +283,9 @@ def test_list_on_call_shift_filter_schedule_id( "updated_shift": None, } ], + "current_page_number": 1, + "page_size": 50, + "total_pages": 1, } assert response.status_code == status.HTTP_200_OK @@ -290,6 +296,9 @@ def test_list_on_call_shift_filter_schedule_id( "next": None, "previous": None, "results": [], + "current_page_number": 1, + "page_size": 50, + "total_pages": 1, } response = client.get( diff --git a/engine/apps/api/tests/test_schedules.py b/engine/apps/api/tests/test_schedules.py index 123851cbb4..996558b3b1 100644 --- a/engine/apps/api/tests/test_schedules.py +++ b/engine/apps/api/tests/test_schedules.py @@ -137,6 +137,9 @@ def test_get_list_schedules( "enable_web_overrides": True, }, ], + "current_page_number": 1, + "page_size": 15, + "total_pages": 1, } response = client.get(url, format="json", **make_user_auth_headers(user, token)) assert response.status_code == status.HTTP_200_OK @@ -247,6 +250,9 @@ def mock_oncall_now(qs, events_datetime): "next": next_url, "previous": previous_url, "results": [schedule], + "current_page_number": 1, + "page_size": 1, + "total_pages": 3, } assert response.json() == expected_payload @@ -334,6 +340,9 @@ def test_get_list_schedules_by_type( "next": None, "previous": None, "results": [available_schedules[schedule_type]], + "current_page_number": 1, + "page_size": 15, + "total_pages": 1, } assert response.json() == expected_payload @@ -346,6 +355,9 @@ def test_get_list_schedules_by_type( "next": None, "previous": None, "results": [available_schedules[0], available_schedules[1]], + "current_page_number": 1, + "page_size": 15, + "total_pages": 1, } assert response.json() == expected_payload diff --git a/engine/apps/api/tests/test_user.py b/engine/apps/api/tests/test_user.py index ec30db63e9..8c9cd84396 100644 --- a/engine/apps/api/tests/test_user.py +++ b/engine/apps/api/tests/test_user.py @@ -173,6 +173,9 @@ def test_list_users( "cloud_connection_status": None, }, ], + "current_page_number": 1, + "page_size": 100, + "total_pages": 1, } response = client.get(url, format="json", **make_user_auth_headers(admin, token)) diff --git a/engine/apps/public_api/tests/test_alerts.py b/engine/apps/public_api/tests/test_alerts.py index 525ea842d7..8cb0de2cbb 100644 --- a/engine/apps/public_api/tests/test_alerts.py +++ b/engine/apps/public_api/tests/test_alerts.py @@ -90,6 +90,9 @@ def test_get_list_alerts( }, }, ], + "current_page_number": 1, + "page_size": 50, + "total_pages": 1, } assert response.status_code == status.HTTP_200_OK assert response.json() == expected_response diff --git a/engine/apps/public_api/tests/test_custom_actions.py b/engine/apps/public_api/tests/test_custom_actions.py index ae15aafb05..3690494031 100644 --- a/engine/apps/public_api/tests/test_custom_actions.py +++ b/engine/apps/public_api/tests/test_custom_actions.py @@ -37,6 +37,9 @@ def test_get_custom_actions( "forward_whole_payload": custom_action.forward_whole_payload, } ], + "current_page_number": 1, + "page_size": 50, + "total_pages": 1, } assert response.status_code == status.HTTP_200_OK @@ -74,6 +77,9 @@ def test_get_custom_actions_filter_by_name( "forward_whole_payload": custom_action.forward_whole_payload, } ], + "current_page_number": 1, + "page_size": 50, + "total_pages": 1, } assert response.status_code == status.HTTP_200_OK @@ -94,7 +100,15 @@ def test_get_custom_actions_filter_by_name_empty_result( response = client.get(f"{url}?name=NonExistentName", format="json", HTTP_AUTHORIZATION=f"{token}") - expected_payload = {"count": 0, "next": None, "previous": None, "results": []} + expected_payload = { + "count": 0, + "next": None, + "previous": None, + "results": [], + "current_page_number": 1, + "page_size": 50, + "total_pages": 1, + } assert response.status_code == status.HTTP_200_OK assert response.data == expected_payload diff --git a/engine/apps/public_api/tests/test_escalation_chain.py b/engine/apps/public_api/tests/test_escalation_chain.py index 99f6778ec8..f8bab811b4 100644 --- a/engine/apps/public_api/tests/test_escalation_chain.py +++ b/engine/apps/public_api/tests/test_escalation_chain.py @@ -25,6 +25,9 @@ def test_get_escalation_chains(make_organization_and_user_with_token): "name": "test", } ], + "current_page_number": 1, + "page_size": 50, + "total_pages": 1, } assert response.status_code == status.HTTP_200_OK diff --git a/engine/apps/public_api/tests/test_escalation_policies.py b/engine/apps/public_api/tests/test_escalation_policies.py index b1b705e146..728fc3837f 100644 --- a/engine/apps/public_api/tests/test_escalation_policies.py +++ b/engine/apps/public_api/tests/test_escalation_policies.py @@ -62,6 +62,9 @@ def _escalation_policies_setup(organization, user): escalation_policy_wait_payload, escalation_policy_notify_persons_empty_payload, ], + "current_page_number": 1, + "page_size": 50, + "total_pages": 1, } return ( escalation_chain, diff --git a/engine/apps/public_api/tests/test_incidents.py b/engine/apps/public_api/tests/test_incidents.py index 7767876286..f98b2479fc 100644 --- a/engine/apps/public_api/tests/test_incidents.py +++ b/engine/apps/public_api/tests/test_incidents.py @@ -46,8 +46,15 @@ def construct_expected_response_from_incidents(incidents): }, } ) - expected_response = {"count": incidents.count(), "next": None, "previous": None, "results": results} - return expected_response + return { + "count": incidents.count(), + "next": None, + "previous": None, + "results": results, + "current_page_number": 1, + "page_size": 50, + "total_pages": 1, + } @pytest.fixture() @@ -275,7 +282,7 @@ def test_pagination(settings, incident_public_api_setup): url = reverse("api-public:alert_groups-list") - with patch("common.api_helpers.paginators.PathPrefixedPagination.get_page_size", return_value=1): + with patch("common.api_helpers.paginators.PathPrefixedPagePagination.get_page_size", return_value=1): response = client.get(url, HTTP_AUTHORIZATION=f"{token}") assert response.status_code == status.HTTP_200_OK diff --git a/engine/apps/public_api/tests/test_integrations.py b/engine/apps/public_api/tests/test_integrations.py index 3a4a2db38d..0fa52a1082 100644 --- a/engine/apps/public_api/tests/test_integrations.py +++ b/engine/apps/public_api/tests/test_integrations.py @@ -74,6 +74,9 @@ def test_get_list_integrations( "maintenance_end_at": None, } ], + "current_page_number": 1, + "page_size": 50, + "total_pages": 1, } url = reverse("api-public:integrations-list") response = client.get(url, format="json", HTTP_AUTHORIZATION=f"{token}") diff --git a/engine/apps/public_api/tests/test_personal_notification_rules.py b/engine/apps/public_api/tests/test_personal_notification_rules.py index 7409a3f2e1..b6414e0d57 100644 --- a/engine/apps/public_api/tests/test_personal_notification_rules.py +++ b/engine/apps/public_api/tests/test_personal_notification_rules.py @@ -98,6 +98,9 @@ def test_get_personal_notification_rules_list(personal_notification_rule_public_ "important": True, }, ], + "current_page_number": 1, + "page_size": 50, + "total_pages": 1, } assert response.status_code == status.HTTP_200_OK @@ -126,6 +129,9 @@ def test_get_personal_notification_rules_list_important(personal_notification_ru "important": True, } ], + "current_page_number": 1, + "page_size": 50, + "total_pages": 1, } assert response.status_code == status.HTTP_200_OK @@ -169,6 +175,9 @@ def test_get_personal_notification_rules_list_non_important(personal_notificatio "important": False, }, ], + "current_page_number": 1, + "page_size": 50, + "total_pages": 1, } assert response.status_code == status.HTTP_200_OK diff --git a/engine/apps/public_api/tests/test_resolution_notes.py b/engine/apps/public_api/tests/test_resolution_notes.py index f976ef05a6..7c5e3b941a 100644 --- a/engine/apps/public_api/tests/test_resolution_notes.py +++ b/engine/apps/public_api/tests/test_resolution_notes.py @@ -57,6 +57,9 @@ def test_get_resolution_notes( "text": resolution_note_1.text, }, ], + "current_page_number": 1, + "page_size": 50, + "total_pages": 1, } assert response.status_code == status.HTTP_200_OK diff --git a/engine/apps/public_api/tests/test_routes.py b/engine/apps/public_api/tests/test_routes.py index 017883fbaf..c77ee531ed 100644 --- a/engine/apps/public_api/tests/test_routes.py +++ b/engine/apps/public_api/tests/test_routes.py @@ -86,6 +86,9 @@ def test_get_routes_list( TEST_MESSAGING_BACKEND_FIELD: {"id": None, "enabled": False}, } ], + "current_page_number": 1, + "page_size": 25, + "total_pages": 1, } assert response.status_code == status.HTTP_200_OK @@ -123,6 +126,9 @@ def test_get_routes_filter_by_integration_id( TEST_MESSAGING_BACKEND_FIELD: {"id": None, "enabled": False}, } ], + "current_page_number": 1, + "page_size": 25, + "total_pages": 1, } assert response.status_code == status.HTTP_200_OK diff --git a/engine/apps/public_api/tests/test_schedules.py b/engine/apps/public_api/tests/test_schedules.py index 6dba833228..820005d8f9 100644 --- a/engine/apps/public_api/tests/test_schedules.py +++ b/engine/apps/public_api/tests/test_schedules.py @@ -702,6 +702,9 @@ def test_get_schedule_list( "slack": {"channel_id": slack_channel_id, "user_group_id": user_group_id}, }, ], + "current_page_number": 1, + "page_size": 50, + "total_pages": 1, } assert response.status_code == status.HTTP_200_OK diff --git a/engine/apps/public_api/tests/test_slack_channels.py b/engine/apps/public_api/tests/test_slack_channels.py index 5884dcedcd..2e0efb8dd9 100644 --- a/engine/apps/public_api/tests/test_slack_channels.py +++ b/engine/apps/public_api/tests/test_slack_channels.py @@ -32,6 +32,9 @@ def test_get_slack_channels_list( "next": None, "previous": None, "results": [{"name": slack_channel.name, "slack_id": slack_channel.slack_id}], + "current_page_number": 1, + "page_size": 50, + "total_pages": 1, } assert response.status_code == status.HTTP_200_OK diff --git a/engine/apps/public_api/tests/test_teams.py b/engine/apps/public_api/tests/test_teams.py index 946585df2c..ce31993ec9 100644 --- a/engine/apps/public_api/tests/test_teams.py +++ b/engine/apps/public_api/tests/test_teams.py @@ -38,6 +38,9 @@ def test_get_teams_list(team_public_api_setup): "avatar_url": team.avatar_url, } ], + "current_page_number": 1, + "page_size": 50, + "total_pages": 1, } assert response.status_code == status.HTTP_200_OK diff --git a/engine/apps/public_api/tests/test_user_groups.py b/engine/apps/public_api/tests/test_user_groups.py index 055d93c689..48aa396784 100644 --- a/engine/apps/public_api/tests/test_user_groups.py +++ b/engine/apps/public_api/tests/test_user_groups.py @@ -42,6 +42,9 @@ def test_get_user_groups( }, } ], + "current_page_number": 1, + "page_size": 50, + "total_pages": 1, } assert response.status_code == status.HTTP_200_OK @@ -80,6 +83,9 @@ def test_get_user_groups_filter_by_handle( }, } ], + "current_page_number": 1, + "page_size": 50, + "total_pages": 1, } assert response.status_code == status.HTTP_200_OK @@ -98,7 +104,15 @@ def test_get_user_groups_filter_by_handle_empty_result( response = client.get(f"{url}?slack_handle=NonExistentSlackHandle", format="json", HTTP_AUTHORIZATION=f"{token}") - expected_payload = {"count": 0, "next": None, "previous": None, "results": []} + expected_payload = { + "count": 0, + "next": None, + "previous": None, + "results": [], + "current_page_number": 1, + "page_size": 50, + "total_pages": 1, + } assert response.status_code == status.HTTP_200_OK assert response.data == expected_payload diff --git a/engine/apps/public_api/tests/test_users.py b/engine/apps/public_api/tests/test_users.py index 0a0cf61276..fc77fcd4f9 100644 --- a/engine/apps/public_api/tests/test_users.py +++ b/engine/apps/public_api/tests/test_users.py @@ -82,6 +82,9 @@ def test_get_users_list( "is_phone_number_verified": False, }, ], + "current_page_number": 1, + "page_size": 100, + "total_pages": 1, } assert response.status_code == status.HTTP_200_OK @@ -121,6 +124,9 @@ def test_get_users_list_short( "is_phone_number_verified": False, }, ], + "current_page_number": 1, + "page_size": 100, + "total_pages": 1, } assert response.status_code == status.HTTP_200_OK @@ -170,6 +176,9 @@ def test_get_users_list_all_role_users(user_public_api_setup, make_user_for_orga } for user, role in expected_users ], + "current_page_number": 1, + "page_size": 100, + "total_pages": 1, } assert response.status_code == status.HTTP_200_OK diff --git a/engine/common/api_helpers/paginators.py b/engine/common/api_helpers/paginators.py index 997d48b44a..a846ddb666 100644 --- a/engine/common/api_helpers/paginators.py +++ b/engine/common/api_helpers/paginators.py @@ -1,45 +1,60 @@ -from rest_framework.pagination import CursorPagination, PageNumberPagination +import typing -from common.api_helpers.utils import create_engine_url +from rest_framework.pagination import BasePagination, CursorPagination, PageNumberPagination +from rest_framework.response import Response -MAX_PAGE_SIZE = 100 -PAGE_QUERY_PARAM = "page" -PAGE_SIZE_QUERY_PARAM = "perpage" +from common.api_helpers.utils import create_engine_url -class PathPrefixedPagination(PageNumberPagination): - max_page_size = MAX_PAGE_SIZE - page_query_param = PAGE_QUERY_PARAM - page_size_query_param = PAGE_SIZE_QUERY_PARAM +class BasePathPrefixedPagination(BasePagination): + max_page_size = 100 + page_query_param = "page" + page_size_query_param = "perpage" def paginate_queryset(self, queryset, request, view=None): + self.request = request request.build_absolute_uri = lambda: create_engine_url(request.get_full_path()) return super().paginate_queryset(queryset, request, view) + def _get_base_paginated_response(self, data: typing.List[typing.Any]) -> typing.Dict: + return { + "next": self.get_next_link(), + "previous": self.get_previous_link(), + "results": data, + "page_size": self.get_page_size(self.request), + } -class PathPrefixedCursorPagination(CursorPagination): - max_page_size = MAX_PAGE_SIZE - page_query_param = PAGE_QUERY_PARAM - page_size_query_param = PAGE_SIZE_QUERY_PARAM - def paginate_queryset(self, queryset, request, view=None): - request.build_absolute_uri = lambda: create_engine_url(request.get_full_path()) - return super().paginate_queryset(queryset, request, view) +class PathPrefixedPagePagination(BasePathPrefixedPagination, PageNumberPagination): + def get_paginated_response(self, data: typing.List[typing.Any]) -> Response: + return Response( + { + **self._get_base_paginated_response(data), + "count": self.page.paginator.count, + "current_page_number": self.page.number, + "total_pages": self.page.paginator.num_pages, + } + ) + + +class PathPrefixedCursorPagination(BasePathPrefixedPagination, CursorPagination): + def get_paginated_response(self, data: typing.List[typing.Any]) -> Response: + return Response(self._get_base_paginated_response(data)) -class HundredPageSizePaginator(PathPrefixedPagination): +class HundredPageSizePaginator(PathPrefixedPagePagination): page_size = 100 -class FiftyPageSizePaginator(PathPrefixedPagination): +class FiftyPageSizePaginator(PathPrefixedPagePagination): page_size = 50 -class TwentyFivePageSizePaginator(PathPrefixedPagination): +class TwentyFivePageSizePaginator(PathPrefixedPagePagination): page_size = 25 -class FifteenPageSizePaginator(PathPrefixedPagination): +class FifteenPageSizePaginator(PathPrefixedPagePagination): page_size = 15 From 662a9519a6d3bb2ea2fbba622a98a618a6d24786 Mon Sep 17 00:00:00 2001 From: Joey Orlando Date: Sat, 8 Jul 2023 20:57:05 +0200 Subject: [PATCH 02/10] wip --- .../alerts/models/alert_receive_channel.py | 9 +- engine/apps/alerts/models/channel_filter.py | 5 - .../apps/alerts/tests/test_channel_filter.py | 49 +---- engine/apps/api/tests/test_channel_filter.py | 35 --- engine/apps/api/views/channel_filter.py | 13 -- .../views/cloud_connection.py | 16 +- .../oss_installation/views/cloud_users.py | 35 ++- engine/common/api_helpers/paginators.py | 40 ++-- .../OrganizationLogFilters.module.css | 19 -- .../OrganizationLogFilters.tsx | 91 -------- .../src/models/current_subscription.ts | 57 ----- .../current_subscription.ts | 23 -- .../current_subscription.types.ts | 86 -------- .../organization_log/organization_log.ts | 60 ------ .../organization_log.types.ts | 10 - grafana-plugin/src/pages/index.tsx | 8 - .../OrganizationLog.module.css | 23 -- .../organization-logs/OrganizationLog.tsx | 199 ------------------ grafana-plugin/src/pages/routes.tsx | 5 - .../src/plugin/GrafanaPluginRootPage.tsx | 4 - .../src/state/rootBaseStore/index.ts | 2 - 21 files changed, 55 insertions(+), 734 deletions(-) delete mode 100644 grafana-plugin/src/containers/OrganizationLogFilters/OrganizationLogFilters.module.css delete mode 100644 grafana-plugin/src/containers/OrganizationLogFilters/OrganizationLogFilters.tsx delete mode 100644 grafana-plugin/src/models/current_subscription.ts delete mode 100644 grafana-plugin/src/models/current_subscription/current_subscription.ts delete mode 100644 grafana-plugin/src/models/current_subscription/current_subscription.types.ts delete mode 100644 grafana-plugin/src/models/organization_log/organization_log.ts delete mode 100644 grafana-plugin/src/models/organization_log/organization_log.types.ts delete mode 100644 grafana-plugin/src/pages/organization-logs/OrganizationLog.module.css delete mode 100644 grafana-plugin/src/pages/organization-logs/OrganizationLog.tsx diff --git a/engine/apps/alerts/models/alert_receive_channel.py b/engine/apps/alerts/models/alert_receive_channel.py index 8579ddb37d..083e56d05c 100644 --- a/engine/apps/alerts/models/alert_receive_channel.py +++ b/engine/apps/alerts/models/alert_receive_channel.py @@ -527,8 +527,8 @@ def heartbeat_module(self): return getattr(heartbeat, self.INTEGRATIONS_TO_REVERSE_URL_MAP[self.integration], None) # Demo alerts - def send_demo_alert(self, force_route_id=None, payload=None): - logger.info(f"send_demo_alert integration={self.pk} force_route_id={force_route_id}") + def send_demo_alert(self, payload=None): + logger.info(f"send_demo_alert integration={self.pk}") if not self.is_demo_alert_enabled: raise UnableToSendDemoAlert("Unable to send demo alert for this integration.") @@ -543,9 +543,7 @@ def send_demo_alert(self, force_route_id=None, payload=None): "Unable to send demo alert as payload has no 'alerts' key, it is not array, or it is empty." ) for alert in alerts: - create_alertmanager_alerts.delay( - alert_receive_channel_pk=self.pk, alert=alert, is_demo=True, force_route_id=force_route_id - ) + create_alertmanager_alerts.delay(alert_receive_channel_pk=self.pk, alert=alert, is_demo=True) else: create_alert.delay( title="Demo alert", @@ -556,7 +554,6 @@ def send_demo_alert(self, force_route_id=None, payload=None): integration_unique_data=None, raw_request_data=payload, is_demo=True, - force_route_id=force_route_id, ) @property diff --git a/engine/apps/alerts/models/channel_filter.py b/engine/apps/alerts/models/channel_filter.py index 677f3a19b3..915aeafff1 100644 --- a/engine/apps/alerts/models/channel_filter.py +++ b/engine/apps/alerts/models/channel_filter.py @@ -165,11 +165,6 @@ def str_for_clients(self): return str(self.filtering_term).replace("`", "") raise Exception("Unknown filtering term") - def send_demo_alert(self): - """Deprecated. May be used in the older versions of the plugin""" - integration = self.alert_receive_channel - integration.send_demo_alert(force_route_id=self.pk) - # Insight logs @property def insight_logs_type_verbal(self): diff --git a/engine/apps/alerts/tests/test_channel_filter.py b/engine/apps/alerts/tests/test_channel_filter.py index 3c9de7e81e..cf66c632a8 100644 --- a/engine/apps/alerts/tests/test_channel_filter.py +++ b/engine/apps/alerts/tests/test_channel_filter.py @@ -1,8 +1,6 @@ -from unittest import mock - import pytest -from apps.alerts.models import AlertReceiveChannel, ChannelFilter +from apps.alerts.models import ChannelFilter @pytest.mark.django_db @@ -93,48 +91,3 @@ def test_channel_filter_select_filter_jinja2(make_organization, make_alert_recei alert_receive_channel, raw_request_data, force_route_id=channel_filter.pk ) assert satisfied_filter == channel_filter - - -@mock.patch("apps.integrations.tasks.create_alert.apply_async", return_value=None) -@pytest.mark.django_db -def test_send_demo_alert( - mocked_create_alert, - make_organization, - make_alert_receive_channel, - make_channel_filter, -): - organization = make_organization() - alert_receive_channel = make_alert_receive_channel( - organization, integration=AlertReceiveChannel.INTEGRATION_WEBHOOK - ) - filtering_term = "test alert" - channel_filter = make_channel_filter(alert_receive_channel, filtering_term=filtering_term, is_default=False) - - channel_filter.send_demo_alert() - assert mocked_create_alert.called - assert mocked_create_alert.call_args.args[1]["is_demo"] - assert mocked_create_alert.call_args.args[1]["force_route_id"] == channel_filter.id - - -@mock.patch("apps.integrations.tasks.create_alertmanager_alerts.apply_async", return_value=None) -@pytest.mark.django_db -@pytest.mark.parametrize( - "integration", - [ - AlertReceiveChannel.INTEGRATION_ALERTMANAGER, - AlertReceiveChannel.INTEGRATION_GRAFANA, - AlertReceiveChannel.INTEGRATION_GRAFANA_ALERTING, - ], -) -def test_send_demo_alert_alertmanager_payload_shape( - mocked_create_alert, make_organization, make_alert_receive_channel, make_channel_filter, integration -): - organization = make_organization() - alert_receive_channel = make_alert_receive_channel(organization) - filtering_term = "test alert" - channel_filter = make_channel_filter(alert_receive_channel, filtering_term=filtering_term, is_default=False) - - channel_filter.send_demo_alert() - assert mocked_create_alert.called - assert mocked_create_alert.call_args.args[1]["is_demo"] - assert mocked_create_alert.call_args.args[1]["force_route_id"] == channel_filter.pk diff --git a/engine/apps/api/tests/test_channel_filter.py b/engine/apps/api/tests/test_channel_filter.py index baf845e308..484090c894 100644 --- a/engine/apps/api/tests/test_channel_filter.py +++ b/engine/apps/api/tests/test_channel_filter.py @@ -219,41 +219,6 @@ def test_channel_filter_move_to_position_permissions( assert response.status_code == expected_status -@pytest.mark.django_db -@pytest.mark.parametrize( - "role,expected_status", - [ - (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), - (LegacyAccessControlRole.EDITOR, status.HTTP_200_OK), - (LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN), - ], -) -def test_alert_receive_channel_send_demo_alert_permissions( - make_organization_and_user_with_plugin_token, - make_user_auth_headers, - make_alert_receive_channel, - make_channel_filter, - role, - expected_status, -): - organization, user, token = make_organization_and_user_with_plugin_token(role) - alert_receive_channel = make_alert_receive_channel(organization) - channel_filter = make_channel_filter(alert_receive_channel, is_default=True) - client = APIClient() - - url = reverse("api-internal:channel_filter-send-demo-alert", kwargs={"pk": channel_filter.public_primary_key}) - - with patch( - "apps.api.views.channel_filter.ChannelFilterView.send_demo_alert", - return_value=Response( - status=status.HTTP_200_OK, - ), - ): - response = client.post(url, format="json", **make_user_auth_headers(user, token)) - - assert response.status_code == expected_status - - @pytest.mark.django_db def test_channel_filter_create_with_order( make_organization_and_user_with_plugin_token, diff --git a/engine/apps/api/views/channel_filter.py b/engine/apps/api/views/channel_filter.py index f00e195ed3..18d72066b2 100644 --- a/engine/apps/api/views/channel_filter.py +++ b/engine/apps/api/views/channel_filter.py @@ -12,7 +12,6 @@ ChannelFilterSerializer, ChannelFilterUpdateSerializer, ) -from apps.api.throttlers import DemoAlertThrottler from apps.auth_token.auth import PluginAuthentication from apps.slack.models import SlackChannel from common.api_helpers.exceptions import BadRequest @@ -23,7 +22,6 @@ UpdateSerializerMixin, ) from common.api_helpers.serializers import get_move_to_position_param -from common.exceptions import UnableToSendDemoAlert from common.insight_log import EntityEvent, write_resource_insight_log @@ -41,7 +39,6 @@ class ChannelFilterView( "partial_update": [RBACPermission.Permissions.INTEGRATIONS_WRITE], "destroy": [RBACPermission.Permissions.INTEGRATIONS_WRITE], "move_to_position": [RBACPermission.Permissions.INTEGRATIONS_WRITE], - "send_demo_alert": [RBACPermission.Permissions.INTEGRATIONS_TEST], "convert_from_regex_to_jinja2": [RBACPermission.Permissions.INTEGRATIONS_WRITE], } @@ -131,16 +128,6 @@ def move_to_position(self, request, pk): return Response(status=status.HTTP_200_OK) - @action(detail=True, methods=["post"], throttle_classes=[DemoAlertThrottler]) - def send_demo_alert(self, request, pk): - """Deprecated action. May be used in the older version of the plugin.""" - instance = self.get_object() - try: - instance.send_demo_alert() - except UnableToSendDemoAlert as e: - raise BadRequest(detail=str(e)) - return Response(status=status.HTTP_200_OK) - @action(detail=True, methods=["post"]) def convert_from_regex_to_jinja2(self, request, pk): instance = self.get_object() diff --git a/engine/apps/oss_installation/views/cloud_connection.py b/engine/apps/oss_installation/views/cloud_connection.py index 004837df2e..74f4c5d1c1 100644 --- a/engine/apps/oss_installation/views/cloud_connection.py +++ b/engine/apps/oss_installation/views/cloud_connection.py @@ -22,14 +22,14 @@ class CloudConnectionView(APIView): def get(self, request): connector = CloudConnector.objects.first() heartbeat = CloudHeartbeat.objects.first() - response = { - "cloud_connection_status": connector is not None, - "cloud_notifications_enabled": live_settings.GRAFANA_CLOUD_NOTIFICATIONS_ENABLED, - "cloud_heartbeat_enabled": live_settings.GRAFANA_CLOUD_ONCALL_HEARTBEAT_ENABLED, - "cloud_heartbeat_link": get_heartbeat_link(connector, heartbeat), - "cloud_heartbeat_status": heartbeat is not None and heartbeat.success, - } - return Response(response) + return Response( + { + "cloud_notifications_enabled": live_settings.GRAFANA_CLOUD_NOTIFICATIONS_ENABLED, + "cloud_heartbeat_enabled": live_settings.GRAFANA_CLOUD_ONCALL_HEARTBEAT_ENABLED, + "cloud_heartbeat_link": get_heartbeat_link(connector, heartbeat), + "cloud_heartbeat_status": heartbeat is not None and heartbeat.success, + } + ) def delete(self, request): s = LiveSetting.objects.filter(name="GRAFANA_CLOUD_ONCALL_TOKEN").first() diff --git a/engine/apps/oss_installation/views/cloud_users.py b/engine/apps/oss_installation/views/cloud_users.py index 75d3886aa4..be12665b9d 100644 --- a/engine/apps/oss_installation/views/cloud_users.py +++ b/engine/apps/oss_installation/views/cloud_users.py @@ -1,5 +1,3 @@ -from collections import OrderedDict - from rest_framework import mixins, status, viewsets from rest_framework.decorators import action from rest_framework.permissions import IsAuthenticated @@ -13,12 +11,22 @@ from apps.oss_installation.utils import cloud_user_identity_status from apps.user_management.models import User from common.api_helpers.mixins import PublicPrimaryKeyMixin -from common.api_helpers.paginators import HundredPageSizePaginator +from common.api_helpers.paginators import HundredPageSizePaginator, PaginatedData PERMISSIONS = [RBACPermission.Permissions.OTHER_SETTINGS_WRITE] -class CloudUsersView(HundredPageSizePaginator, APIView): +class CloudUsersPagination(HundredPageSizePaginator): + def get_paginated_response(self, data: PaginatedData, matched_users_count: int) -> Response: + return Response( + { + **self._get_paginated_response_data(data), + "matched_users_count": matched_users_count, + } + ) + + +class CloudUsersView(CloudUsersPagination, APIView): authentication_classes = (PluginAuthentication,) permission_classes = (IsAuthenticated, RBACPermission) @@ -44,14 +52,14 @@ def get(self, request): cloud_identities = list(CloudUserIdentity.objects.filter(email__in=emails)) cloud_identities = {cloud_identity.email: cloud_identity for cloud_identity in cloud_identities} - response = [] + data = [] connector = CloudConnector.objects.first() for user in results: cloud_identity = cloud_identities.get(user.email, None) status, link = cloud_user_identity_status(connector, cloud_identity) - response.append( + data.append( { "id": user.public_primary_key, "email": user.email, @@ -60,20 +68,7 @@ def get(self, request): } ) - return self.get_paginated_response_with_matched_users_count(response, len(cloud_identities)) - - def get_paginated_response_with_matched_users_count(self, data, matched_users_count): - return Response( - OrderedDict( - [ - ("count", self.page.paginator.count), - ("matched_users_count", matched_users_count), - ("next", self.get_next_link()), - ("previous", self.get_previous_link()), - ("results", data), - ] - ) - ) + return self.get_paginated_response(data, len(cloud_identities)) def post(self, request): connector = CloudConnector.objects.first() diff --git a/engine/common/api_helpers/paginators.py b/engine/common/api_helpers/paginators.py index a846ddb666..b4d19fcdce 100644 --- a/engine/common/api_helpers/paginators.py +++ b/engine/common/api_helpers/paginators.py @@ -5,6 +5,21 @@ from common.api_helpers.utils import create_engine_url +PaginatedData = typing.List[typing.Any] + + +class BasePaginatedResponseData(typing.TypedDict): + next: str | None + previous: str | None + results: PaginatedData + page_size: int + + +class PageBasedPaginationResponseData(BasePaginatedResponseData): + count: int + current_page_number: int + total_pages: int + class BasePathPrefixedPagination(BasePagination): max_page_size = 100 @@ -16,7 +31,7 @@ def paginate_queryset(self, queryset, request, view=None): request.build_absolute_uri = lambda: create_engine_url(request.get_full_path()) return super().paginate_queryset(queryset, request, view) - def _get_base_paginated_response(self, data: typing.List[typing.Any]) -> typing.Dict: + def _get_base_paginated_response_data(self, data: PaginatedData) -> BasePaginatedResponseData: return { "next": self.get_next_link(), "previous": self.get_previous_link(), @@ -26,20 +41,21 @@ def _get_base_paginated_response(self, data: typing.List[typing.Any]) -> typing. class PathPrefixedPagePagination(BasePathPrefixedPagination, PageNumberPagination): - def get_paginated_response(self, data: typing.List[typing.Any]) -> Response: - return Response( - { - **self._get_base_paginated_response(data), - "count": self.page.paginator.count, - "current_page_number": self.page.number, - "total_pages": self.page.paginator.num_pages, - } - ) + def _get_paginated_response_data(self, data: PaginatedData) -> PageBasedPaginationResponseData: + return { + **self._get_base_paginated_response_data(data), + "count": self.page.paginator.count, + "current_page_number": self.page.number, + "total_pages": self.page.paginator.num_pages, + } + + def get_paginated_response(self, data: PaginatedData) -> Response: + return Response(self._get_paginated_response_data(data)) class PathPrefixedCursorPagination(BasePathPrefixedPagination, CursorPagination): - def get_paginated_response(self, data: typing.List[typing.Any]) -> Response: - return Response(self._get_base_paginated_response(data)) + def get_paginated_response(self, data: PaginatedData) -> Response: + return Response(self._get_base_paginated_response_data(data)) class HundredPageSizePaginator(PathPrefixedPagePagination): diff --git a/grafana-plugin/src/containers/OrganizationLogFilters/OrganizationLogFilters.module.css b/grafana-plugin/src/containers/OrganizationLogFilters/OrganizationLogFilters.module.css deleted file mode 100644 index 913c13e03c..0000000000 --- a/grafana-plugin/src/containers/OrganizationLogFilters/OrganizationLogFilters.module.css +++ /dev/null @@ -1,19 +0,0 @@ -.root { - display: block; -} - -.root > * { - margin-bottom: 10px !important; -} - -.root > *:not(:last-child) { - margin-right: 10px !important; -} - -.root .search { - width: 400px; -} - -.select { - min-width: 300px; -} diff --git a/grafana-plugin/src/containers/OrganizationLogFilters/OrganizationLogFilters.tsx b/grafana-plugin/src/containers/OrganizationLogFilters/OrganizationLogFilters.tsx deleted file mode 100644 index 2e0831a80c..0000000000 --- a/grafana-plugin/src/containers/OrganizationLogFilters/OrganizationLogFilters.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import React, { ChangeEvent, useCallback, useMemo, useState } from 'react'; - -import { RawTimeRange } from '@grafana/data'; -import { HorizontalGroup, Input, TimeRangeInput } from '@grafana/ui'; -import cn from 'classnames/bind'; -import { observer } from 'mobx-react'; - -import RemoteSelect from 'containers/RemoteSelect/RemoteSelect'; - -import styles from './OrganizationLogFilters.module.css'; - -const cx = cn.bind(styles); - -interface OrganizationLogFiltersProps { - value: any; - onChange: (filters: any) => void; - className?: string; -} - -const OrganizationLogFilters = observer((props: OrganizationLogFiltersProps) => { - const { value, onChange } = props; - - const [createAtRaw, setCreateAtRaw] = useState(); - - const onSearchTermChangeCallback = useCallback( - (e: ChangeEvent) => { - const filters = { - ...value, - search: e.currentTarget.value, - }; - - onChange(filters); - }, - [onChange, value] - ); - - const getChangeHandler = (field: string) => { - return (newValue: any) => { - onChange({ - ...value, - [field]: newValue, - }); - }; - }; - - const handleChangeCreatedAt = useCallback( - (filter) => { - onChange({ - ...value, - created_at: filter.from._isValid && filter.to._isValid ? [filter.from, filter.to] : undefined, - }); - - setCreateAtRaw(filter.raw); - }, - [value] - ); - - const createdAtValue = useMemo(() => { - if (value['created_at']) { - return { from: value['created_at'][0].toDate(), to: value['created_at'][1].toDate(), raw: createAtRaw }; - } - return { from: undefined, to: undefined, raw: undefined }; - }, [value]); - - return ( -
- - - - - -
- ); -}); - -export default OrganizationLogFilters; diff --git a/grafana-plugin/src/models/current_subscription.ts b/grafana-plugin/src/models/current_subscription.ts deleted file mode 100644 index 547bcf6057..0000000000 --- a/grafana-plugin/src/models/current_subscription.ts +++ /dev/null @@ -1,57 +0,0 @@ -export interface CurrentSubscriptionDTO { - uuid: string; - created_at: string; - activation_expire_at: any; - charges: string; - stats: { - result_credit: string; - result_active_users_number: string; - month: string; - }; - subscription_plan: string; - users_limit: string; - current_stats: { - // users_on_call: User['pk'][]; - // users_1_weeks_ago: User['pk'][]; - // users_2_weeks_ago: User['pk'][]; - // users_3_weeks_ago: User['pk'][]; - users_on_call: any[]; - users_1_weeks_ago: any[]; - users_2_weeks_ago: any[]; - users_3_weeks_ago: any[]; - active_users_count: number; - estimate_credit: number; - - is_billing_exists: boolean; - active_plan: string; - expires_at: string; - paid_up_users: number; - active_users: number; - admins: number; - users: number; - - active_users_history: Array<{ - month: string; - active_users_amount: number; - }>; - - billing_history: Array<{ - date: string; - plan: number; - paid_up_users_amount: number; - charges: string; - active_users: number; - billing_statement: string; - planned_next_period: boolean; - }>; - - usage_statistics: { - users_on_call: string[]; - users_1_weeks_ago: string[]; - users_2_weeks_ago: string[]; - users_3_weeks_ago: string[]; - active_users_count: number; - estimate_credit: number; - }; - }; -} diff --git a/grafana-plugin/src/models/current_subscription/current_subscription.ts b/grafana-plugin/src/models/current_subscription/current_subscription.ts deleted file mode 100644 index 527ed1925f..0000000000 --- a/grafana-plugin/src/models/current_subscription/current_subscription.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { action, observable } from 'mobx'; - -import BaseStore from 'models/base_store'; -import { makeRequest } from 'network'; -import { RootStore } from 'state'; - -import { Subscription } from './current_subscription.types'; - -export class CurrentSubscriptionStore extends BaseStore { - @observable - currentSubscription?: Subscription; - - constructor(rootStore: RootStore) { - super(rootStore); - - this.path = '/current_subscription/'; - } - - @action - async updateCurrentSubscription() { - this.currentSubscription = await makeRequest(this.path, {}); - } -} diff --git a/grafana-plugin/src/models/current_subscription/current_subscription.types.ts b/grafana-plugin/src/models/current_subscription/current_subscription.types.ts deleted file mode 100644 index 7a52972ae9..0000000000 --- a/grafana-plugin/src/models/current_subscription/current_subscription.types.ts +++ /dev/null @@ -1,86 +0,0 @@ -interface UpdateLicenseOption { - per_month: { - price: number; - product_id: number; - }; - per_year: { - price: number; - product_id: number; - }; -} - -export interface Subscription { - uuid: string; - created_at: string; - activation_expire_at: any; - charges: string; - stats: { - result_credit: string; - result_active_users_number: string; - month: string; - }; - subscription_plan: string; - - active_plan: string; - expires_at: string; - - active_users: string[]; - stakeholders: string[]; - active_users_count: number; - users_limit: number; - stakeholders_count: number; - stakeholders_limit: number; - - show_stakeholders_in_violation_message: boolean; - - update_licence_options: { - business: UpdateLicenseOption; - team: UpdateLicenseOption; - }; - - current_team_primary_key: number; - current_user_primary_key: number; - - trial_days_left: number; - - current_stats: { - users_on_call: any[]; - users_1_weeks_ago: any[]; - users_2_weeks_ago: any[]; - users_3_weeks_ago: any[]; - active_users_count: number; - estimate_credit: number; - - is_billing_exists: boolean; - active_plan: string; - expires_at: string; - paid_up_users: number; - active_users: number; - admins: number; - users: number; - - active_users_history: Array<{ - month: string; - active_users_amount: number; - }>; - - billing_history: Array<{ - date: string; - plan: number; - paid_up_users_amount: number; - charges: string; - active_users: number; - billing_statement: string; - planned_next_period: boolean; - }>; - - usage_statistics: { - users_on_call: string[]; - users_1_weeks_ago: string[]; - users_2_weeks_ago: string[]; - users_3_weeks_ago: string[]; - active_users_count: number; - estimate_credit: number; - }; - }; -} diff --git a/grafana-plugin/src/models/organization_log/organization_log.ts b/grafana-plugin/src/models/organization_log/organization_log.ts deleted file mode 100644 index 857317b863..0000000000 --- a/grafana-plugin/src/models/organization_log/organization_log.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { action, observable } from 'mobx'; - -import BaseStore from 'models/base_store'; -import { makeRequest } from 'network'; -import { RootStore } from 'state'; - -import { OrganizationLog } from './organization_log.types'; - -export class OrganizationLogStore extends BaseStore { - @observable.shallow - items: { [id: string]: OrganizationLog } = {}; - - @observable.shallow - searchResult?: { - total: number; - page: number; - results: Array; - }; - - constructor(rootStore: RootStore) { - super(rootStore); - - this.path = '/organization_logs/'; - } - - @action - async updateItems(query = '', page: number, filters?: any) { - const { results, count } = await makeRequest(`${this.path}`, { - params: { search: query, page, ...filters }, - }); - - this.items = { - ...this.items, - ...results.reduce( - (acc: { [key: string]: OrganizationLog }, item: OrganizationLog) => ({ - ...acc, - [item.id]: item, - }), - {} - ), - }; - - this.searchResult = { - total: count, - page, - results: results.map((item: OrganizationLog) => item.id), - }; - } - - getSearchResult() { - if (!this.searchResult) { - return undefined; - } - - return { - ...this.searchResult, - results: this.searchResult.results.map((id: OrganizationLog['id']) => this.items[id]), - }; - } -} diff --git a/grafana-plugin/src/models/organization_log/organization_log.types.ts b/grafana-plugin/src/models/organization_log/organization_log.types.ts deleted file mode 100644 index e63da78f1a..0000000000 --- a/grafana-plugin/src/models/organization_log/organization_log.types.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { User } from 'models/user/user.types'; - -export interface OrganizationLog { - id: string; - author: Partial; - type: number; - created_at: string; - description: string; - labels: string[]; -} diff --git a/grafana-plugin/src/pages/index.tsx b/grafana-plugin/src/pages/index.tsx index 9ada4824d9..48e9a989f5 100644 --- a/grafana-plugin/src/pages/index.tsx +++ b/grafana-plugin/src/pages/index.tsx @@ -144,13 +144,6 @@ export const pages: { [id: string]: PageDefinition } = [ path: getPath('cloud'), action: UserActions.OtherSettingsWrite, }, - { - icon: 'gf-logs', - id: 'organization-logs', - text: 'Org Logs', - hideFromTabs: true, - path: getPath('organization-logs'), - }, { icon: 'cog', id: 'test', @@ -188,7 +181,6 @@ export const ROUTES = { outgoing_webhooks_2: ['outgoing_webhooks_2', 'outgoing_webhooks_2/:id', 'outgoing_webhooks_2/:action/:id'], maintenance: ['maintenance'], settings: ['settings'], - 'organization-logs': ['organization-logs'], 'chat-ops': ['chat-ops'], 'live-settings': ['live-settings'], cloud: ['cloud'], diff --git a/grafana-plugin/src/pages/organization-logs/OrganizationLog.module.css b/grafana-plugin/src/pages/organization-logs/OrganizationLog.module.css deleted file mode 100644 index 805f05ea85..0000000000 --- a/grafana-plugin/src/pages/organization-logs/OrganizationLog.module.css +++ /dev/null @@ -1,23 +0,0 @@ -.header { - display: flex; - justify-content: space-between; -} - -.align-top { - vertical-align: top; -} - -.no-author { - background: #fff7e6; -} - -.root .no-background { - background: none; -} - -.root .short-description { - overflow: hidden; - text-overflow: ellipsis; - width: 400px; - white-space: nowrap; -} diff --git a/grafana-plugin/src/pages/organization-logs/OrganizationLog.tsx b/grafana-plugin/src/pages/organization-logs/OrganizationLog.tsx deleted file mode 100644 index 66a7d5b5c1..0000000000 --- a/grafana-plugin/src/pages/organization-logs/OrganizationLog.tsx +++ /dev/null @@ -1,199 +0,0 @@ -import React from 'react'; - -import { Button, HorizontalGroup, Tag, Tooltip } from '@grafana/ui'; -import cn from 'classnames/bind'; -import { debounce } from 'lodash-es'; -import { observer } from 'mobx-react'; -import moment, { Moment } from 'moment-timezone'; -import { RouteComponentProps } from 'react-router-dom'; - -import Avatar from 'components/Avatar/Avatar'; -import GTable from 'components/GTable/GTable'; -import PluginLink from 'components/PluginLink/PluginLink'; -import Text from 'components/Text/Text'; -import OrganizationLogFilters from 'containers/OrganizationLogFilters/OrganizationLogFilters'; -import logo from 'img/logo.svg'; -import { OrganizationLog } from 'models/organization_log/organization_log.types'; -import { WithStoreProps } from 'state/types'; -import { withMobXProviderContext } from 'state/withStore'; -import sanitize from 'utils/sanitize'; - -import styles from './OrganizationLog.module.css'; - -const cx = cn.bind(styles); - -interface OrganizationLogProps extends WithStoreProps, RouteComponentProps {} - -interface OrganizationLogState { - filters: { [key: string]: any }; - page: number; - expandedLogsKeys: string[]; -} - -const INITIAL_FILTERS = {}; - -const ITEMS_PER_PAGE = 50; - -@observer -class OrganizationLogPage extends React.Component { - state: OrganizationLogState = { filters: { ...INITIAL_FILTERS }, page: 1, expandedLogsKeys: [] }; - - componentDidMount() { - this.refresh(); - } - - refresh = () => { - const { store } = this.props; - - const { filters, page } = this.state; - store.OrganizationLogStore.updateItems('', page, { - ...filters, - created_at: filters.created_at - ? filters.created_at.map((m: Moment) => m.utc().format('YYYY-MM-DDTHH:mm:ss')).join('/') - : undefined, - }); - }; - - debouncedRefresh = debounce(this.refresh, 500); - - render() { - const { filters, expandedLogsKeys } = this.state; - const { store } = this.props; - const { OrganizationLogStore } = store; - - const columns = [ - { - width: '40%', - title: 'Action', - render: this.renderShortDescription, - key: 'action', - }, - { - width: '10%', - title: 'User', - render: this.renderUser, - key: 'user', - }, - { - width: '30%', - title: 'Labels', - render: this.renderLabels, - key: 'labels', - }, - { - width: '20%', - title: 'Time', - render: this.renderCreatedAt, - key: 'created_at', - }, - ]; - - const searchResult: any = OrganizationLogStore.getSearchResult() || {}; - - const { total, page, results } = searchResult; - - const loading = !results; - - return ( -
- - ( -
- - Organization Logs - - -
- )} - showHeader={true} - data={results} - loading={loading} - emptyText={results ? 'No logs found' : 'Loading...'} - columns={columns} - pagination={{ - page, - total: Math.ceil((total || 0) / ITEMS_PER_PAGE), - onChange: this.handleChangePage, - }} - rowClassName={cx('align-top')} - expandable={{ - expandedRowRender: this.renderFullDescription, - expandRowByClick: true, - expandedRowKeys: expandedLogsKeys, - onExpandedRowsChange: this.handleExpandedRowsChange, - }} - /> -
- ); - } - - handleExpandedRowsChange = (expandedRows: string[]) => { - this.setState({ expandedLogsKeys: expandedRows }); - }; - - handleChangePage = (page: number) => { - this.setState({ page }, this.refresh); - }; - - handleChangeOrganizationLogFilters = (filters: any) => { - this.setState({ filters, page: 1 }, this.debouncedRefresh); - }; - - renderShortDescription = (item: OrganizationLog) => { - return
{item.description}
; - }; - - renderFullDescription = (item: OrganizationLog) => { - return ( -
- ); - }; - - renderUser = (item: OrganizationLog) => { - if (!item.author) { - return ( - - - - ); - } - - return ( - - - - - - - - ); - }; - - renderLabels = (item: OrganizationLog) => { - if (!item.labels) { - return null; - } - - return ( - - {item.labels.map((label) => ( - - ))} - - ); - }; - - renderCreatedAt = (item: OrganizationLog) => { - return moment(item.created_at).toString(); - }; -} - -export default withMobXProviderContext(OrganizationLogPage); diff --git a/grafana-plugin/src/pages/routes.tsx b/grafana-plugin/src/pages/routes.tsx index 7969957794..6bacfe95ed 100644 --- a/grafana-plugin/src/pages/routes.tsx +++ b/grafana-plugin/src/pages/routes.tsx @@ -2,7 +2,6 @@ import EscalationsChainsPage from 'pages/escalation-chains/EscalationChains'; import IncidentPage from 'pages/incident/Incident'; import IncidentsPage from 'pages/incidents/Incidents'; import MaintenancePage from 'pages/maintenance/Maintenance'; -import OrganizationLogPage from 'pages/organization-logs/OrganizationLog'; import OutgoingWebhooks from 'pages/outgoing_webhooks/OutgoingWebhooks'; import OutgoingWebhooks2 from 'pages/outgoing_webhooks_2/OutgoingWebhooks2'; import SchedulePage from 'pages/schedule/Schedule'; @@ -73,10 +72,6 @@ export const routes: { [id: string]: NavRoute } = [ component: LiveSettingsPage, id: 'live-settings', }, - { - component: OrganizationLogPage, - id: 'organization-logs', - }, { component: CloudPage, id: 'cloud', diff --git a/grafana-plugin/src/plugin/GrafanaPluginRootPage.tsx b/grafana-plugin/src/plugin/GrafanaPluginRootPage.tsx index 0980b4fc00..b38a35f702 100644 --- a/grafana-plugin/src/plugin/GrafanaPluginRootPage.tsx +++ b/grafana-plugin/src/plugin/GrafanaPluginRootPage.tsx @@ -27,7 +27,6 @@ import Incidents from 'pages/incidents/Incidents'; import Integration from 'pages/integration/Integration'; import Integrations from 'pages/integrations/Integrations'; import Maintenance from 'pages/maintenance/Maintenance'; -import OrganizationLogPage from 'pages/organization-logs/OrganizationLog'; import OutgoingWebhooks from 'pages/outgoing_webhooks/OutgoingWebhooks'; import OutgoingWebhooks2 from 'pages/outgoing_webhooks_2/OutgoingWebhooks2'; import Schedule from 'pages/schedule/Schedule'; @@ -176,9 +175,6 @@ export const Root = observer((props: AppRootProps) => { - - - diff --git a/grafana-plugin/src/state/rootBaseStore/index.ts b/grafana-plugin/src/state/rootBaseStore/index.ts index eaf8804acb..08204da173 100644 --- a/grafana-plugin/src/state/rootBaseStore/index.ts +++ b/grafana-plugin/src/state/rootBaseStore/index.ts @@ -19,7 +19,6 @@ import { GlobalSettingStore } from 'models/global_setting/global_setting'; import { GrafanaTeamStore } from 'models/grafana_team/grafana_team'; import { HeartbeatStore } from 'models/heartbeat/heartbeat'; import { MaintenanceStore } from 'models/maintenance/maintenance'; -import { OrganizationLogStore } from 'models/organization_log/organization_log'; import { OutgoingWebhookStore } from 'models/outgoing_webhook/outgoing_webhook'; import { OutgoingWebhook2Store } from 'models/outgoing_webhook_2/outgoing_webhook_2'; import { ResolutionNotesStore } from 'models/resolution_note/resolution_note'; @@ -104,7 +103,6 @@ export class RootBaseStore { alertGroupStore: AlertGroupStore = new AlertGroupStore(this); resolutionNotesStore: ResolutionNotesStore = new ResolutionNotesStore(this); apiTokenStore: ApiTokenStore = new ApiTokenStore(this); - OrganizationLogStore: OrganizationLogStore = new OrganizationLogStore(this); globalSettingStore: GlobalSettingStore = new GlobalSettingStore(this); filtersStore: FiltersStore = new FiltersStore(this); // stores From ed28917689e670331e2885a741bc1fd60be74126 Mon Sep 17 00:00:00 2001 From: Joey Orlando Date: Mon, 10 Jul 2023 08:57:36 +0200 Subject: [PATCH 03/10] small update --- engine/common/api_helpers/paginators.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/engine/common/api_helpers/paginators.py b/engine/common/api_helpers/paginators.py index b4d19fcdce..f927738243 100644 --- a/engine/common/api_helpers/paginators.py +++ b/engine/common/api_helpers/paginators.py @@ -27,8 +27,8 @@ class BasePathPrefixedPagination(BasePagination): page_size_query_param = "perpage" def paginate_queryset(self, queryset, request, view=None): - self.request = request request.build_absolute_uri = lambda: create_engine_url(request.get_full_path()) + self.request = request return super().paginate_queryset(queryset, request, view) def _get_base_paginated_response_data(self, data: PaginatedData) -> BasePaginatedResponseData: From 711c8dd975574df752efd0f585c9f8097e33ef30 Mon Sep 17 00:00:00 2001 From: Joey Orlando Date: Mon, 10 Jul 2023 09:02:27 +0200 Subject: [PATCH 04/10] fix typo --- .../oss_installation/views/cloud_connection.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/engine/apps/oss_installation/views/cloud_connection.py b/engine/apps/oss_installation/views/cloud_connection.py index 74f4c5d1c1..004837df2e 100644 --- a/engine/apps/oss_installation/views/cloud_connection.py +++ b/engine/apps/oss_installation/views/cloud_connection.py @@ -22,14 +22,14 @@ class CloudConnectionView(APIView): def get(self, request): connector = CloudConnector.objects.first() heartbeat = CloudHeartbeat.objects.first() - return Response( - { - "cloud_notifications_enabled": live_settings.GRAFANA_CLOUD_NOTIFICATIONS_ENABLED, - "cloud_heartbeat_enabled": live_settings.GRAFANA_CLOUD_ONCALL_HEARTBEAT_ENABLED, - "cloud_heartbeat_link": get_heartbeat_link(connector, heartbeat), - "cloud_heartbeat_status": heartbeat is not None and heartbeat.success, - } - ) + response = { + "cloud_connection_status": connector is not None, + "cloud_notifications_enabled": live_settings.GRAFANA_CLOUD_NOTIFICATIONS_ENABLED, + "cloud_heartbeat_enabled": live_settings.GRAFANA_CLOUD_ONCALL_HEARTBEAT_ENABLED, + "cloud_heartbeat_link": get_heartbeat_link(connector, heartbeat), + "cloud_heartbeat_status": heartbeat is not None and heartbeat.success, + } + return Response(response) def delete(self, request): s = LiveSetting.objects.filter(name="GRAFANA_CLOUD_ONCALL_TOKEN").first() From 9033450cc5ba053bc2c5b57e7d41a6cfb2968c0b Mon Sep 17 00:00:00 2001 From: Joey Orlando Date: Mon, 10 Jul 2023 09:13:57 +0200 Subject: [PATCH 05/10] update public docs --- docs/sources/oncall-api-reference/alertgroups.md | 5 ++++- docs/sources/oncall-api-reference/alerts.md | 5 ++++- .../oncall-api-reference/escalation_chains.md | 5 ++++- .../oncall-api-reference/escalation_policies.md | 5 ++++- docs/sources/oncall-api-reference/integrations.md | 7 +++++-- docs/sources/oncall-api-reference/on_call_shifts.md | 5 ++++- .../oncall-api-reference/outgoing_webhooks.md | 5 ++++- .../personal_notification_rules.md | 5 ++++- .../oncall-api-reference/resolution_notes.md | 13 ++++++++----- docs/sources/oncall-api-reference/routes.md | 5 ++++- docs/sources/oncall-api-reference/schedules.md | 10 ++++++++-- docs/sources/oncall-api-reference/slack_channels.md | 5 ++++- docs/sources/oncall-api-reference/user_groups.md | 5 ++++- docs/sources/oncall-api-reference/users.md | 5 ++++- engine/apps/public_api/tests/test_schedules.py | 12 +++++++++--- engine/apps/public_api/views/schedules.py | 3 +++ 16 files changed, 77 insertions(+), 23 deletions(-) diff --git a/docs/sources/oncall-api-reference/alertgroups.md b/docs/sources/oncall-api-reference/alertgroups.md index 9dfe58e4ba..dd5efbbd5f 100644 --- a/docs/sources/oncall-api-reference/alertgroups.md +++ b/docs/sources/oncall-api-reference/alertgroups.md @@ -36,7 +36,10 @@ The above command returns JSON structured in the following way: "telegram": "https://t.me/c/5354/1234?thread=1234" } } - ] + ], + "current_page_number": 1, + "page_size": 50, + "total_pages": 1 } ``` diff --git a/docs/sources/oncall-api-reference/alerts.md b/docs/sources/oncall-api-reference/alerts.md index 8cbc46a01c..7d8935b7bb 100644 --- a/docs/sources/oncall-api-reference/alerts.md +++ b/docs/sources/oncall-api-reference/alerts.md @@ -96,7 +96,10 @@ The above command returns JSON structured in the following way: ] } } - ] + ], + "current_page_number": 1, + "page_size": 50, + "total_pages": 1 } ``` diff --git a/docs/sources/oncall-api-reference/escalation_chains.md b/docs/sources/oncall-api-reference/escalation_chains.md index 301deb2abb..4957402d6a 100644 --- a/docs/sources/oncall-api-reference/escalation_chains.md +++ b/docs/sources/oncall-api-reference/escalation_chains.md @@ -80,7 +80,10 @@ The above command returns JSON structured in the following way: "name": "default", "team_id": null } - ] + ], + "current_page_number": 1, + "page_size": 50, + "total_pages": 1 } ``` diff --git a/docs/sources/oncall-api-reference/escalation_policies.md b/docs/sources/oncall-api-reference/escalation_policies.md index 02c740a82e..c13e1fee99 100644 --- a/docs/sources/oncall-api-reference/escalation_policies.md +++ b/docs/sources/oncall-api-reference/escalation_policies.md @@ -105,7 +105,10 @@ The above command returns JSON structured in the following way: "type": "notify_person_next_each_time", "persons_to_notify_next_each_time": ["U4DNY931HHJS5"] } - ] + ], + "current_page_number": 1, + "page_size": 50, + "total_pages": 1 } ``` diff --git a/docs/sources/oncall-api-reference/integrations.md b/docs/sources/oncall-api-reference/integrations.md index e67ddf135a..52ca69eebc 100644 --- a/docs/sources/oncall-api-reference/integrations.md +++ b/docs/sources/oncall-api-reference/integrations.md @@ -33,7 +33,7 @@ The above command returns JSON structured in the following way: "channel_id": "CH23212D" } }, - "templates": { + "templates": { "grouping_key": null, "resolve_signal": null, "acknowledge_signal": null, @@ -219,7 +219,10 @@ The above command returns JSON structured in the following way: } } } - ] + ], + "current_page_number": 1, + "page_size": 50, + "total_pages": 1 } ``` diff --git a/docs/sources/oncall-api-reference/on_call_shifts.md b/docs/sources/oncall-api-reference/on_call_shifts.md index 98eeab498e..979755f349 100644 --- a/docs/sources/oncall-api-reference/on_call_shifts.md +++ b/docs/sources/oncall-api-reference/on_call_shifts.md @@ -141,7 +141,10 @@ The above command returns JSON structured in the following way: "by_monthday": null, "users": ["U4DNY931HHJS5"] } - ] + ], + "current_page_number": 1, + "page_size": 50, + "total_pages": 1 } ``` diff --git a/docs/sources/oncall-api-reference/outgoing_webhooks.md b/docs/sources/oncall-api-reference/outgoing_webhooks.md index 20bd59f02a..547bdb77cd 100644 --- a/docs/sources/oncall-api-reference/outgoing_webhooks.md +++ b/docs/sources/oncall-api-reference/outgoing_webhooks.md @@ -29,7 +29,10 @@ The above command returns JSON structured in the following way: "id": "KGEFG74LU1D8L", "name": "Publish alert group notification to JIRA" } - ] + ], + "current_page_number": 1, + "page_size": 50, + "total_pages": 1 } ``` diff --git a/docs/sources/oncall-api-reference/personal_notification_rules.md b/docs/sources/oncall-api-reference/personal_notification_rules.md index 853cc47c36..22c3d5d505 100644 --- a/docs/sources/oncall-api-reference/personal_notification_rules.md +++ b/docs/sources/oncall-api-reference/personal_notification_rules.md @@ -113,7 +113,10 @@ The above command returns JSON structured in the following ways: "important": true, "type": "notify_by_phone_call" } - ] + ], + "current_page_number": 1, + "page_size": 50, + "total_pages": 1 } ``` diff --git a/docs/sources/oncall-api-reference/resolution_notes.md b/docs/sources/oncall-api-reference/resolution_notes.md index b5d8cc77c1..24d4eecd6b 100644 --- a/docs/sources/oncall-api-reference/resolution_notes.md +++ b/docs/sources/oncall-api-reference/resolution_notes.md @@ -30,10 +30,10 @@ The above command returns JSON structured in the following way: } ``` -| Parameter | Required | Description | -| --------------- | :------: | :--------------------- | -| `alert_group_id`| Yes | Alert group ID | | -| `text` | Yes | Resolution note text | +| Parameter | Required | Description | +| ---------------- | :------: | :------------------- | --- | +| `alert_group_id` | Yes | Alert group ID | | +| `text` | Yes | Resolution note text | **HTTP request** @@ -90,7 +90,10 @@ The above command returns JSON structured in the following way: "created_at": "2020-06-19T12:40:01.429805Z", "text": "Demo resolution note" } - ] + ], + "current_page_number": 1, + "page_size": 50, + "total_pages": 1 } ``` diff --git a/docs/sources/oncall-api-reference/routes.md b/docs/sources/oncall-api-reference/routes.md index 45c39ae695..8194047766 100644 --- a/docs/sources/oncall-api-reference/routes.md +++ b/docs/sources/oncall-api-reference/routes.md @@ -125,7 +125,10 @@ The above command returns JSON structured in the following way: "channel_id": "CH23212D" } } - ] + ], + "current_page_number": 1, + "page_size": 25, + "total_pages": 1 } ``` diff --git a/docs/sources/oncall-api-reference/schedules.md b/docs/sources/oncall-api-reference/schedules.md index 9076985ca7..32a8d309f2 100644 --- a/docs/sources/oncall-api-reference/schedules.md +++ b/docs/sources/oncall-api-reference/schedules.md @@ -129,7 +129,10 @@ The above command returns JSON structured in the following way: "user_group_id": "MEOW_SLACK_ID" } } - ] + ], + "current_page_number": 1, + "page_size": 50, + "total_pages": 1 } ``` @@ -294,7 +297,10 @@ The above command returns JSON structured in the following way: "shift_start": "2023-01-27T09:00:00Z", "shift_end": "2023-01-27T17:00:00Z" } - ] + ], + "current_page_number": 1, + "page_size": 50, + "total_pages": 1 } ``` diff --git a/docs/sources/oncall-api-reference/slack_channels.md b/docs/sources/oncall-api-reference/slack_channels.md index 82e7815277..e2c39b4406 100644 --- a/docs/sources/oncall-api-reference/slack_channels.md +++ b/docs/sources/oncall-api-reference/slack_channels.md @@ -25,7 +25,10 @@ The above command returns JSON structured in the following way: "name": "meow_channel", "slack_id": "MEOW_SLACK_ID" } - ] + ], + "current_page_number": 1, + "page_size": 50, + "total_pages": 1 } ``` diff --git a/docs/sources/oncall-api-reference/user_groups.md b/docs/sources/oncall-api-reference/user_groups.md index e8f6a80683..49d5894229 100644 --- a/docs/sources/oncall-api-reference/user_groups.md +++ b/docs/sources/oncall-api-reference/user_groups.md @@ -32,7 +32,10 @@ The above command returns JSON structured in the following way: "handle": "meow_group" } } - ] + ], + "current_page_number": 1, + "page_size": 50, + "total_pages": 1 } ``` diff --git a/docs/sources/oncall-api-reference/users.md b/docs/sources/oncall-api-reference/users.md index 9d205fb066..f703e870f3 100644 --- a/docs/sources/oncall-api-reference/users.md +++ b/docs/sources/oncall-api-reference/users.md @@ -76,7 +76,10 @@ The above command returns JSON structured in the following way: "username": "alex", "role": "admin" } - ] + ], + "current_page_number": 1, + "page_size": 100, + "total_pages": 1 } ``` diff --git a/engine/apps/public_api/tests/test_schedules.py b/engine/apps/public_api/tests/test_schedules.py index 820005d8f9..a1b402ed55 100644 --- a/engine/apps/public_api/tests/test_schedules.py +++ b/engine/apps/public_api/tests/test_schedules.py @@ -915,6 +915,12 @@ def test_oncall_shifts_export( assert total_time_on_call[user2_public_primary_key] == expected_time_on_call # pagination parameters are mocked out for now - assert response_json["next"] is None - assert response_json["previous"] is None - assert response_json["count"] == len(shifts) + del response_json["results"] + assert response_json == { + "next": None, + "previous": None, + "count": len(shifts), + "current_page_number": 1, + "page_size": 50, + "total_pages": 1, + } diff --git a/engine/apps/public_api/views/schedules.py b/engine/apps/public_api/views/schedules.py index 6ba33f554d..83aa3207f2 100644 --- a/engine/apps/public_api/views/schedules.py +++ b/engine/apps/public_api/views/schedules.py @@ -175,5 +175,8 @@ def final_shifts(self, request, pk): "next": None, "previous": None, "results": data, + "current_page_number": 1, + "page_size": 50, + "total_pages": 1, } ) From c45d678d8e49d18b9fd3c4b134ec5e7b158f5c1e Mon Sep 17 00:00:00 2001 From: Joey Orlando Date: Mon, 10 Jul 2023 09:25:59 +0200 Subject: [PATCH 06/10] add comment --- engine/common/api_helpers/paginators.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/engine/common/api_helpers/paginators.py b/engine/common/api_helpers/paginators.py index f927738243..34a20c273b 100644 --- a/engine/common/api_helpers/paginators.py +++ b/engine/common/api_helpers/paginators.py @@ -28,7 +28,13 @@ class BasePathPrefixedPagination(BasePagination): def paginate_queryset(self, queryset, request, view=None): request.build_absolute_uri = lambda: create_engine_url(request.get_full_path()) + + # we're setting the request object explicitly here because the way the paginate_quersey works + # between PageNumberPagination and CursorPagination is slightly different. In the latter class, + # it does not set self.request in the paginate_queryset method, whereas in the former it does. + # this leads to an issue in _get_base_paginated_response_data where the self.request would not be set self.request = request + return super().paginate_queryset(queryset, request, view) def _get_base_paginated_response_data(self, data: PaginatedData) -> BasePaginatedResponseData: From d669abe45a1e3a922d1d8ec9d188cdf48ed037e7 Mon Sep 17 00:00:00 2001 From: Joey Orlando Date: Mon, 10 Jul 2023 09:33:57 +0200 Subject: [PATCH 07/10] update changelog --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4db19143df..868f93016b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,12 +11,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add `event.users.avatar_full` field to `GET /api/internal/v1/schedules/{schedule_id}/filter_events` payload by @joeyorlando ([#2459](https://github.com/grafana/oncall/pull/2459)) +- Add `page_size`, `current_page_number`, and `total_pages` attributes to paginated API responses by @joeyorlando ([#2471](https://github.com/grafana/oncall/pull/2471)) ### Changed - Modified DRF pagination class used by `GET /api/internal/v1/alert_receive_channels` and `GET /api/internal/v1/schedules` endpoints so that the `next` and `previous` pagination links are properly set when OnCall is run behind - a reverse proxy by @joeyorlando ([#TBD](https://github.com/grafana/oncall/pull/TBD)) + a reverse proxy by @joeyorlando ([#2467](https://github.com/grafana/oncall/pull/2467)) +- Remove deprecated `organization-logs` plugin UI page by @joeyorlando ([#2467](https://github.com/grafana/oncall/pull/2467)) ## v1.3.7 (2023-07-06) From 0ab753815f67a3d9d1f99cbbb9c42138790e2d51 Mon Sep 17 00:00:00 2001 From: Joey Orlando Date: Mon, 10 Jul 2023 09:40:36 +0200 Subject: [PATCH 08/10] ignore mypy error (safe to do so) --- engine/apps/oss_installation/views/cloud_users.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/engine/apps/oss_installation/views/cloud_users.py b/engine/apps/oss_installation/views/cloud_users.py index be12665b9d..d62fd61209 100644 --- a/engine/apps/oss_installation/views/cloud_users.py +++ b/engine/apps/oss_installation/views/cloud_users.py @@ -17,7 +17,9 @@ class CloudUsersPagination(HundredPageSizePaginator): - def get_paginated_response(self, data: PaginatedData, matched_users_count: int) -> Response: + # the override ignore here is expected. The parent classes' get_paginated_response method does not + # take a matched_users_count argument. This is fine in this case + def get_paginated_response(self, data: PaginatedData, matched_users_count: int) -> Response: # type: ignore[override] return Response( { **self._get_paginated_response_data(data), From 3566c26b14a331c6c5a65f818f83b3ca3f48ca27 Mon Sep 17 00:00:00 2001 From: Joey Orlando Date: Mon, 10 Jul 2023 09:49:15 +0200 Subject: [PATCH 09/10] update tests --- engine/apps/alerts/tests/test_alert_receiver_channel.py | 2 -- engine/apps/api/tests/test_schedules.py | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/engine/apps/alerts/tests/test_alert_receiver_channel.py b/engine/apps/alerts/tests/test_alert_receiver_channel.py index 566b5be6a3..c829913b87 100644 --- a/engine/apps/alerts/tests/test_alert_receiver_channel.py +++ b/engine/apps/alerts/tests/test_alert_receiver_channel.py @@ -110,7 +110,6 @@ def test_send_demo_alert(mocked_create_alert, make_organization, make_alert_rece mocked_create_alert.call_args.args[1]["raw_request_data"] == payload or alert_receive_channel.config.example_payload ) - assert mocked_create_alert.call_args.args[1]["force_route_id"] is None @mock.patch("apps.integrations.tasks.create_alertmanager_alerts.apply_async", return_value=None) @@ -143,7 +142,6 @@ def test_send_demo_alert_alertmanager_payload_shape( if payload else alert_receive_channel.config.example_payload["alerts"][0] ) - assert mocked_create_alert.call_args.args[1]["force_route_id"] is None @mock.patch("apps.integrations.tasks.create_alert.apply_async", return_value=None) diff --git a/engine/apps/api/tests/test_schedules.py b/engine/apps/api/tests/test_schedules.py index 996558b3b1..4cf8eb9c98 100644 --- a/engine/apps/api/tests/test_schedules.py +++ b/engine/apps/api/tests/test_schedules.py @@ -250,7 +250,7 @@ def mock_oncall_now(qs, events_datetime): "next": next_url, "previous": previous_url, "results": [schedule], - "current_page_number": 1, + "current_page_number": p, "page_size": 1, "total_pages": 3, } From c1312d8e8c997fe6510fd02f42fc532b8f2986ca Mon Sep 17 00:00:00 2001 From: Joey Orlando Date: Fri, 14 Jul 2023 07:41:55 -0400 Subject: [PATCH 10/10] Update CHANGELOG.md --- CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 04d22ccc09..fc67849706 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -67,7 +67,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Modified DRF pagination class used by `GET /api/internal/v1/alert_receive_channels` and `GET /api/internal/v1/schedules` endpoints so that the `next` and `previous` pagination links are properly set when OnCall is run behind a reverse proxy by @joeyorlando ([#2467](https://github.com/grafana/oncall/pull/2467)) -- Remove deprecated `organization-logs` plugin UI page by @joeyorlando ([#2471](https://github.com/grafana/oncall/pull/2471)) ### Fixed