Merge pull request #1287 from grafana/dev

Merge dev to main
This commit is contained in:
Vadim Stepanov 2023-02-03 12:53:43 +00:00 committed by GitHub
commit e3ef65fcc2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 59 additions and 36 deletions

View file

@ -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

View file

@ -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`

View file

@ -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

View file

@ -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,
)

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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):

View file

@ -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)

View file

@ -22,7 +22,7 @@ function RealPlugin(props: AppPluginPageProps): React.ReactNode {
<RealPluginPage {...props}>
{/* Render alerts at the top */}
<Alerts />
<Header page={page} backendLicense={store.backendLicense} />
<Header backendLicense={store.backendLicense} />
{pages[page]?.text && <h3 className="page-title">{pages[page].text}</h3>}
{props.children}
</RealPluginPage>

View file

@ -32,8 +32,10 @@ const DefaultPageLayout: FC<DefaultPageLayoutProps> = observer((props) => {
return renderLegacyNavbar();
function renderTopNavbar(): JSX.Element {
const matchingPageNav = (pages[page] || pages[DEFAULT_PAGE]).getPageNav();
return (
<PluginPage pageNav={pages[page].getPageNav()}>
<PluginPage page={page} pageNav={matchingPageNav}>
<div className={cx('root')}>{children}</div>
</PluginPage>
);

View file

@ -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 = (

View file

@ -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 (
<div className={cx('root')}>
<div className={cx('page-header__inner', { 'header-topnavbar': isTopNavbar() })}>
@ -25,7 +25,7 @@ export default function Header({ page, backendLicense }: { page: string; backend
<div className="page-header__info-block">{renderHeading()}</div>
</div>
<div className={cx('navbar-right')}>
<GrafanaTeamSelect currentPage={page} />
<GrafanaTeamSelect />
</div>
</div>
</div>

View file

@ -110,7 +110,7 @@ export const Root = observer((props: AppRootProps) => {
<DefaultPageLayout {...props} page={page}>
{!isTopNavbar() && (
<>
<Header page={page} backendLicense={store.backendLicense} />
<Header backendLicense={store.backendLicense} />
<LegacyNavTabsBar currentPage={page} />
</>
)}