commit
e3ef65fcc2
16 changed files with 59 additions and 36 deletions
13
CHANGELOG.md
13
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
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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 = (
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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} />
|
||||
</>
|
||||
)}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue