diff --git a/CHANGELOG.md b/CHANGELOG.md index 79fed934..cce7d1ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,18 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## v1.1.22 (2023-02-03) + +### Fixed + +- Fix bug with root/dependant alert groups list api endpoint ([1284](https://github.com/grafana/oncall/pull/1284)) +- Fixed NPE on teams switch + +### Added + +- Optimize alert and alert group public api endpoints and add filter by id ([1274](https://github.com/grafana/oncall/pull/1274)) +- Enable mobile app backend by default on OSS + ## v1.1.21 (2023-02-02) ### Added @@ -12,6 +24,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add [`django-dbconn-retry` library](https://github.com/jdelic/django-dbconn-retry) to `INSTALLED_APPS` to attempt to alleviate occasional `django.db.utils.OperationalError` errors - Improve alerts and alert group endpoint response time in internal API with caching ([1261](https://github.com/grafana/oncall/pull/1261)) +- Optimize alert and alert group public api endpoints and add filter by id ([1274](https://github.com/grafana/oncall/pull/1274) - Added Coming Soon for iOS on Mobile App screen ### Fixed diff --git a/docs/sources/oncall-api-reference/alertgroups.md b/docs/sources/oncall-api-reference/alertgroups.md index 0d756a5a..5f5addf8 100644 --- a/docs/sources/oncall-api-reference/alertgroups.md +++ b/docs/sources/oncall-api-reference/alertgroups.md @@ -44,6 +44,7 @@ The above command returns JSON structured in the following way: These available filter parameters should be provided as `GET` arguments: +- `id` - `route_id` - `integration_id` - `state` diff --git a/docs/sources/oncall-api-reference/alerts.md b/docs/sources/oncall-api-reference/alerts.md index d67f8e14..ab4b6a3a 100644 --- a/docs/sources/oncall-api-reference/alerts.md +++ b/docs/sources/oncall-api-reference/alerts.md @@ -104,6 +104,7 @@ The above command returns JSON structured in the following way: The following available filter parameters should be provided as `GET` arguments: +- `id` - `alert_group_id` - `search`—string-based inclusion search by alert payload diff --git a/engine/apps/api/serializers/alert_group.py b/engine/apps/api/serializers/alert_group.py index 58c58fdd..d171cd7b 100644 --- a/engine/apps/api/serializers/alert_group.py +++ b/engine/apps/api/serializers/alert_group.py @@ -23,6 +23,7 @@ class AlertGroupFieldsCacheSerializerMixin: def get_or_set_web_template_field( cls, obj, + last_alert, field_name, renderer_class, cache_lifetime=60 * 60 * 24, @@ -31,7 +32,7 @@ class AlertGroupFieldsCacheSerializerMixin: cached_field = cache.get(CACHE_KEY, None) web_templates_modified_at = obj.channel.web_templates_modified_at - last_alert_created_at = obj.last_alert.created_at + last_alert_created_at = last_alert.created_at # use cache only if cache exists # and cache was created after the last alert created @@ -44,7 +45,7 @@ class AlertGroupFieldsCacheSerializerMixin: ): field = cached_field.get(field_name) else: - field = renderer_class(obj, obj.last_alert).render() + field = renderer_class(obj, last_alert).render() cache.set(CACHE_KEY, {"cache_created_at": timezone.now(), field_name: field}, cache_lifetime) return field @@ -60,10 +61,12 @@ class ShortAlertGroupSerializer(AlertGroupFieldsCacheSerializerMixin, serializer fields = ["pk", "render_for_web", "alert_receive_channel", "inside_organization_number"] def get_render_for_web(self, obj): - if not obj.last_alert: + last_alert = obj.alerts.last() + if last_alert is None: return {} return AlertGroupFieldsCacheSerializerMixin.get_or_set_web_template_field( obj, + last_alert, "render_for_web", AlertGroupWebRenderer, ) @@ -133,6 +136,7 @@ class AlertGroupListSerializer(EagerLoadingMixin, AlertGroupFieldsCacheSerialize return {} return AlertGroupFieldsCacheSerializerMixin.get_or_set_web_template_field( obj, + obj.last_alert, "render_for_web", AlertGroupWebRenderer, ) @@ -142,6 +146,7 @@ class AlertGroupListSerializer(EagerLoadingMixin, AlertGroupFieldsCacheSerialize return {} return AlertGroupFieldsCacheSerializerMixin.get_or_set_web_template_field( obj, + obj.last_alert, "render_for_classic_markdown", AlertGroupClassicMarkdownRenderer, ) diff --git a/engine/apps/api/tests/test_features.py b/engine/apps/api/tests/test_features.py index 6fe5c444..6052cd69 100644 --- a/engine/apps/api/tests/test_features.py +++ b/engine/apps/api/tests/test_features.py @@ -7,6 +7,7 @@ from apps.api.views.features import ( FEATURE_GRAFANA_CLOUD_CONNECTION, FEATURE_GRAFANA_CLOUD_NOTIFICATIONS, FEATURE_LIVE_SETTINGS, + FEATURE_MOBILE_APP, FEATURE_SLACK, FEATURE_TELEGRAM, FEATURE_WEB_SCHEDULES, @@ -40,6 +41,7 @@ def test_select_features_all_enabled( settings.OSS_INSTALLATION = True settings.FEATURE_SLACK_INTEGRATION_ENABLED = True settings.FEATURE_TELEGRAM_INTEGRATION_ENABLED = True + settings.FEATURE_MOBILE_APP_INTEGRATION_ENABLED = True settings.FEATURE_LIVE_SETTINGS_ENABLED = True settings.FEATURE_GRAFANA_CLOUD_CONNECTION = True settings.FEATURE_GRAFANA_CLOUD_NOTIFICATIONS = True @@ -52,6 +54,7 @@ def test_select_features_all_enabled( assert response.json() == [ FEATURE_SLACK, FEATURE_TELEGRAM, + FEATURE_MOBILE_APP, FEATURE_GRAFANA_CLOUD_CONNECTION, FEATURE_LIVE_SETTINGS, FEATURE_GRAFANA_CLOUD_NOTIFICATIONS, @@ -69,6 +72,7 @@ def test_select_features_all_disabled( settings.OSS_INSTALLATION = False settings.FEATURE_SLACK_INTEGRATION_ENABLED = False settings.FEATURE_TELEGRAM_INTEGRATION_ENABLED = False + settings.FEATURE_MOBILE_APP_INTEGRATION_ENABLED = False settings.FEATURE_LIVE_SETTINGS_ENABLED = False settings.FEATURE_GRAFANA_CLOUD_CONNECTION = False settings.FEATURE_GRAFANA_CLOUD_NOTIFICATIONS = FEATURE_GRAFANA_CLOUD_NOTIFICATIONS diff --git a/engine/apps/api/views/features.py b/engine/apps/api/views/features.py index b705c23c..ca4cc488 100644 --- a/engine/apps/api/views/features.py +++ b/engine/apps/api/views/features.py @@ -9,7 +9,7 @@ from apps.base.utils import live_settings FEATURE_SLACK = "slack" FEATURE_TELEGRAM = "telegram" FEATURE_LIVE_SETTINGS = "live_settings" -MOBILE_APP_PUSH_NOTIFICATIONS = "mobile_app" +FEATURE_MOBILE_APP = "mobile_app" FEATURE_GRAFANA_CLOUD_NOTIFICATIONS = "grafana_cloud_notifications" FEATURE_GRAFANA_CLOUD_CONNECTION = "grafana_cloud_connection" FEATURE_WEB_SCHEDULES = "web_schedules" @@ -37,7 +37,7 @@ class FeaturesAPIView(APIView): enabled_features.append(FEATURE_TELEGRAM) if settings.FEATURE_MOBILE_APP_INTEGRATION_ENABLED: - enabled_features.append(MOBILE_APP_PUSH_NOTIFICATIONS) + enabled_features.append(FEATURE_MOBILE_APP) if settings.OSS_INSTALLATION: # Features below should be enabled only in OSS diff --git a/engine/apps/api_for_grafana_incident/views.py b/engine/apps/api_for_grafana_incident/views.py index 182100a4..d0c41f04 100644 --- a/engine/apps/api_for_grafana_incident/views.py +++ b/engine/apps/api_for_grafana_incident/views.py @@ -1,4 +1,4 @@ -from rest_framework.viewsets import ReadOnlyModelViewSet +from rest_framework import mixins, viewsets from apps.alerts.models import AlertGroup from apps.auth_token.auth import GrafanaIncidentStaticKeyAuth @@ -6,7 +6,13 @@ from apps.auth_token.auth import GrafanaIncidentStaticKeyAuth from .serializers import AlertGroupSerializer -class AlertGroupsView(ReadOnlyModelViewSet): +class RetrieveViewSet(mixins.RetrieveModelMixin, viewsets.GenericViewSet): + """ + A viewset that provides only `retrieve` actions. + """ + + +class AlertGroupsView(RetrieveViewSet): authentication_classes = (GrafanaIncidentStaticKeyAuth,) queryset = AlertGroup.unarchived_objects.all() serializer_class = AlertGroupSerializer diff --git a/engine/apps/public_api/serializers/incidents.py b/engine/apps/public_api/serializers/incidents.py index 00ec64f6..e6e7e490 100644 --- a/engine/apps/public_api/serializers/incidents.py +++ b/engine/apps/public_api/serializers/incidents.py @@ -13,10 +13,10 @@ class IncidentSerializer(EagerLoadingMixin, serializers.ModelSerializer): route_id = serializers.SerializerMethodField() created_at = serializers.DateTimeField(source="started_at") alerts_count = serializers.SerializerMethodField() - title = serializers.SerializerMethodField() + title = serializers.CharField(source="web_title_cache") state = serializers.SerializerMethodField() - SELECT_RELATED = ["channel", "channel_filter", "slack_message"] + SELECT_RELATED = ["channel", "channel_filter", "slack_message", "channel__organization"] PREFETCH_RELATED = [ "alerts", Prefetch( @@ -44,9 +44,6 @@ class IncidentSerializer(EagerLoadingMixin, serializers.ModelSerializer): def get_alerts_count(self, obj): return len(obj.alerts.all()) - def get_title(self, obj): - return obj.alerts.all()[0].title - def get_state(self, obj): return obj.state diff --git a/engine/apps/public_api/views/alerts.py b/engine/apps/public_api/views/alerts.py index da332176..b4be1c73 100644 --- a/engine/apps/public_api/views/alerts.py +++ b/engine/apps/public_api/views/alerts.py @@ -1,5 +1,6 @@ from django.db.models import CharField from django.db.models.functions import Cast +from django_filters import rest_framework as filters from rest_framework import mixins from rest_framework.permissions import IsAuthenticated from rest_framework.viewsets import GenericViewSet @@ -12,6 +13,10 @@ from common.api_helpers.mixins import RateLimitHeadersMixin from common.api_helpers.paginators import FiftyPageSizePaginator +class AlertFilter(filters.FilterSet): + id = filters.CharFilter(field_name="public_primary_key") + + class AlertView(RateLimitHeadersMixin, mixins.ListModelMixin, GenericViewSet): authentication_classes = (ApiTokenAuthentication,) permission_classes = (IsAuthenticated,) @@ -22,6 +27,9 @@ class AlertView(RateLimitHeadersMixin, mixins.ListModelMixin, GenericViewSet): serializer_class = AlertSerializer pagination_class = FiftyPageSizePaginator + filter_backends = (filters.DjangoFilterBackend,) + filterset_class = AlertFilter + def get_queryset(self): alert_group_id = self.request.query_params.get("alert_group_id", None) search = self.request.query_params.get("search", None) diff --git a/engine/apps/public_api/views/incidents.py b/engine/apps/public_api/views/incidents.py index b771da28..1abb3ad4 100644 --- a/engine/apps/public_api/views/incidents.py +++ b/engine/apps/public_api/views/incidents.py @@ -29,6 +29,8 @@ class IncidentByTeamFilter(ByTeamModelFieldFilterMixin, filters.FilterSet): method=ByTeamModelFieldFilterMixin.filter_model_field_with_single_value.__name__, ) + id = filters.CharFilter(field_name="public_primary_key") + class IncidentView(RateLimitHeadersMixin, mixins.ListModelMixin, mixins.DestroyModelMixin, GenericViewSet): authentication_classes = (ApiTokenAuthentication,) @@ -76,8 +78,6 @@ class IncidentView(RateLimitHeadersMixin, mixins.ListModelMixin, mixins.DestroyM ) raise BadRequest(detail={"state": f"Must be one of the following: {valid_choices_text}"}) - queryset = self.serializer_class.setup_eager_loading(queryset) - return queryset def get_object(self): diff --git a/engine/settings/base.py b/engine/settings/base.py index be04ad98..3e7bf3a3 100644 --- a/engine/settings/base.py +++ b/engine/settings/base.py @@ -57,7 +57,7 @@ FEATURE_LIVE_SETTINGS_ENABLED = getenv_boolean("FEATURE_LIVE_SETTINGS_ENABLED", FEATURE_TELEGRAM_INTEGRATION_ENABLED = getenv_boolean("FEATURE_TELEGRAM_INTEGRATION_ENABLED", default=True) FEATURE_EMAIL_INTEGRATION_ENABLED = getenv_boolean("FEATURE_EMAIL_INTEGRATION_ENABLED", default=True) FEATURE_SLACK_INTEGRATION_ENABLED = getenv_boolean("FEATURE_SLACK_INTEGRATION_ENABLED", default=True) -FEATURE_MOBILE_APP_INTEGRATION_ENABLED = getenv_boolean("FEATURE_MOBILE_APP_INTEGRATION_ENABLED", default=False) +FEATURE_MOBILE_APP_INTEGRATION_ENABLED = getenv_boolean("FEATURE_MOBILE_APP_INTEGRATION_ENABLED", default=True) FEATURE_WEB_SCHEDULES_ENABLED = getenv_boolean("FEATURE_WEB_SCHEDULES_ENABLED", default=False) FEATURE_MULTIREGION_ENABLED = getenv_boolean("FEATURE_MULTIREGION_ENABLED", default=False) GRAFANA_CLOUD_ONCALL_HEARTBEAT_ENABLED = getenv_boolean("GRAFANA_CLOUD_ONCALL_HEARTBEAT_ENABLED", default=True) diff --git a/grafana-plugin/src/PluginPage.tsx b/grafana-plugin/src/PluginPage.tsx index f8a240cb..2fbb8dae 100644 --- a/grafana-plugin/src/PluginPage.tsx +++ b/grafana-plugin/src/PluginPage.tsx @@ -22,7 +22,7 @@ function RealPlugin(props: AppPluginPageProps): React.ReactNode { {/* Render alerts at the top */} -
+
{pages[page]?.text &&

{pages[page].text}

} {props.children} diff --git a/grafana-plugin/src/containers/DefaultPageLayout/DefaultPageLayout.tsx b/grafana-plugin/src/containers/DefaultPageLayout/DefaultPageLayout.tsx index 46ef2f1b..c1fbd2d5 100644 --- a/grafana-plugin/src/containers/DefaultPageLayout/DefaultPageLayout.tsx +++ b/grafana-plugin/src/containers/DefaultPageLayout/DefaultPageLayout.tsx @@ -32,8 +32,10 @@ const DefaultPageLayout: FC = observer((props) => { return renderLegacyNavbar(); function renderTopNavbar(): JSX.Element { + const matchingPageNav = (pages[page] || pages[DEFAULT_PAGE]).getPageNav(); + return ( - +
{children}
); diff --git a/grafana-plugin/src/containers/GrafanaTeamSelect/GrafanaTeamSelect.tsx b/grafana-plugin/src/containers/GrafanaTeamSelect/GrafanaTeamSelect.tsx index 368042fb..7d3f25a4 100644 --- a/grafana-plugin/src/containers/GrafanaTeamSelect/GrafanaTeamSelect.tsx +++ b/grafana-plugin/src/containers/GrafanaTeamSelect/GrafanaTeamSelect.tsx @@ -16,14 +16,9 @@ import styles from './GrafanaTeamSelect.module.scss'; const cx = cn.bind(styles); -interface GrafanaTeamSelectProps { - currentPage: string; -} - -const GrafanaTeamSelect = observer((props: GrafanaTeamSelectProps) => { +const GrafanaTeamSelect = observer(() => { const store = useStore(); - const { currentPage } = props; const { userStore, grafanaTeamStore } = store; const grafanaTeams = grafanaTeamStore.getSearchResult(); const user = userStore.currentUser; @@ -35,16 +30,7 @@ const GrafanaTeamSelect = observer((props: GrafanaTeamSelectProps) => { const onTeamChange = async (teamId: GrafanaTeam['id']) => { await userStore.updateCurrentUser({ current_team: teamId }); - const queryParams = new URLSearchParams(); - queryParams.set('page', mapCurrentPage()); - window.location.search = queryParams.toString(); - - function mapCurrentPage() { - if (currentPage === 'incident') { - return 'incidents'; - } - return currentPage; - } + window.location.reload(); }; const content = ( diff --git a/grafana-plugin/src/navbar/Header/Header.tsx b/grafana-plugin/src/navbar/Header/Header.tsx index 157e74b9..0fac31b4 100644 --- a/grafana-plugin/src/navbar/Header/Header.tsx +++ b/grafana-plugin/src/navbar/Header/Header.tsx @@ -13,7 +13,7 @@ import styles from './Header.module.scss'; const cx = cn.bind(styles); -export default function Header({ page, backendLicense }: { page: string; backendLicense: string }) { +export default function Header({ backendLicense }: { backendLicense: string }) { return (
@@ -25,7 +25,7 @@ export default function Header({ page, backendLicense }: { page: string; backend
{renderHeading()}
- +
diff --git a/grafana-plugin/src/plugin/GrafanaPluginRootPage.tsx b/grafana-plugin/src/plugin/GrafanaPluginRootPage.tsx index 1769f0cb..aa36713f 100644 --- a/grafana-plugin/src/plugin/GrafanaPluginRootPage.tsx +++ b/grafana-plugin/src/plugin/GrafanaPluginRootPage.tsx @@ -110,7 +110,7 @@ export const Root = observer((props: AppRootProps) => { {!isTopNavbar() && ( <> -
+
)}