commit
128240000d
76 changed files with 1031 additions and 585 deletions
13
CHANGELOG.md
13
CHANGELOG.md
|
|
@ -5,7 +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).
|
||||
|
||||
## Unreleased
|
||||
## v1.3.86 (2024-01-12)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix unicode characters not rendering correctly in webhooks @mderynck ([#3670](https://github.com/grafana/oncall/pull/3670))
|
||||
- UI bug related to time inputs for "current UTC time is in" range escalation policy step ([#3585](https://github.com/grafana/oncall/issues/3585)]
|
||||
- MS Teams Connection user profile tab - shouldn't reshow connection steps if already connected ([#2427](https://github.com/grafana/oncall-private/issues/2427))
|
||||
- Fix internal schedule detail API to set oncall_now for a schedule in orgs with multiple entries ([#3671](https://github.com/grafana/oncall/pull/3671))
|
||||
|
||||
## v1.3.85 (2024-01-12)
|
||||
|
||||
Maintenance release
|
||||
|
||||
## v1.3.84 (2024-01-10)
|
||||
|
||||
|
|
|
|||
2
Tiltfile
2
Tiltfile
|
|
@ -11,7 +11,7 @@ if not running_under_parent_tiltfile:
|
|||
# Load the custom Grafana extensions
|
||||
v1alpha1.extension_repo(
|
||||
name="grafana-tilt-extensions",
|
||||
ref="main",
|
||||
ref="v1.2.0",
|
||||
url="https://github.com/grafana/tilt-extensions",
|
||||
)
|
||||
v1alpha1.extension(
|
||||
|
|
|
|||
|
|
@ -348,7 +348,7 @@ class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models.
|
|||
return self.silenced and self.silenced_until is not None
|
||||
|
||||
@property
|
||||
def status(self):
|
||||
def status(self) -> int:
|
||||
if self.resolved:
|
||||
return AlertGroup.RESOLVED
|
||||
elif self.acknowledged:
|
||||
|
|
|
|||
|
|
@ -412,7 +412,7 @@ class AlertReceiveChannel(IntegrationOptionsMixin, MaintainableObject):
|
|||
return Alert.objects.filter(group__channel=self).count()
|
||||
|
||||
@property
|
||||
def is_able_to_autoresolve(self):
|
||||
def is_able_to_autoresolve(self) -> bool:
|
||||
return self.config.is_able_to_autoresolve
|
||||
|
||||
@property
|
||||
|
|
@ -420,7 +420,7 @@ class AlertReceiveChannel(IntegrationOptionsMixin, MaintainableObject):
|
|||
return self.config.is_demo_alert_enabled
|
||||
|
||||
@property
|
||||
def description(self):
|
||||
def description(self) -> str | None:
|
||||
# TODO: AMV2: Remove this check after legacy integrations are migrated.
|
||||
if self.integration == AlertReceiveChannel.INTEGRATION_LEGACY_GRAFANA_ALERTING:
|
||||
contact_points = self.contact_points.all()
|
||||
|
|
@ -496,7 +496,7 @@ class AlertReceiveChannel(IntegrationOptionsMixin, MaintainableObject):
|
|||
return urljoin(self.organization.web_link, f"integrations/{self.public_primary_key}")
|
||||
|
||||
@property
|
||||
def integration_url(self):
|
||||
def integration_url(self) -> str | None:
|
||||
if self.integration in [
|
||||
AlertReceiveChannel.INTEGRATION_MANUAL,
|
||||
AlertReceiveChannel.INTEGRATION_SLACK_CHANNEL,
|
||||
|
|
@ -595,7 +595,7 @@ class AlertReceiveChannel(IntegrationOptionsMixin, MaintainableObject):
|
|||
|
||||
# Heartbeat
|
||||
@property
|
||||
def is_available_for_integration_heartbeat(self):
|
||||
def is_available_for_integration_heartbeat(self) -> bool:
|
||||
return self.heartbeat_module is not None
|
||||
|
||||
@property
|
||||
|
|
|
|||
|
|
@ -151,7 +151,7 @@ class MaintainableObject(models.Model):
|
|||
)
|
||||
|
||||
@property
|
||||
def till_maintenance_timestamp(self):
|
||||
def till_maintenance_timestamp(self) -> int | None:
|
||||
if self.maintenance_started_at is not None and self.maintenance_duration is not None:
|
||||
return int((self.maintenance_started_at + self.maintenance_duration).astimezone(pytz.UTC).timestamp())
|
||||
return None
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import typing
|
||||
|
||||
from django.core.cache import cache
|
||||
from django.utils import timezone
|
||||
from rest_framework import serializers
|
||||
|
|
@ -8,6 +10,13 @@ from apps.alerts.models import Alert
|
|||
from .alerts_field_cache_buster_mixin import AlertsFieldCacheBusterMixin
|
||||
|
||||
|
||||
class RenderForWeb(typing.TypedDict):
|
||||
title: str
|
||||
message: str
|
||||
image_url: str | None
|
||||
source_link: str | None
|
||||
|
||||
|
||||
class AlertFieldsCacheSerializerMixin(AlertsFieldCacheBusterMixin):
|
||||
CACHE_KEY_FORMAT_TEMPLATE = "{field_name}_alert_{object_id}"
|
||||
|
||||
|
|
@ -51,7 +60,7 @@ class AlertSerializer(AlertFieldsCacheSerializerMixin, serializers.ModelSerializ
|
|||
"created_at",
|
||||
]
|
||||
|
||||
def get_render_for_web(self, obj):
|
||||
def get_render_for_web(self, obj) -> RenderForWeb:
|
||||
return AlertFieldsCacheSerializerMixin.get_or_set_web_template_field(
|
||||
obj,
|
||||
AlertFieldsCacheSerializerMixin.RENDER_FOR_WEB_FIELD_NAME,
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import typing
|
|||
|
||||
from django.core.cache import cache
|
||||
from django.utils import timezone
|
||||
from drf_spectacular.utils import extend_schema_field, inline_serializer
|
||||
from drf_spectacular.utils import extend_schema_field
|
||||
from rest_framework import serializers
|
||||
|
||||
from apps.alerts.incident_appearance.renderers.web_renderer import AlertGroupWebRenderer
|
||||
|
|
@ -22,6 +22,17 @@ logger = logging.getLogger(__name__)
|
|||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
|
||||
class RenderForWeb(typing.TypedDict):
|
||||
title: str
|
||||
message: str
|
||||
image_url: str | None
|
||||
source_link: str | None
|
||||
|
||||
|
||||
class EmptyRenderForWeb(typing.TypedDict):
|
||||
pass
|
||||
|
||||
|
||||
class AlertGroupFieldsCacheSerializerMixin(AlertsFieldCacheBusterMixin):
|
||||
CACHE_KEY_FORMAT_TEMPLATE = "{field_name}_alert_group_{object_id}"
|
||||
|
||||
|
|
@ -80,18 +91,7 @@ class ShortAlertGroupSerializer(AlertGroupFieldsCacheSerializerMixin, serializer
|
|||
fields = ["pk", "render_for_web", "alert_receive_channel", "inside_organization_number"]
|
||||
read_only_fields = ["pk", "render_for_web", "alert_receive_channel", "inside_organization_number"]
|
||||
|
||||
@extend_schema_field(
|
||||
inline_serializer(
|
||||
name="render_for_web",
|
||||
fields={
|
||||
"title": serializers.CharField(),
|
||||
"message": serializers.CharField(),
|
||||
"image_url": serializers.CharField(),
|
||||
"source_link": serializers.CharField(),
|
||||
},
|
||||
)
|
||||
)
|
||||
def get_render_for_web(self, obj: "AlertGroup"):
|
||||
def get_render_for_web(self, obj: "AlertGroup") -> RenderForWeb | EmptyRenderForWeb:
|
||||
last_alert = obj.alerts.last()
|
||||
if last_alert is None:
|
||||
return {}
|
||||
|
|
@ -170,18 +170,7 @@ class AlertGroupListSerializer(
|
|||
"labels",
|
||||
]
|
||||
|
||||
@extend_schema_field(
|
||||
inline_serializer(
|
||||
name="render_for_web",
|
||||
fields={
|
||||
"title": serializers.CharField(),
|
||||
"message": serializers.CharField(),
|
||||
"image_url": serializers.CharField(),
|
||||
"source_link": serializers.CharField(),
|
||||
},
|
||||
)
|
||||
)
|
||||
def get_render_for_web(self, obj: "AlertGroup"):
|
||||
def get_render_for_web(self, obj: "AlertGroup") -> RenderForWeb | EmptyRenderForWeb:
|
||||
if not obj.last_alert:
|
||||
return {}
|
||||
return AlertGroupFieldsCacheSerializerMixin.get_or_set_web_template_field(
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import typing
|
|||
from collections import OrderedDict
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.core.exceptions import ValidationError as DjangoValidationError
|
||||
from django.db.models import Q
|
||||
from jinja2 import TemplateSyntaxError
|
||||
|
|
@ -53,17 +52,17 @@ class IntegrationAlertGroupLabels(typing.TypedDict):
|
|||
class CustomLabelSerializer(serializers.Serializer):
|
||||
"""This serializer is consistent with apps.api.serializers.labels.LabelSerializer, but allows null for value ID."""
|
||||
|
||||
class KeySerializer(serializers.Serializer):
|
||||
class CustomLabelKeySerializer(serializers.Serializer):
|
||||
id = serializers.CharField()
|
||||
name = serializers.CharField()
|
||||
|
||||
class ValueSerializer(serializers.Serializer):
|
||||
class CustomLabelValueSerializer(serializers.Serializer):
|
||||
# ID is null for templated labels. For such labels, the "name" value is a Jinja2 template.
|
||||
id = serializers.CharField(allow_null=True)
|
||||
name = serializers.CharField()
|
||||
|
||||
key = KeySerializer()
|
||||
value = ValueSerializer()
|
||||
key = CustomLabelKeySerializer()
|
||||
value = CustomLabelValueSerializer()
|
||||
|
||||
|
||||
class IntegrationAlertGroupLabelsSerializer(serializers.Serializer):
|
||||
|
|
@ -215,9 +214,9 @@ class AlertReceiveChannelSerializer(
|
|||
default_channel_filter = serializers.SerializerMethodField()
|
||||
instructions = serializers.SerializerMethodField()
|
||||
demo_alert_enabled = serializers.BooleanField(source="is_demo_alert_enabled", read_only=True)
|
||||
is_based_on_alertmanager = serializers.BooleanField(source="has_alertmanager_payload_structure", read_only=True)
|
||||
is_based_on_alertmanager = serializers.BooleanField(source="based_on_alertmanager", read_only=True)
|
||||
maintenance_till = serializers.ReadOnlyField(source="till_maintenance_timestamp")
|
||||
heartbeat = serializers.SerializerMethodField()
|
||||
heartbeat = IntegrationHeartBeatSerializer(read_only=True, allow_null=True, source="integration_heartbeat")
|
||||
allow_delete = serializers.SerializerMethodField()
|
||||
description_short = serializers.CharField(max_length=250, required=False, allow_null=True)
|
||||
demo_alert_payload = serializers.JSONField(source="config.example_payload", read_only=True)
|
||||
|
|
@ -334,15 +333,16 @@ class AlertReceiveChannelSerializer(
|
|||
except AlertReceiveChannel.DuplicateDirectPagingError:
|
||||
raise BadRequest(detail=AlertReceiveChannel.DuplicateDirectPagingError.DETAIL)
|
||||
|
||||
def get_instructions(self, obj: "AlertReceiveChannel"):
|
||||
def get_instructions(self, obj: "AlertReceiveChannel") -> str:
|
||||
# Deprecated, kept for api-backward compatibility
|
||||
return ""
|
||||
|
||||
# MethodFields are used instead of relevant properties because of properties hit db on each instance in queryset
|
||||
def get_default_channel_filter(self, obj: "AlertReceiveChannel"):
|
||||
def get_default_channel_filter(self, obj: "AlertReceiveChannel") -> str | None:
|
||||
for filter in obj.channel_filters.all():
|
||||
if filter.is_default:
|
||||
return filter.public_primary_key
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def validate_integration(integration):
|
||||
|
|
@ -367,21 +367,14 @@ class AlertReceiveChannelSerializer(
|
|||
else:
|
||||
raise serializers.ValidationError(detail="Integration with this name already exists")
|
||||
|
||||
def get_heartbeat(self, obj: "AlertReceiveChannel"):
|
||||
try:
|
||||
heartbeat = obj.integration_heartbeat
|
||||
except ObjectDoesNotExist:
|
||||
return None
|
||||
return IntegrationHeartBeatSerializer(heartbeat).data
|
||||
|
||||
def get_allow_delete(self, obj: "AlertReceiveChannel"):
|
||||
def get_allow_delete(self, obj: "AlertReceiveChannel") -> bool:
|
||||
# don't allow deleting direct paging integrations
|
||||
return obj.integration != AlertReceiveChannel.INTEGRATION_DIRECT_PAGING
|
||||
|
||||
def get_alert_count(self, obj: "AlertReceiveChannel"):
|
||||
def get_alert_count(self, obj: "AlertReceiveChannel") -> int:
|
||||
return 0
|
||||
|
||||
def get_alert_groups_count(self, obj: "AlertReceiveChannel"):
|
||||
def get_alert_groups_count(self, obj: "AlertReceiveChannel") -> int:
|
||||
return 0
|
||||
|
||||
def get_routes_count(self, obj: "AlertReceiveChannel") -> int:
|
||||
|
|
@ -428,10 +421,10 @@ class FilterAlertReceiveChannelSerializer(serializers.ModelSerializer[AlertRecei
|
|||
model = AlertReceiveChannel
|
||||
fields = ["value", "display_name", "integration_url"]
|
||||
|
||||
def _get_value(self, obj: "AlertReceiveChannel"):
|
||||
def _get_value(self, obj: "AlertReceiveChannel") -> str:
|
||||
return obj.public_primary_key
|
||||
|
||||
def get_display_name(self, obj: "AlertReceiveChannel"):
|
||||
def get_display_name(self, obj: "AlertReceiveChannel") -> str:
|
||||
display_name = obj.verbal_name or AlertReceiveChannel.INTEGRATION_CHOICES[obj.integration][1]
|
||||
return display_name
|
||||
|
||||
|
|
|
|||
|
|
@ -41,10 +41,10 @@ class IntegrationHeartBeatSerializer(EagerLoadingMixin, serializers.ModelSeriali
|
|||
{"alert_receive_channel": "Heartbeat is not available for this integration"}
|
||||
)
|
||||
|
||||
def get_last_heartbeat_time_verbal(self, obj):
|
||||
def get_last_heartbeat_time_verbal(self, obj) -> str | None:
|
||||
return self._last_heartbeat_time_verbal(obj) if obj.last_heartbeat_time else None
|
||||
|
||||
def get_instruction(self, obj):
|
||||
def get_instruction(self, obj) -> str:
|
||||
# Deprecated. Kept for API backward compatibility.
|
||||
return ""
|
||||
|
||||
|
|
|
|||
|
|
@ -14,5 +14,5 @@ class SlackUserIdentitySerializer(serializers.ModelSerializer):
|
|||
fields = ["slack_login", "slack_id", "avatar", "name", "display_name"]
|
||||
read_only_fields = ["slack_login", "slack_id", "avatar", "name", "display_name"]
|
||||
|
||||
def get_display_name(self, obj):
|
||||
def get_display_name(self, obj) -> str | None:
|
||||
return obj.profile_display_name or obj.slack_verbal
|
||||
|
|
|
|||
|
|
@ -9,10 +9,10 @@ from apps.api.serializers.telegram import TelegramToUserConnectorSerializer
|
|||
from apps.base.messaging import get_messaging_backends
|
||||
from apps.base.models import UserNotificationPolicy
|
||||
from apps.base.utils import live_settings
|
||||
from apps.oss_installation.constants import CloudSyncStatus
|
||||
from apps.oss_installation.utils import cloud_user_identity_status
|
||||
from apps.schedules.ical_utils import SchedulesOnCallUsers
|
||||
from apps.user_management.models import User
|
||||
from apps.user_management.models.user import default_working_hours
|
||||
from common.api_helpers.custom_fields import TeamPrimaryKeyRelatedField, TimeZoneField
|
||||
from common.api_helpers.mixins import EagerLoadingMixin
|
||||
from common.api_helpers.utils import check_phone_number_is_valid
|
||||
|
|
@ -31,6 +31,26 @@ class UserPermissionSerializer(serializers.Serializer):
|
|||
action = serializers.CharField(read_only=True)
|
||||
|
||||
|
||||
class NotificationChainVerbal(typing.TypedDict):
|
||||
default: str
|
||||
important: str
|
||||
|
||||
|
||||
class WorkingHoursPeriodSerializer(serializers.Serializer):
|
||||
start = serializers.CharField()
|
||||
end = serializers.CharField()
|
||||
|
||||
|
||||
class WorkingHoursSerializer(serializers.Serializer):
|
||||
monday = serializers.ListField(child=WorkingHoursPeriodSerializer())
|
||||
tuesday = serializers.ListField(child=WorkingHoursPeriodSerializer())
|
||||
wednesday = serializers.ListField(child=WorkingHoursPeriodSerializer())
|
||||
thursday = serializers.ListField(child=WorkingHoursPeriodSerializer())
|
||||
friday = serializers.ListField(child=WorkingHoursPeriodSerializer())
|
||||
saturday = serializers.ListField(child=WorkingHoursPeriodSerializer())
|
||||
sunday = serializers.ListField(child=WorkingHoursPeriodSerializer())
|
||||
|
||||
|
||||
class UserSerializer(DynamicFieldsModelSerializer, EagerLoadingMixin):
|
||||
pk = serializers.CharField(read_only=True, source="public_primary_key")
|
||||
slack_user_identity = SlackUserIdentitySerializer(read_only=True)
|
||||
|
|
@ -47,6 +67,7 @@ class UserSerializer(DynamicFieldsModelSerializer, EagerLoadingMixin):
|
|||
avatar_full = serializers.URLField(source="avatar_full_url", read_only=True)
|
||||
notification_chain_verbal = serializers.SerializerMethodField()
|
||||
cloud_connection_status = serializers.SerializerMethodField()
|
||||
working_hours = WorkingHoursSerializer(required=False)
|
||||
|
||||
SELECT_RELATED = ["telegram_verification_code", "telegram_connection", "organization", "slack_user_identity"]
|
||||
|
||||
|
|
@ -82,29 +103,8 @@ class UserSerializer(DynamicFieldsModelSerializer, EagerLoadingMixin):
|
|||
]
|
||||
|
||||
def validate_working_hours(self, working_hours):
|
||||
if not isinstance(working_hours, dict):
|
||||
raise serializers.ValidationError("must be dict")
|
||||
|
||||
# check that all days are present
|
||||
if sorted(working_hours.keys()) != sorted(default_working_hours().keys()):
|
||||
raise serializers.ValidationError("missing some days")
|
||||
|
||||
for day in working_hours:
|
||||
periods = working_hours[day]
|
||||
|
||||
if not isinstance(periods, list):
|
||||
raise serializers.ValidationError("periods must be list")
|
||||
|
||||
for period in periods:
|
||||
if not isinstance(period, dict):
|
||||
raise serializers.ValidationError("period must be dict")
|
||||
|
||||
if sorted(period.keys()) != sorted(["start", "end"]):
|
||||
raise serializers.ValidationError("'start' and 'end' fields must be present")
|
||||
|
||||
if not isinstance(period["start"], str) or not isinstance(period["end"], str):
|
||||
raise serializers.ValidationError("'start' and 'end' fields must be str")
|
||||
|
||||
for period in working_hours[day]:
|
||||
try:
|
||||
start = time.strptime(period["start"], "%H:%M:%S")
|
||||
end = time.strptime(period["end"], "%H:%M:%S")
|
||||
|
|
@ -113,7 +113,6 @@ class UserSerializer(DynamicFieldsModelSerializer, EagerLoadingMixin):
|
|||
|
||||
if start >= end:
|
||||
raise serializers.ValidationError("'start' must be less than 'end'")
|
||||
|
||||
return working_hours
|
||||
|
||||
def validate_unverified_phone_number(self, value):
|
||||
|
|
@ -127,18 +126,18 @@ class UserSerializer(DynamicFieldsModelSerializer, EagerLoadingMixin):
|
|||
else:
|
||||
return None
|
||||
|
||||
def get_messaging_backends(self, obj: User):
|
||||
def get_messaging_backends(self, obj: User) -> dict[str, dict]:
|
||||
serialized_data = {}
|
||||
supported_backends = get_messaging_backends()
|
||||
for backend_id, backend in supported_backends:
|
||||
serialized_data[backend_id] = backend.serialize_user(obj)
|
||||
return serialized_data
|
||||
|
||||
def get_notification_chain_verbal(self, obj: User):
|
||||
def get_notification_chain_verbal(self, obj: User) -> NotificationChainVerbal:
|
||||
default, important = UserNotificationPolicy.get_short_verbals_for_user(user=obj)
|
||||
return {"default": " - ".join(default), "important": " - ".join(important)}
|
||||
|
||||
def get_cloud_connection_status(self, obj: User):
|
||||
def get_cloud_connection_status(self, obj: User) -> CloudSyncStatus | None:
|
||||
if settings.IS_OPEN_SOURCE and live_settings.GRAFANA_CLOUD_NOTIFICATIONS_ENABLED:
|
||||
connector = self.context.get("connector", None)
|
||||
identities = self.context.get("cloud_identities", {})
|
||||
|
|
|
|||
|
|
@ -4,14 +4,7 @@ from django.urls import reverse
|
|||
from rest_framework import status
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from apps.api.views.features import (
|
||||
FEATURE_GRAFANA_CLOUD_CONNECTION,
|
||||
FEATURE_GRAFANA_CLOUD_NOTIFICATIONS,
|
||||
FEATURE_LIVE_SETTINGS,
|
||||
FEATURE_MSTEAMS,
|
||||
FEATURE_SLACK,
|
||||
FEATURE_TELEGRAM,
|
||||
)
|
||||
from apps.api.views.features import Feature
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
|
|
@ -35,9 +28,9 @@ def test_features_view(
|
|||
@pytest.mark.parametrize(
|
||||
"feature_attr,expected_feature",
|
||||
[
|
||||
("FEATURE_SLACK_INTEGRATION_ENABLED", FEATURE_SLACK),
|
||||
("FEATURE_TELEGRAM_INTEGRATION_ENABLED", FEATURE_TELEGRAM),
|
||||
("FEATURE_LIVE_SETTINGS_ENABLED", FEATURE_LIVE_SETTINGS),
|
||||
("FEATURE_SLACK_INTEGRATION_ENABLED", Feature.SLACK),
|
||||
("FEATURE_TELEGRAM_INTEGRATION_ENABLED", Feature.TELEGRAM),
|
||||
("FEATURE_LIVE_SETTINGS_ENABLED", Feature.LIVE_SETTINGS),
|
||||
],
|
||||
)
|
||||
def test_core_features_switch(
|
||||
|
|
@ -76,9 +69,9 @@ def test_oss_features_enabled_in_oss_installation_by_default(
|
|||
response = client.get(url, format="json", **make_user_auth_headers(user, token))
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert FEATURE_GRAFANA_CLOUD_CONNECTION in response.json()
|
||||
assert FEATURE_GRAFANA_CLOUD_NOTIFICATIONS in response.json()
|
||||
assert FEATURE_MSTEAMS not in response.json()
|
||||
assert Feature.GRAFANA_CLOUD_CONNECTION in response.json()
|
||||
assert Feature.GRAFANA_CLOUD_NOTIFICATIONS in response.json()
|
||||
assert Feature.MSTEAMS not in response.json()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
|
|
@ -93,14 +86,14 @@ def test_non_oss_features_enabled(
|
|||
response = client.get(url, format="json", **make_user_auth_headers(user, token))
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert FEATURE_MSTEAMS in response.json()
|
||||
assert Feature.MSTEAMS in response.json()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.parametrize(
|
||||
"feature_attr,expected_feature",
|
||||
[
|
||||
("GRAFANA_CLOUD_NOTIFICATIONS_ENABLED", FEATURE_GRAFANA_CLOUD_NOTIFICATIONS),
|
||||
("GRAFANA_CLOUD_NOTIFICATIONS_ENABLED", Feature.GRAFANA_CLOUD_NOTIFICATIONS),
|
||||
],
|
||||
)
|
||||
def test_oss_features_switch(
|
||||
|
|
|
|||
|
|
@ -565,6 +565,69 @@ def test_get_detail_web_schedule(
|
|||
assert response.data == expected_payload
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_get_detail_schedule_oncall_now_multipage_objects(
|
||||
make_organization_and_user_with_plugin_token, make_schedule, make_on_call_shift, make_user_auth_headers
|
||||
):
|
||||
organization, user, token = make_organization_and_user_with_plugin_token()
|
||||
# make sure our schedule would be in the second page of the listing page
|
||||
for i in range(16):
|
||||
make_schedule(organization, schedule_class=OnCallScheduleWeb, name=f"schedule {i}")
|
||||
|
||||
schedule = make_schedule(
|
||||
organization,
|
||||
schedule_class=OnCallScheduleWeb,
|
||||
name="test_web_schedule",
|
||||
)
|
||||
now = timezone.now().replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
start_date = now - timezone.timedelta(days=7)
|
||||
data = {
|
||||
"start": start_date,
|
||||
"rotation_start": start_date,
|
||||
"duration": timezone.timedelta(seconds=86400),
|
||||
"priority_level": 1,
|
||||
"frequency": CustomOnCallShift.FREQUENCY_DAILY,
|
||||
"schedule": schedule,
|
||||
}
|
||||
on_call_shift = make_on_call_shift(
|
||||
organization=organization, shift_type=CustomOnCallShift.TYPE_ROLLING_USERS_EVENT, **data
|
||||
)
|
||||
on_call_shift.add_rolling_users([[user]])
|
||||
|
||||
client = APIClient()
|
||||
url = reverse("api-internal:schedule-detail", kwargs={"pk": schedule.public_primary_key})
|
||||
|
||||
expected_payload = {
|
||||
"id": schedule.public_primary_key,
|
||||
"team": None,
|
||||
"name": "test_web_schedule",
|
||||
"type": 2,
|
||||
"time_zone": "UTC",
|
||||
"slack_channel": None,
|
||||
"user_group": None,
|
||||
"warnings": [],
|
||||
"on_call_now": [
|
||||
{
|
||||
"pk": user.public_primary_key,
|
||||
"username": user.username,
|
||||
"avatar": user.avatar_url,
|
||||
"avatar_full": user.avatar_full_url,
|
||||
}
|
||||
],
|
||||
"has_gaps": False,
|
||||
"mention_oncall_next": False,
|
||||
"mention_oncall_start": True,
|
||||
"notify_empty_oncall": 0,
|
||||
"notify_oncall_shift_freq": 1,
|
||||
"number_of_escalation_chains": 0,
|
||||
"enable_web_overrides": True,
|
||||
}
|
||||
|
||||
response = client.get(url, format="json", **make_user_auth_headers(user, token))
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.data == expected_payload
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_create_calendar_schedule(schedule_internal_api_setup, make_user_auth_headers):
|
||||
user, token, _, _, _, _ = schedule_internal_api_setup
|
||||
|
|
|
|||
|
|
@ -287,6 +287,48 @@ def test_list_users_filtered_by_public_primary_key(
|
|||
assert returned_user_pks == [user1.public_primary_key]
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_list_users_filtered_by_team(
|
||||
make_organization,
|
||||
make_team,
|
||||
make_user_for_organization,
|
||||
make_token_for_organization,
|
||||
make_user_auth_headers,
|
||||
):
|
||||
organization = make_organization()
|
||||
user1 = make_user_for_organization(organization)
|
||||
user2 = make_user_for_organization(organization)
|
||||
|
||||
team1 = make_team(organization)
|
||||
team2 = make_team(organization)
|
||||
team3 = make_team(organization)
|
||||
|
||||
user1.teams.add(team1)
|
||||
user1.teams.add(team2)
|
||||
user2.teams.add(team2)
|
||||
|
||||
_, token = make_token_for_organization(organization)
|
||||
client = APIClient()
|
||||
url = reverse("api-internal:user-list")
|
||||
|
||||
def _get_user_pks(teams):
|
||||
response = client.get(
|
||||
url,
|
||||
data={"team": [team.public_primary_key for team in teams]}, # these are query params
|
||||
**make_user_auth_headers(user1, token),
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
return [u["pk"] for u in response.json()["results"]]
|
||||
|
||||
assert _get_user_pks([team1]) == [user1.public_primary_key]
|
||||
assert _get_user_pks([team1, team2]) == [user1.public_primary_key, user2.public_primary_key]
|
||||
assert _get_user_pks([team3]) == []
|
||||
|
||||
# check non-existent team returns bad request
|
||||
response = client.get(f"{url}?team=null", **make_user_auth_headers(user1, token))
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_notification_chain_verbal(
|
||||
make_organization,
|
||||
|
|
|
|||
|
|
@ -4,8 +4,7 @@ from django.core.exceptions import ObjectDoesNotExist
|
|||
from django.db.models import Count, Max, Q
|
||||
from django.utils import timezone
|
||||
from django_filters import rest_framework as filters
|
||||
from django_filters.widgets import RangeWidget
|
||||
from drf_spectacular.utils import extend_schema, extend_schema_view, inline_serializer
|
||||
from drf_spectacular.utils import extend_schema, inline_serializer
|
||||
from rest_framework import mixins, serializers, status, viewsets
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.exceptions import NotFound
|
||||
|
|
@ -29,7 +28,12 @@ from apps.labels.utils import is_labels_feature_enabled
|
|||
from apps.mobile_app.auth import MobileAppAuthTokenAuthentication
|
||||
from apps.user_management.models import Team, User
|
||||
from common.api_helpers.exceptions import BadRequest
|
||||
from common.api_helpers.filters import NO_TEAM_VALUE, DateRangeFilterMixin, ModelFieldFilterMixin
|
||||
from common.api_helpers.filters import (
|
||||
NO_TEAM_VALUE,
|
||||
DateRangeFilterMixin,
|
||||
ModelFieldFilterMixin,
|
||||
MultipleChoiceCharFilter,
|
||||
)
|
||||
from common.api_helpers.mixins import PreviewTemplateMixin, PublicPrimaryKeyMixin, TeamFilteringMixin
|
||||
from common.api_helpers.paginators import AlertGroupCursorPaginator
|
||||
|
||||
|
|
@ -55,31 +59,6 @@ def get_user_queryset(request):
|
|||
return User.objects.filter(organization=request.user.organization).distinct()
|
||||
|
||||
|
||||
class AlertGroupFilterBackend(filters.DjangoFilterBackend):
|
||||
"""
|
||||
See here for more context on how this works
|
||||
|
||||
https://github.com/carltongibson/django-filter/discussions/1572
|
||||
https://youtu.be/e52S1SjuUeM?t=841
|
||||
"""
|
||||
|
||||
def get_filterset(self, request, queryset, view):
|
||||
filterset = super().get_filterset(request, queryset, view)
|
||||
|
||||
filterset.form.fields["integration"].queryset = get_integration_queryset(request)
|
||||
filterset.form.fields["escalation_chain"].queryset = get_escalation_chain_queryset(request)
|
||||
|
||||
user_queryset = get_user_queryset(request)
|
||||
|
||||
filterset.form.fields["silenced_by"].queryset = user_queryset
|
||||
filterset.form.fields["acknowledged_by"].queryset = user_queryset
|
||||
filterset.form.fields["resolved_by"].queryset = user_queryset
|
||||
filterset.form.fields["invitees_are"].queryset = user_queryset
|
||||
filterset.form.fields["involved_users_are"].queryset = user_queryset
|
||||
|
||||
return filterset
|
||||
|
||||
|
||||
class AlertGroupFilter(DateRangeFilterMixin, ModelFieldFilterMixin, filters.FilterSet):
|
||||
"""
|
||||
Examples of possible date formats here https://docs.djangoproject.com/en/1.9/ref/settings/#datetime-input-formats
|
||||
|
|
@ -87,69 +66,55 @@ class AlertGroupFilter(DateRangeFilterMixin, ModelFieldFilterMixin, filters.Filt
|
|||
|
||||
FILTER_BY_INVOLVED_USERS_ALERT_GROUPS_CUTOFF = 1000
|
||||
|
||||
started_at_gte = filters.DateTimeFilter(field_name="started_at", lookup_expr="gte")
|
||||
started_at_lte = filters.DateTimeFilter(field_name="started_at", lookup_expr="lte")
|
||||
resolved_at_lte = filters.DateTimeFilter(field_name="resolved_at", lookup_expr="lte")
|
||||
is_root = filters.BooleanFilter(field_name="root_alert_group", lookup_expr="isnull")
|
||||
id__in = filters.BaseInFilter(field_name="public_primary_key", lookup_expr="in")
|
||||
status = filters.MultipleChoiceFilter(choices=AlertGroup.STATUS_CHOICES, method="filter_status")
|
||||
started_at = filters.CharFilter(field_name="started_at", method=DateRangeFilterMixin.filter_date_range.__name__)
|
||||
resolved_at = filters.CharFilter(field_name="resolved_at", method=DateRangeFilterMixin.filter_date_range.__name__)
|
||||
silenced_at = filters.CharFilter(field_name="silenced_at", method=DateRangeFilterMixin.filter_date_range.__name__)
|
||||
silenced_by = filters.ModelMultipleChoiceFilter(
|
||||
field_name="silenced_by_user",
|
||||
queryset=None,
|
||||
to_field_name="public_primary_key",
|
||||
method=ModelFieldFilterMixin.filter_model_field.__name__,
|
||||
started_at = filters.CharFilter(
|
||||
field_name="started_at",
|
||||
method=DateRangeFilterMixin.filter_date_range.__name__,
|
||||
)
|
||||
integration = filters.ModelMultipleChoiceFilter(
|
||||
resolved_at = filters.CharFilter(
|
||||
field_name="resolved_at",
|
||||
method=DateRangeFilterMixin.filter_date_range.__name__,
|
||||
)
|
||||
integration = MultipleChoiceCharFilter(
|
||||
field_name="channel",
|
||||
queryset=None,
|
||||
queryset=get_integration_queryset,
|
||||
to_field_name="public_primary_key",
|
||||
method=ModelFieldFilterMixin.filter_model_field.__name__,
|
||||
)
|
||||
escalation_chain = filters.ModelMultipleChoiceFilter(
|
||||
escalation_chain = MultipleChoiceCharFilter(
|
||||
field_name="channel_filter__escalation_chain",
|
||||
queryset=None,
|
||||
queryset=get_escalation_chain_queryset,
|
||||
to_field_name="public_primary_key",
|
||||
method=ModelFieldFilterMixin.filter_model_field.__name__,
|
||||
)
|
||||
started_at_range = filters.DateFromToRangeFilter(
|
||||
field_name="started_at", widget=RangeWidget(attrs={"type": "date"})
|
||||
)
|
||||
resolved_by = filters.ModelMultipleChoiceFilter(
|
||||
resolved_by = MultipleChoiceCharFilter(
|
||||
field_name="resolved_by_user",
|
||||
queryset=None,
|
||||
queryset=get_user_queryset,
|
||||
to_field_name="public_primary_key",
|
||||
method=ModelFieldFilterMixin.filter_model_field.__name__,
|
||||
)
|
||||
acknowledged_by = filters.ModelMultipleChoiceFilter(
|
||||
acknowledged_by = MultipleChoiceCharFilter(
|
||||
field_name="acknowledged_by_user",
|
||||
queryset=None,
|
||||
queryset=get_user_queryset,
|
||||
to_field_name="public_primary_key",
|
||||
method=ModelFieldFilterMixin.filter_model_field.__name__,
|
||||
)
|
||||
invitees_are = filters.ModelMultipleChoiceFilter(
|
||||
queryset=None, to_field_name="public_primary_key", method="filter_invitees_are"
|
||||
silenced_by = MultipleChoiceCharFilter(
|
||||
field_name="silenced_by_user",
|
||||
queryset=get_user_queryset,
|
||||
to_field_name="public_primary_key",
|
||||
method=ModelFieldFilterMixin.filter_model_field.__name__,
|
||||
)
|
||||
involved_users_are = filters.ModelMultipleChoiceFilter(
|
||||
queryset=None, to_field_name="public_primary_key", method="filter_by_involved_users"
|
||||
invitees_are = MultipleChoiceCharFilter(
|
||||
queryset=get_user_queryset, to_field_name="public_primary_key", method="filter_invitees_are"
|
||||
)
|
||||
involved_users_are = MultipleChoiceCharFilter(
|
||||
queryset=get_user_queryset, to_field_name="public_primary_key", method="filter_by_involved_users"
|
||||
)
|
||||
with_resolution_note = filters.BooleanFilter(method="filter_with_resolution_note")
|
||||
mine = filters.BooleanFilter(method="filter_mine")
|
||||
|
||||
class Meta:
|
||||
model = AlertGroup
|
||||
fields = [
|
||||
"id__in",
|
||||
"started_at_gte",
|
||||
"started_at_lte",
|
||||
"resolved_at_lte",
|
||||
"is_root",
|
||||
"resolved_by",
|
||||
"acknowledged_by",
|
||||
]
|
||||
|
||||
def filter_status(self, queryset, name, value):
|
||||
if not value:
|
||||
return queryset
|
||||
|
|
@ -263,12 +228,6 @@ class AlertGroupTeamFilteringMixin(TeamFilteringMixin):
|
|||
return Response(data={"error_code": "wrong_team"}, status=status.HTTP_403_FORBIDDEN)
|
||||
|
||||
|
||||
@extend_schema_view(
|
||||
list=extend_schema(description="Fetch a list of alert groups"),
|
||||
retrieve=extend_schema(description="Fetch a single alert group"),
|
||||
destroy=extend_schema(description="Delete an alert group"),
|
||||
preview_template=extend_schema(description="Preview a template for an alert group"),
|
||||
)
|
||||
class AlertGroupView(
|
||||
PreviewTemplateMixin,
|
||||
AlertGroupTeamFilteringMixin,
|
||||
|
|
@ -278,6 +237,10 @@ class AlertGroupView(
|
|||
mixins.DestroyModelMixin,
|
||||
viewsets.GenericViewSet,
|
||||
):
|
||||
"""
|
||||
Internal API endpoints for alert groups.
|
||||
"""
|
||||
|
||||
authentication_classes = (
|
||||
MobileAppAuthTokenAuthentication,
|
||||
PluginAuthentication,
|
||||
|
|
@ -307,15 +270,12 @@ class AlertGroupView(
|
|||
"escalation_snapshot": [RBACPermission.Permissions.ALERT_GROUPS_READ],
|
||||
}
|
||||
|
||||
http_method_names = ["get", "post", "delete"]
|
||||
|
||||
queryset = AlertGroup.objects.none() # needed for drf-spectacular introspection
|
||||
serializer_class = AlertGroupSerializer
|
||||
|
||||
pagination_class = AlertGroupCursorPaginator
|
||||
|
||||
filter_backends = [SearchFilter, AlertGroupFilterBackend]
|
||||
# search_fields = ["=public_primary_key", "=inside_organization_number", "web_title_cache"]
|
||||
|
||||
filter_backends = [SearchFilter, filters.DjangoFilterBackend]
|
||||
filterset_class = AlertGroupFilter
|
||||
|
||||
def get_serializer_class(self):
|
||||
|
|
@ -375,7 +335,7 @@ class AlertGroupView(
|
|||
obj = self.enrich([obj])[0]
|
||||
return obj
|
||||
|
||||
def retrieve(self, request, pk, *args, **kwargs):
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
"""Return alert group details.
|
||||
|
||||
It is worth mentioning that `render_after_resolve_report_json` property will return a list
|
||||
|
|
@ -433,7 +393,7 @@ class AlertGroupView(
|
|||
- 1: web
|
||||
|
||||
"""
|
||||
return super().retrieve(request, pk, *args, **kwargs)
|
||||
return super().retrieve(request, *args, **kwargs)
|
||||
|
||||
def enrich(self, alert_groups):
|
||||
"""
|
||||
|
|
@ -482,9 +442,12 @@ class AlertGroupView(
|
|||
delete_alert_group.apply_async((instance.pk, request.user.pk))
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@extend_schema(responses=inline_serializer(name="AlertGroupStats", fields={"count": serializers.IntegerField()}))
|
||||
@action(detail=False)
|
||||
def stats(self, *args, **kwargs):
|
||||
@extend_schema(
|
||||
filters=True, # filter alert groups before counting them
|
||||
responses=inline_serializer(name="AlertGroupStats", fields={"count": serializers.IntegerField()}),
|
||||
)
|
||||
@action(methods=["get"], detail=False)
|
||||
def stats(self, request):
|
||||
"""
|
||||
Return number of alert groups capped at 100001
|
||||
"""
|
||||
|
|
@ -492,12 +455,9 @@ class AlertGroupView(
|
|||
alert_groups = self.filter_queryset(self.get_queryset())[:MAX_COUNT]
|
||||
count = alert_groups.count()
|
||||
count = f"{MAX_COUNT-1}+" if count == MAX_COUNT else str(count)
|
||||
return Response(
|
||||
{
|
||||
"count": count,
|
||||
}
|
||||
)
|
||||
return Response({"count": count})
|
||||
|
||||
@extend_schema(responses=AlertGroupSerializer)
|
||||
@action(methods=["post"], detail=True)
|
||||
def acknowledge(self, request, pk):
|
||||
"""
|
||||
|
|
@ -512,6 +472,7 @@ class AlertGroupView(
|
|||
|
||||
return Response(AlertGroupSerializer(alert_group, context={"request": self.request}).data)
|
||||
|
||||
@extend_schema(responses=AlertGroupSerializer)
|
||||
@action(methods=["post"], detail=True)
|
||||
def unacknowledge(self, request, pk):
|
||||
"""
|
||||
|
|
@ -534,6 +495,12 @@ class AlertGroupView(
|
|||
|
||||
return Response(AlertGroupSerializer(alert_group, context={"request": self.request}).data)
|
||||
|
||||
@extend_schema(
|
||||
request=inline_serializer(
|
||||
name="AlertGroupResolve", fields={"resolution_note": serializers.CharField(required=False, allow_null=True)}
|
||||
),
|
||||
responses=AlertGroupSerializer,
|
||||
)
|
||||
@action(methods=["post"], detail=True)
|
||||
def resolve(self, request, pk):
|
||||
"""
|
||||
|
|
@ -579,6 +546,7 @@ class AlertGroupView(
|
|||
alert_group.resolve_by_user(self.request.user, action_source=ActionSource.WEB)
|
||||
return Response(AlertGroupSerializer(alert_group, context={"request": self.request}).data)
|
||||
|
||||
@extend_schema(responses=AlertGroupSerializer)
|
||||
@action(methods=["post"], detail=True)
|
||||
def unresolve(self, request, pk):
|
||||
"""
|
||||
|
|
@ -597,6 +565,10 @@ class AlertGroupView(
|
|||
alert_group.un_resolve_by_user(self.request.user, action_source=ActionSource.WEB)
|
||||
return Response(AlertGroupSerializer(alert_group, context={"request": self.request}).data)
|
||||
|
||||
@extend_schema(
|
||||
request=inline_serializer(name="AlertGroupAttach", fields={"root_alert_group_pk": serializers.CharField()}),
|
||||
responses=AlertGroupSerializer,
|
||||
)
|
||||
@action(methods=["post"], detail=True)
|
||||
def attach(self, request, pk=None):
|
||||
"""
|
||||
|
|
@ -622,6 +594,7 @@ class AlertGroupView(
|
|||
alert_group.attach_by_user(self.request.user, root_alert_group, action_source=ActionSource.WEB)
|
||||
return Response(AlertGroupSerializer(alert_group, context={"request": self.request}).data)
|
||||
|
||||
@extend_schema(responses=AlertGroupSerializer)
|
||||
@action(methods=["post"], detail=True)
|
||||
def unattach(self, request, pk=None):
|
||||
"""
|
||||
|
|
@ -636,6 +609,10 @@ class AlertGroupView(
|
|||
alert_group.un_attach_by_user(self.request.user, action_source=ActionSource.WEB)
|
||||
return Response(AlertGroupSerializer(alert_group, context={"request": self.request}).data)
|
||||
|
||||
@extend_schema(
|
||||
request=inline_serializer(name="AlertGroupSilence", fields={"delay": serializers.IntegerField()}),
|
||||
responses=AlertGroupSerializer,
|
||||
)
|
||||
@action(methods=["post"], detail=True)
|
||||
def silence(self, request, pk=None):
|
||||
"""
|
||||
|
|
@ -655,9 +632,13 @@ class AlertGroupView(
|
|||
|
||||
@extend_schema(
|
||||
responses=inline_serializer(
|
||||
name="silence_options",
|
||||
fields={"value": serializers.CharField(), "display_name": serializers.CharField()},
|
||||
many=True,
|
||||
name="AlertGroupSilenceOptions",
|
||||
fields={
|
||||
"value": serializers.ChoiceField(choices=[value for value, _ in AlertGroup.SILENCE_DELAY_OPTIONS]),
|
||||
"display_name": serializers.ChoiceField(
|
||||
choices=[display_name for _, display_name in AlertGroup.SILENCE_DELAY_OPTIONS]
|
||||
),
|
||||
},
|
||||
)
|
||||
)
|
||||
@action(methods=["get"], detail=False)
|
||||
|
|
@ -670,6 +651,7 @@ class AlertGroupView(
|
|||
]
|
||||
return Response(data)
|
||||
|
||||
@extend_schema(responses=AlertGroupSerializer)
|
||||
@action(methods=["post"], detail=True)
|
||||
def unsilence(self, request, pk=None):
|
||||
"""
|
||||
|
|
@ -693,6 +675,10 @@ class AlertGroupView(
|
|||
|
||||
return Response(AlertGroupSerializer(alert_group, context={"request": request}).data)
|
||||
|
||||
@extend_schema(
|
||||
request=inline_serializer(name="AlertGroupUnpageUser", fields={"user_id": serializers.CharField()}),
|
||||
responses=AlertGroupSerializer,
|
||||
)
|
||||
@action(methods=["post"], detail=True)
|
||||
def unpage_user(self, request, pk=None):
|
||||
"""
|
||||
|
|
@ -715,12 +701,32 @@ class AlertGroupView(
|
|||
unpage_user(alert_group=alert_group, user=user, from_user=from_user)
|
||||
return Response(status=status.HTTP_200_OK)
|
||||
|
||||
@extend_schema(
|
||||
responses=inline_serializer(
|
||||
name="AlertGroupFilters",
|
||||
fields={
|
||||
"name": serializers.CharField(),
|
||||
"type": serializers.CharField(),
|
||||
"href": serializers.CharField(required=False),
|
||||
"global": serializers.BooleanField(required=False),
|
||||
"default": serializers.JSONField(required=False),
|
||||
"description": serializers.CharField(required=False),
|
||||
"options": inline_serializer(
|
||||
name="AlertGroupFiltersOptions",
|
||||
fields={
|
||||
"value": serializers.CharField(),
|
||||
"display_name": serializers.IntegerField(),
|
||||
},
|
||||
),
|
||||
},
|
||||
many=True,
|
||||
)
|
||||
)
|
||||
@action(methods=["get"], detail=False)
|
||||
def filters(self, request):
|
||||
"""
|
||||
Retrieve a list of valid filter options that can be used to filter alert groups
|
||||
"""
|
||||
filter_name = request.query_params.get("search", None)
|
||||
api_root = "/api/internal/v1/"
|
||||
|
||||
now = timezone.now()
|
||||
|
|
@ -779,7 +785,6 @@ class AlertGroupView(
|
|||
{"display_name": "silenced", "value": AlertGroup.SILENCED},
|
||||
],
|
||||
},
|
||||
# {'name': 'is_root', 'type': 'boolean', 'default': True},
|
||||
{
|
||||
"name": "started_at",
|
||||
"type": "daterange",
|
||||
|
|
@ -812,41 +817,60 @@ class AlertGroupView(
|
|||
}
|
||||
)
|
||||
|
||||
if filter_name is not None:
|
||||
filter_options = list(filter(lambda f: filter_name in f["name"], filter_options))
|
||||
|
||||
return Response(filter_options)
|
||||
|
||||
@extend_schema(
|
||||
request=inline_serializer(
|
||||
name="AlertGroupBulkActionRequest",
|
||||
fields={
|
||||
"alert_group_pks": serializers.ListField(child=serializers.CharField()),
|
||||
"action": serializers.ChoiceField(choices=AlertGroup.BULK_ACTIONS),
|
||||
"delay": serializers.IntegerField(
|
||||
required=False, allow_null=True, help_text="only applicable for silence"
|
||||
),
|
||||
},
|
||||
)
|
||||
)
|
||||
@action(methods=["post"], detail=False)
|
||||
def bulk_action(self, request):
|
||||
"""
|
||||
Perform a bulk action on a list of alert groups
|
||||
"""
|
||||
alert_group_public_pks = self.request.data.get("alert_group_pks", [])
|
||||
action_with_incidents = self.request.data.get("action", None)
|
||||
alert_group_pks = self.request.data.get("alert_group_pks", [])
|
||||
action_name = self.request.data.get("action", None)
|
||||
delay = self.request.data.get("delay")
|
||||
kwargs = {}
|
||||
|
||||
if action_with_incidents not in AlertGroup.BULK_ACTIONS:
|
||||
if action_name not in AlertGroup.BULK_ACTIONS:
|
||||
return Response("Unknown action", status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
if action_with_incidents == AlertGroup.SILENCE:
|
||||
if action_name == AlertGroup.SILENCE:
|
||||
if delay is None:
|
||||
raise BadRequest(detail="Please specify a delay for silence")
|
||||
kwargs["silence_delay"] = delay
|
||||
|
||||
alert_groups = AlertGroup.objects.filter(
|
||||
channel__organization=self.request.auth.organization, public_primary_key__in=alert_group_public_pks
|
||||
channel__organization=self.request.auth.organization, public_primary_key__in=alert_group_pks
|
||||
)
|
||||
|
||||
kwargs["user"] = self.request.user
|
||||
kwargs["alert_groups"] = alert_groups
|
||||
|
||||
method = getattr(AlertGroup, f"bulk_{action_with_incidents}")
|
||||
method = getattr(AlertGroup, f"bulk_{action_name}")
|
||||
method(**kwargs)
|
||||
|
||||
return Response(status=status.HTTP_200_OK)
|
||||
|
||||
@extend_schema(
|
||||
responses=inline_serializer(
|
||||
name="AlertGroupBulkActionOptions",
|
||||
fields={
|
||||
"value": serializers.ChoiceField(choices=AlertGroup.BULK_ACTIONS),
|
||||
"display_name": serializers.ChoiceField(choices=AlertGroup.BULK_ACTIONS),
|
||||
},
|
||||
many=True,
|
||||
)
|
||||
)
|
||||
@action(methods=["get"], detail=False)
|
||||
def bulk_action_options(self, request):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -1,7 +1,11 @@
|
|||
import typing
|
||||
|
||||
from django.db.models import Q
|
||||
from django_filters import rest_framework as filters
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from rest_framework import status
|
||||
from drf_spectacular.plumbing import resolve_type_hint
|
||||
from drf_spectacular.utils import PolymorphicProxySerializer, extend_schema, extend_schema_view, inline_serializer
|
||||
from rest_framework import serializers, status
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.filters import SearchFilter
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
|
|
@ -39,6 +43,14 @@ from common.exceptions import MaintenanceCouldNotBeStartedError, TeamCanNotBeCha
|
|||
from common.insight_log import EntityEvent, write_resource_insight_log
|
||||
|
||||
|
||||
class AlertReceiveChannelCounter(typing.TypedDict):
|
||||
alerts_count: int
|
||||
alert_groups_count: int
|
||||
|
||||
|
||||
AlertReceiveChannelCounters = dict[str, AlertReceiveChannelCounter]
|
||||
|
||||
|
||||
class AlertReceiveChannelFilter(ByTeamModelFieldFilterMixin, filters.FilterSet):
|
||||
maintenance_mode = filters.MultipleChoiceFilter(
|
||||
choices=AlertReceiveChannel.MAINTENANCE_MODE_CHOICES, method="filter_maintenance_mode"
|
||||
|
|
@ -71,6 +83,17 @@ class AlertReceiveChannelFilter(ByTeamModelFieldFilterMixin, filters.FilterSet):
|
|||
return queryset
|
||||
|
||||
|
||||
@extend_schema_view(
|
||||
list=extend_schema(
|
||||
responses=PolymorphicProxySerializer(
|
||||
component_name="AlertReceiveChannelPolymorphic",
|
||||
serializers=[AlertReceiveChannelSerializer, FilterAlertReceiveChannelSerializer],
|
||||
resource_type_field_name=None,
|
||||
)
|
||||
),
|
||||
update=extend_schema(responses=AlertReceiveChannelUpdateSerializer),
|
||||
partial_update=extend_schema(responses=AlertReceiveChannelUpdateSerializer),
|
||||
)
|
||||
class AlertReceiveChannelView(
|
||||
PreviewTemplateMixin,
|
||||
TeamFilteringMixin,
|
||||
|
|
@ -79,6 +102,10 @@ class AlertReceiveChannelView(
|
|||
UpdateSerializerMixin,
|
||||
ModelViewSet,
|
||||
):
|
||||
"""
|
||||
Internal API endpoints for alert receive channels (integrations).
|
||||
"""
|
||||
|
||||
authentication_classes = (
|
||||
MobileAppAuthTokenAuthentication,
|
||||
PluginAuthentication,
|
||||
|
|
@ -86,6 +113,7 @@ class AlertReceiveChannelView(
|
|||
permission_classes = (IsAuthenticated, RBACPermission)
|
||||
|
||||
model = AlertReceiveChannel
|
||||
queryset = AlertReceiveChannel.objects.none() # needed for drf-spectacular introspection
|
||||
serializer_class = AlertReceiveChannelSerializer
|
||||
filter_serializer_class = FilterAlertReceiveChannelSerializer
|
||||
update_serializer_class = AlertReceiveChannelUpdateSerializer
|
||||
|
|
@ -194,6 +222,14 @@ class AlertReceiveChannelView(
|
|||
schedule_update_label_cache(self.model.__name__, self.request.auth.organization, ids)
|
||||
return page
|
||||
|
||||
@extend_schema(
|
||||
request=inline_serializer(
|
||||
name="AlertReceiveChannelSendDemoAlert",
|
||||
fields={
|
||||
"demo_alert_payload": serializers.DictField(required=False, allow_null=True),
|
||||
},
|
||||
),
|
||||
)
|
||||
@action(detail=True, methods=["post"], throttle_classes=[DemoAlertThrottler])
|
||||
def send_demo_alert(self, request, pk):
|
||||
instance = self.get_object()
|
||||
|
|
@ -209,6 +245,19 @@ class AlertReceiveChannelView(
|
|||
|
||||
return Response(status=status.HTTP_200_OK)
|
||||
|
||||
@extend_schema(
|
||||
responses=inline_serializer(
|
||||
name="AlertReceiveChannelIntegrationOptions",
|
||||
fields={
|
||||
"value": serializers.CharField(),
|
||||
"display_name": serializers.CharField(),
|
||||
"short_description": serializers.CharField(),
|
||||
"featured": serializers.BooleanField(),
|
||||
"featured_tag_name": serializers.CharField(allow_null=True),
|
||||
},
|
||||
many=True,
|
||||
)
|
||||
)
|
||||
@action(detail=False, methods=["get"])
|
||||
def integration_options(self, request):
|
||||
choices = []
|
||||
|
|
@ -231,6 +280,11 @@ class AlertReceiveChannelView(
|
|||
choices.append(choice)
|
||||
return Response(featured_choices + choices)
|
||||
|
||||
@extend_schema(
|
||||
parameters=[
|
||||
inline_serializer(name="AlertReceiveChannelChangeTeam", fields={"team_id": serializers.CharField()})
|
||||
]
|
||||
)
|
||||
@action(detail=True, methods=["put"])
|
||||
def change_team(self, request, pk):
|
||||
instance = self.get_object()
|
||||
|
|
@ -249,6 +303,7 @@ class AlertReceiveChannelView(
|
|||
|
||||
return Response()
|
||||
|
||||
@extend_schema(responses={status.HTTP_200_OK: resolve_type_hint(AlertReceiveChannelCounters)})
|
||||
@action(methods=["get"], detail=False)
|
||||
def counters(self, request):
|
||||
queryset = self.filter_queryset(self.get_queryset(eager=False))
|
||||
|
|
@ -260,6 +315,11 @@ class AlertReceiveChannelView(
|
|||
}
|
||||
return Response(response)
|
||||
|
||||
@extend_schema(
|
||||
# make operation_id unique, otherwise drf-spectacular will issue a warning
|
||||
operation_id="alert_receive_channels_counters_per_integration_retrieve",
|
||||
responses={status.HTTP_200_OK: resolve_type_hint(AlertReceiveChannelCounters)},
|
||||
)
|
||||
@action(methods=["get"], detail=True, url_path="counters")
|
||||
def counters_per_integration(self, request, pk):
|
||||
alert_receive_channel = self.get_object()
|
||||
|
|
@ -287,10 +347,22 @@ class AlertReceiveChannelView(
|
|||
except AttributeError:
|
||||
return None
|
||||
|
||||
@extend_schema(
|
||||
responses=inline_serializer(
|
||||
name="AlertReceiveChannelFilters",
|
||||
fields={
|
||||
"name": serializers.CharField(),
|
||||
"display_name": serializers.CharField(required=False),
|
||||
"type": serializers.CharField(),
|
||||
"href": serializers.CharField(),
|
||||
"global": serializers.BooleanField(required=False),
|
||||
},
|
||||
many=True,
|
||||
)
|
||||
)
|
||||
@action(methods=["get"], detail=False)
|
||||
def filters(self, request):
|
||||
organization = self.request.auth.organization
|
||||
filter_name = request.query_params.get("search", None)
|
||||
api_root = "/api/internal/v1/"
|
||||
|
||||
filter_options = [
|
||||
|
|
@ -317,11 +389,19 @@ class AlertReceiveChannelView(
|
|||
}
|
||||
)
|
||||
|
||||
if filter_name is not None:
|
||||
filter_options = list(filter(lambda f: filter_name in f["name"], filter_options))
|
||||
|
||||
return Response(filter_options)
|
||||
|
||||
@extend_schema(
|
||||
request=inline_serializer(
|
||||
name="AlertReceiveChannelStartMaintenance",
|
||||
fields={
|
||||
"mode": serializers.ChoiceField(choices=MaintainableObject.MAINTENANCE_MODE_CHOICES),
|
||||
"duration": serializers.ChoiceField(
|
||||
choices=MaintainableObject.maintenance_duration_options_in_seconds()
|
||||
),
|
||||
},
|
||||
),
|
||||
)
|
||||
@action(detail=True, methods=["post"])
|
||||
def start_maintenance(self, request, pk):
|
||||
instance = self.get_object()
|
||||
|
|
@ -355,8 +435,7 @@ class AlertReceiveChannelView(
|
|||
@action(detail=True, methods=["post"])
|
||||
def stop_maintenance(self, request, pk):
|
||||
instance = self.get_object()
|
||||
user = request.user
|
||||
instance.force_disable_maintenance(user)
|
||||
instance.force_disable_maintenance(request.user)
|
||||
return Response(status=status.HTTP_200_OK)
|
||||
|
||||
@action(detail=True, methods=["post"])
|
||||
|
|
@ -394,6 +473,20 @@ class AlertReceiveChannelView(
|
|||
instance.save()
|
||||
return Response(status=status.HTTP_200_OK)
|
||||
|
||||
@extend_schema(
|
||||
parameters=[
|
||||
inline_serializer(
|
||||
name="AlertReceiveChannelValidateName",
|
||||
fields={
|
||||
"verbal_name": serializers.CharField(),
|
||||
},
|
||||
)
|
||||
],
|
||||
responses={
|
||||
status.HTTP_200_OK: None,
|
||||
status.HTTP_409_CONFLICT: None,
|
||||
},
|
||||
)
|
||||
@action(detail=False, methods=["get"])
|
||||
def validate_name(self, request):
|
||||
"""
|
||||
|
|
@ -412,6 +505,17 @@ class AlertReceiveChannelView(
|
|||
|
||||
return r
|
||||
|
||||
@extend_schema(
|
||||
responses=inline_serializer(
|
||||
name="AlertReceiveChannelConnectedContactPoints",
|
||||
fields={
|
||||
"uid": serializers.CharField(),
|
||||
"name": serializers.CharField(),
|
||||
"contact_points": serializers.ListField(child=serializers.CharField()),
|
||||
},
|
||||
many=True,
|
||||
)
|
||||
)
|
||||
@action(detail=True, methods=["get"])
|
||||
def connected_contact_points(self, request, pk):
|
||||
instance = self.get_object()
|
||||
|
|
@ -420,12 +524,32 @@ class AlertReceiveChannelView(
|
|||
contact_points = instance.grafana_alerting_sync_manager.get_connected_contact_points()
|
||||
return Response(contact_points)
|
||||
|
||||
@extend_schema(
|
||||
responses=inline_serializer(
|
||||
name="AlertReceiveChannelContactPoints",
|
||||
fields={
|
||||
"uid": serializers.CharField(),
|
||||
"name": serializers.CharField(),
|
||||
"contact_points": serializers.ListField(child=serializers.CharField()),
|
||||
},
|
||||
many=True,
|
||||
)
|
||||
)
|
||||
@action(detail=False, methods=["get"])
|
||||
def contact_points(self, request):
|
||||
organization = request.auth.organization
|
||||
contact_points = GrafanaAlertingSyncManager.get_contact_points(organization)
|
||||
return Response(contact_points)
|
||||
|
||||
@extend_schema(
|
||||
request=inline_serializer(
|
||||
name="AlertReceiveChannelConnectContactPoint",
|
||||
fields={
|
||||
"datasource_uid": serializers.CharField(),
|
||||
"contact_point_name": serializers.CharField(),
|
||||
},
|
||||
),
|
||||
)
|
||||
@action(detail=True, methods=["post"])
|
||||
def connect_contact_point(self, request, pk):
|
||||
instance = self.get_object()
|
||||
|
|
@ -443,6 +567,15 @@ class AlertReceiveChannelView(
|
|||
raise BadRequest(detail=error)
|
||||
return Response(status=status.HTTP_200_OK)
|
||||
|
||||
@extend_schema(
|
||||
request=inline_serializer(
|
||||
name="AlertReceiveChannelCreateContactPoint",
|
||||
fields={
|
||||
"datasource_uid": serializers.CharField(),
|
||||
"contact_point_name": serializers.CharField(),
|
||||
},
|
||||
),
|
||||
)
|
||||
@action(detail=True, methods=["post"])
|
||||
def create_contact_point(self, request, pk):
|
||||
instance = self.get_object()
|
||||
|
|
@ -460,6 +593,15 @@ class AlertReceiveChannelView(
|
|||
raise BadRequest(detail=error)
|
||||
return Response(status=status.HTTP_201_CREATED)
|
||||
|
||||
@extend_schema(
|
||||
request=inline_serializer(
|
||||
name="AlertReceiveChannelDisconnectContactPoint",
|
||||
fields={
|
||||
"datasource_uid": serializers.CharField(),
|
||||
"contact_point_name": serializers.CharField(),
|
||||
},
|
||||
),
|
||||
)
|
||||
@action(detail=True, methods=["post"])
|
||||
def disconnect_contact_point(self, request, pk):
|
||||
instance = self.get_object()
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
import enum
|
||||
|
||||
from django.conf import settings
|
||||
from drf_spectacular.utils import OpenApiExample, extend_schema
|
||||
from rest_framework import serializers
|
||||
from drf_spectacular.plumbing import resolve_type_hint
|
||||
from drf_spectacular.utils import extend_schema
|
||||
from rest_framework import status
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
|
|
@ -8,14 +11,16 @@ from apps.auth_token.auth import PluginAuthentication
|
|||
from apps.base.utils import live_settings
|
||||
from apps.labels.utils import is_labels_feature_enabled
|
||||
|
||||
FEATURE_MSTEAMS = "msteams"
|
||||
FEATURE_SLACK = "slack"
|
||||
FEATURE_TELEGRAM = "telegram"
|
||||
FEATURE_LIVE_SETTINGS = "live_settings"
|
||||
FEATURE_GRAFANA_CLOUD_NOTIFICATIONS = "grafana_cloud_notifications"
|
||||
FEATURE_GRAFANA_CLOUD_CONNECTION = "grafana_cloud_connection"
|
||||
FEATURE_GRAFANA_ALERTING_V2 = "grafana_alerting_v2"
|
||||
FEATURE_LABELS = "labels"
|
||||
|
||||
class Feature(enum.StrEnum):
|
||||
MSTEAMS = "msteams"
|
||||
SLACK = "slack"
|
||||
TELEGRAM = "telegram"
|
||||
LIVE_SETTINGS = "live_settings"
|
||||
GRAFANA_CLOUD_NOTIFICATIONS = "grafana_cloud_notifications"
|
||||
GRAFANA_CLOUD_CONNECTION = "grafana_cloud_connection"
|
||||
GRAFANA_ALERTING_V2 = "grafana_alerting_v2"
|
||||
LABELS = "labels"
|
||||
|
||||
|
||||
class FeaturesAPIView(APIView):
|
||||
|
|
@ -26,16 +31,7 @@ class FeaturesAPIView(APIView):
|
|||
|
||||
authentication_classes = (PluginAuthentication,)
|
||||
|
||||
@extend_schema(
|
||||
request=None,
|
||||
responses=serializers.ListField(child=serializers.CharField()),
|
||||
examples=[
|
||||
OpenApiExample(
|
||||
name="Example response",
|
||||
value=["slack", "telegram", "grafana_cloud_connection", "live_settings", "grafana_cloud_notifications"],
|
||||
)
|
||||
],
|
||||
)
|
||||
@extend_schema(responses={status.HTTP_200_OK: resolve_type_hint(list[Feature])})
|
||||
def get(self, request):
|
||||
data = self._get_enabled_features(request)
|
||||
return Response(data)
|
||||
|
|
@ -44,25 +40,25 @@ class FeaturesAPIView(APIView):
|
|||
enabled_features = []
|
||||
|
||||
if settings.FEATURE_SLACK_INTEGRATION_ENABLED:
|
||||
enabled_features.append(FEATURE_SLACK)
|
||||
enabled_features.append(Feature.SLACK)
|
||||
|
||||
if settings.FEATURE_TELEGRAM_INTEGRATION_ENABLED:
|
||||
enabled_features.append(FEATURE_TELEGRAM)
|
||||
enabled_features.append(Feature.TELEGRAM)
|
||||
|
||||
if settings.IS_OPEN_SOURCE:
|
||||
# Features below should be enabled only in OSS
|
||||
enabled_features.append(FEATURE_GRAFANA_CLOUD_CONNECTION)
|
||||
enabled_features.append(Feature.GRAFANA_CLOUD_CONNECTION)
|
||||
if settings.FEATURE_LIVE_SETTINGS_ENABLED:
|
||||
enabled_features.append(FEATURE_LIVE_SETTINGS)
|
||||
enabled_features.append(Feature.LIVE_SETTINGS)
|
||||
if live_settings.GRAFANA_CLOUD_NOTIFICATIONS_ENABLED:
|
||||
enabled_features.append(FEATURE_GRAFANA_CLOUD_NOTIFICATIONS)
|
||||
enabled_features.append(Feature.GRAFANA_CLOUD_NOTIFICATIONS)
|
||||
else:
|
||||
enabled_features.append(FEATURE_MSTEAMS)
|
||||
enabled_features.append(Feature.MSTEAMS)
|
||||
|
||||
if settings.FEATURE_GRAFANA_ALERTING_V2_ENABLED:
|
||||
enabled_features.append(FEATURE_GRAFANA_ALERTING_V2)
|
||||
enabled_features.append(Feature.GRAFANA_ALERTING_V2)
|
||||
|
||||
if is_labels_feature_enabled(self.request.auth.organization):
|
||||
enabled_features.append(FEATURE_LABELS)
|
||||
enabled_features.append(Feature.LABELS)
|
||||
|
||||
return enabled_features
|
||||
|
|
|
|||
|
|
@ -145,6 +145,8 @@ class LabelsViewSet(LabelsFeatureFlagViewSet):
|
|||
return super().handle_exception(exc)
|
||||
|
||||
|
||||
# specifying a tag explicitly to avoid these endpoints being grouped with alert group endpoints
|
||||
@extend_schema(tags=["alert group labels"])
|
||||
class AlertGroupLabelsViewSet(LabelsFeatureFlagViewSet):
|
||||
"""
|
||||
This viewset is similar to LabelsViewSet, but it works with alert group labels.
|
||||
|
|
|
|||
|
|
@ -137,8 +137,14 @@ class ScheduleView(
|
|||
The result of this method is cached and is reused for the whole lifetime of a request,
|
||||
since self.get_serializer_context() is called multiple times for every instance in the queryset.
|
||||
"""
|
||||
current_page_schedules = self.paginate_queryset(self.filter_queryset(self.get_queryset(annotate=False)))
|
||||
return get_oncall_users_for_multiple_schedules(current_page_schedules)
|
||||
current_schedules = self.get_queryset(annotate=False).none()
|
||||
if self.action == "list":
|
||||
# listing page, only get oncall users for current page schedules
|
||||
current_schedules = self.paginate_queryset(self.filter_queryset(self.get_queryset(annotate=False)))
|
||||
elif self.kwargs.get("pk"):
|
||||
# if this is a particular schedule detail, only consider it as current
|
||||
current_schedules = [self.get_object(annotate=False)]
|
||||
return get_oncall_users_for_multiple_schedules(current_schedules)
|
||||
|
||||
def get_serializer_context(self):
|
||||
context = super().get_serializer_context()
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import logging
|
||||
import typing
|
||||
|
||||
import pytz
|
||||
from django.conf import settings
|
||||
|
|
@ -8,7 +9,9 @@ from django.urls import reverse
|
|||
from django.utils import timezone
|
||||
from django.utils.functional import cached_property
|
||||
from django_filters import rest_framework as filters
|
||||
from rest_framework import mixins, status, viewsets
|
||||
from drf_spectacular.plumbing import resolve_type_hint
|
||||
from drf_spectacular.utils import PolymorphicProxySerializer, extend_schema, inline_serializer
|
||||
from rest_framework import mixins, serializers, status, viewsets
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.exceptions import NotFound
|
||||
from rest_framework.filters import SearchFilter
|
||||
|
|
@ -60,11 +63,13 @@ from apps.phone_notifications.exceptions import (
|
|||
from apps.phone_notifications.phone_backend import PhoneBackend
|
||||
from apps.schedules.ical_utils import get_cached_oncall_users_for_multiple_schedules
|
||||
from apps.schedules.models import OnCallSchedule
|
||||
from apps.schedules.models.on_call_schedule import ScheduleEvent
|
||||
from apps.telegram.client import TelegramClient
|
||||
from apps.telegram.models import TelegramVerificationCode
|
||||
from apps.user_management.models import Team, User
|
||||
from common.api_helpers.exceptions import Conflict
|
||||
from common.api_helpers.mixins import FilterSerializerMixin, PublicPrimaryKeyMixin
|
||||
from common.api_helpers.filters import ByTeamModelFieldFilterMixin, TeamModelMultipleChoiceFilter
|
||||
from common.api_helpers.mixins import PublicPrimaryKeyMixin
|
||||
from common.api_helpers.paginators import HundredPageSizePaginator
|
||||
from common.api_helpers.utils import create_engine_url
|
||||
from common.insight_log import (
|
||||
|
|
@ -85,6 +90,17 @@ UPCOMING_SHIFTS_DEFAULT_DAYS = 7
|
|||
UPCOMING_SHIFTS_MAX_DAYS = 65
|
||||
|
||||
|
||||
class UpcomingShift(typing.TypedDict):
|
||||
schedule_id: str
|
||||
schedule_name: str
|
||||
is_oncall: bool
|
||||
current_shift: ScheduleEvent | None
|
||||
next_shift: ScheduleEvent | None
|
||||
|
||||
|
||||
UpcomingShifts = list[UpcomingShift]
|
||||
|
||||
|
||||
class CurrentUserView(APIView):
|
||||
authentication_classes = (MobileAppAuthTokenAuthentication, PluginAuthentication)
|
||||
permission_classes = (IsAuthenticated,)
|
||||
|
|
@ -113,7 +129,7 @@ class CurrentUserView(APIView):
|
|||
return Response(serializer.data)
|
||||
|
||||
|
||||
class UserFilter(filters.FilterSet):
|
||||
class UserFilter(ByTeamModelFieldFilterMixin, filters.FilterSet):
|
||||
"""
|
||||
https://django-filter.readthedocs.io/en/master/guide/rest_framework.html
|
||||
"""
|
||||
|
|
@ -122,6 +138,7 @@ class UserFilter(filters.FilterSet):
|
|||
# TODO: remove "roles" in next version
|
||||
roles = filters.MultipleChoiceFilter(field_name="role", choices=LegacyAccessControlRole.choices())
|
||||
permission = filters.ChoiceFilter(method="filter_by_permission", choices=ALL_PERMISSION_CHOICES)
|
||||
team = TeamModelMultipleChoiceFilter(field_name="teams", null_label=None, null_value=None)
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
|
|
@ -141,12 +158,15 @@ class UserFilter(filters.FilterSet):
|
|||
|
||||
class UserView(
|
||||
PublicPrimaryKeyMixin,
|
||||
FilterSerializerMixin,
|
||||
mixins.RetrieveModelMixin,
|
||||
mixins.UpdateModelMixin,
|
||||
mixins.ListModelMixin,
|
||||
viewsets.GenericViewSet,
|
||||
):
|
||||
"""
|
||||
Internal API endpoints for users.
|
||||
"""
|
||||
|
||||
authentication_classes = (
|
||||
MobileAppAuthTokenAuthentication,
|
||||
PluginAuthentication,
|
||||
|
|
@ -206,7 +226,7 @@ class UserView(
|
|||
],
|
||||
}
|
||||
|
||||
filter_serializer_class = FilterUserSerializer
|
||||
queryset = User.objects.none() # needed for drf-spectacular introspection
|
||||
|
||||
pagination_class = HundredPageSizePaginator
|
||||
|
||||
|
|
@ -262,7 +282,7 @@ class UserView(
|
|||
is_filters_request = query_params.get("filters", "false") == "true"
|
||||
|
||||
if is_list_request and is_filters_request:
|
||||
return self.get_filter_serializer_class()
|
||||
return FilterUserSerializer
|
||||
elif is_list_request and self._is_currently_oncall_request():
|
||||
return UserIsCurrentlyOnCallSerializer
|
||||
|
||||
|
|
@ -285,6 +305,13 @@ class UserView(
|
|||
|
||||
return queryset.order_by("id")
|
||||
|
||||
@extend_schema(
|
||||
responses=PolymorphicProxySerializer(
|
||||
component_name="UserPolymorphic",
|
||||
serializers=[FilterUserSerializer, UserIsCurrentlyOnCallSerializer, UserSerializer],
|
||||
resource_type_field_name=None,
|
||||
)
|
||||
)
|
||||
def list(self, request, *args, **kwargs) -> Response:
|
||||
queryset = self.filter_queryset(self.get_queryset())
|
||||
|
||||
|
|
@ -322,6 +349,7 @@ class UserView(
|
|||
serializer = self.get_serializer(queryset, many=True, context=context)
|
||||
return Response(serializer.data)
|
||||
|
||||
@extend_schema(responses=UserSerializer)
|
||||
def retrieve(self, request, *args, **kwargs) -> Response:
|
||||
context = self.get_serializer_context()
|
||||
|
||||
|
|
@ -343,6 +371,14 @@ class UserView(
|
|||
serializer = self.get_serializer(instance, context=context)
|
||||
return Response(serializer.data)
|
||||
|
||||
@extend_schema(request=UserSerializer, responses=UserSerializer)
|
||||
def update(self, request, *args, **kwargs):
|
||||
return super().update(request, *args, **kwargs)
|
||||
|
||||
@extend_schema(request=UserSerializer, responses=UserSerializer)
|
||||
def partial_update(self, request, *args, **kwargs):
|
||||
return super().partial_update(request, *args, **kwargs)
|
||||
|
||||
def wrong_team_response(self) -> Response:
|
||||
"""
|
||||
This method returns 403 and {"error_code": "wrong_team", "owner_team": {"name", "id", "email", "avatar_url"}}.
|
||||
|
|
@ -369,6 +405,7 @@ class UserView(
|
|||
serializer = UserSerializer(self.get_queryset().get(pk=self.request.user.pk))
|
||||
return Response(serializer.data)
|
||||
|
||||
@extend_schema(responses={status.HTTP_200_OK: resolve_type_hint(typing.List[str])})
|
||||
@action(detail=False, methods=["get"])
|
||||
def timezone_options(self, request) -> Response:
|
||||
return Response(pytz.common_timezones)
|
||||
|
|
@ -427,6 +464,7 @@ class UserView(
|
|||
)
|
||||
return Response(status=status.HTTP_200_OK)
|
||||
|
||||
@extend_schema(parameters=[inline_serializer(name="UserVerifyNumber", fields={"token": serializers.CharField()})])
|
||||
@action(
|
||||
detail=True,
|
||||
methods=["put"],
|
||||
|
|
@ -506,6 +544,13 @@ class UserView(
|
|||
|
||||
return Response(status=status.HTTP_200_OK)
|
||||
|
||||
@extend_schema(
|
||||
parameters=[
|
||||
inline_serializer(
|
||||
name="UserSendTestPush", fields={"critical": serializers.BooleanField(required=False, default=False)}
|
||||
)
|
||||
]
|
||||
)
|
||||
@action(detail=True, methods=["post"], throttle_classes=[TestPushThrottler])
|
||||
def send_test_push(self, request, pk) -> Response:
|
||||
user = self.get_object()
|
||||
|
|
@ -525,6 +570,11 @@ class UserView(
|
|||
)
|
||||
return Response(status=status.HTTP_200_OK)
|
||||
|
||||
@extend_schema(
|
||||
parameters=[
|
||||
inline_serializer(name="UserGetBackendVerificationCode", fields={"backend": serializers.CharField()})
|
||||
]
|
||||
)
|
||||
@action(detail=True, methods=["get"])
|
||||
def get_backend_verification_code(self, request, pk) -> Response:
|
||||
user = self.get_object()
|
||||
|
|
@ -537,6 +587,15 @@ class UserView(
|
|||
code = backend.generate_user_verification_code(user)
|
||||
return Response(code)
|
||||
|
||||
@extend_schema(
|
||||
responses=inline_serializer(
|
||||
name="UserGetTelegramVerificationCode",
|
||||
fields={
|
||||
"telegram_code": serializers.CharField(),
|
||||
"bot_link": serializers.CharField(),
|
||||
},
|
||||
)
|
||||
)
|
||||
@action(detail=True, methods=["get"])
|
||||
def get_telegram_verification_code(self, request, pk) -> Response:
|
||||
user = self.get_object()
|
||||
|
|
@ -594,6 +653,9 @@ class UserView(
|
|||
return Response(status=status.HTTP_400_BAD_REQUEST)
|
||||
return Response(status=status.HTTP_200_OK)
|
||||
|
||||
@extend_schema(
|
||||
parameters=[inline_serializer(name="UserUnlinkBackend", fields={"backend": serializers.CharField()})]
|
||||
)
|
||||
@action(detail=True, methods=["post"])
|
||||
def unlink_backend(self, request, pk) -> Response:
|
||||
# TODO: insight logs support
|
||||
|
|
@ -617,6 +679,15 @@ class UserView(
|
|||
return Response(status=status.HTTP_400_BAD_REQUEST)
|
||||
return Response(status=status.HTTP_200_OK)
|
||||
|
||||
@extend_schema(
|
||||
parameters=[
|
||||
inline_serializer(
|
||||
name="UserUpcomingShiftsParams",
|
||||
fields={"days": serializers.IntegerField(required=False, default=UPCOMING_SHIFTS_DEFAULT_DAYS)},
|
||||
)
|
||||
],
|
||||
responses={status.HTTP_200_OK: resolve_type_hint(UpcomingShifts)},
|
||||
)
|
||||
@action(detail=True, methods=["get"])
|
||||
def upcoming_shifts(self, request, pk) -> Response:
|
||||
user = self.get_object()
|
||||
|
|
@ -656,6 +727,28 @@ class UserView(
|
|||
|
||||
return Response(upcoming, status=status.HTTP_200_OK)
|
||||
|
||||
@extend_schema(
|
||||
methods=["get"],
|
||||
responses=inline_serializer(
|
||||
name="UserExportTokenGetResponse",
|
||||
fields={
|
||||
"created_at": serializers.DateTimeField(),
|
||||
"revoked_at": serializers.DateTimeField(allow_null=True),
|
||||
"active": serializers.BooleanField(),
|
||||
},
|
||||
),
|
||||
)
|
||||
@extend_schema(
|
||||
methods=["post"],
|
||||
responses=inline_serializer(
|
||||
name="UserExportTokenPostResponse",
|
||||
fields={
|
||||
"token": serializers.CharField(),
|
||||
"created_at": serializers.DateTimeField(),
|
||||
"export_url": serializers.CharField(),
|
||||
},
|
||||
),
|
||||
)
|
||||
@action(detail=True, methods=["get", "post", "delete"])
|
||||
def export_token(self, request, pk) -> Response:
|
||||
user = self.get_object()
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ from typing import Tuple
|
|||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
from drf_spectacular.extensions import OpenApiAuthenticationExtension
|
||||
from rest_framework import exceptions
|
||||
from rest_framework.authentication import BaseAuthentication, get_authorization_header
|
||||
from rest_framework.request import Request
|
||||
|
|
@ -142,6 +143,22 @@ class PluginAuthentication(BasePluginAuthentication):
|
|||
raise exceptions.AuthenticationFailed("Non-existent or anonymous user.")
|
||||
|
||||
|
||||
class PluginAuthenticationSchema(OpenApiAuthenticationExtension):
|
||||
target_class = PluginAuthentication
|
||||
name = "PluginAuthentication"
|
||||
|
||||
def get_security_definition(self, auto_schema):
|
||||
return {
|
||||
"type": "apiKey",
|
||||
"in": "header",
|
||||
"name": "Authorization",
|
||||
"description": (
|
||||
"Additional X-Instance-Context and X-Grafana-Context headers must be set. "
|
||||
"THIS WILL NOT WORK IN SWAGGER UI."
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
class GrafanaIncidentUser(AnonymousUser):
|
||||
@property
|
||||
def is_authenticated(self):
|
||||
|
|
|
|||
|
|
@ -95,7 +95,10 @@ class IntegrationHeartBeat(models.Model):
|
|||
return not self.is_expired
|
||||
|
||||
@property
|
||||
def link(self) -> str:
|
||||
def link(self) -> str | None:
|
||||
if not self.alert_receive_channel.integration_url:
|
||||
return None
|
||||
|
||||
return urljoin(self.alert_receive_channel.integration_url, "heartbeat/")
|
||||
|
||||
# Insight logs
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
from typing import Optional, Tuple
|
||||
|
||||
from drf_spectacular.extensions import OpenApiAuthenticationExtension
|
||||
from rest_framework import exceptions
|
||||
from rest_framework.authentication import BaseAuthentication, get_authorization_header
|
||||
|
||||
|
|
@ -43,3 +44,15 @@ class MobileAppAuthTokenAuthentication(BaseAuthentication):
|
|||
return None, None
|
||||
|
||||
return auth_token.user, auth_token
|
||||
|
||||
|
||||
class MobileAppAuthTokenAuthenticationSchema(OpenApiAuthenticationExtension):
|
||||
target_class = MobileAppAuthTokenAuthentication
|
||||
name = "MobileAppAuthTokenAuthentication"
|
||||
|
||||
def get_security_definition(self, auto_schema):
|
||||
return {
|
||||
"type": "apiKey",
|
||||
"in": "header",
|
||||
"name": "Authorization",
|
||||
}
|
||||
|
|
|
|||
|
|
@ -92,6 +92,46 @@ def test_notify_user_about_new_alert_group_no_device_connected(
|
|||
)
|
||||
|
||||
|
||||
@patch("apps.mobile_app.utils.send_message_to_fcm_device", return_value=False)
|
||||
@pytest.mark.django_db
|
||||
def test_notify_user_about_new_alert_group_error_log_if_not_succeeded(
|
||||
mock_send_message_to_fcm_device,
|
||||
settings,
|
||||
make_organization_and_user,
|
||||
make_user_notification_policy,
|
||||
make_alert_receive_channel,
|
||||
make_channel_filter,
|
||||
make_alert_group,
|
||||
make_alert,
|
||||
):
|
||||
# create a user and connect a mobile device
|
||||
organization, user = make_organization_and_user()
|
||||
FCMDevice.objects.create(user=user, registration_id="test_device_id")
|
||||
# set up notification policy and alert group
|
||||
notification_policy = make_user_notification_policy(
|
||||
user,
|
||||
UserNotificationPolicy.Step.NOTIFY,
|
||||
notify_by=MOBILE_APP_BACKEND_ID,
|
||||
)
|
||||
alert_receive_channel = make_alert_receive_channel(organization=organization)
|
||||
channel_filter = make_channel_filter(alert_receive_channel)
|
||||
alert_group = make_alert_group(alert_receive_channel, channel_filter=channel_filter)
|
||||
make_alert(alert_group=alert_group, raw_request_data={})
|
||||
# check FCM is contacted directly when using the cloud license
|
||||
settings.LICENSE = CLOUD_LICENSE_NAME
|
||||
settings.IS_OPEN_SOURCE = False
|
||||
|
||||
notify_user_about_new_alert_group(
|
||||
user_pk=user.pk,
|
||||
alert_group_pk=alert_group.pk,
|
||||
notification_policy_pk=notification_policy.pk,
|
||||
critical=False,
|
||||
)
|
||||
mock_send_message_to_fcm_device.assert_called_once()
|
||||
log_record = alert_group.personal_log_records.last()
|
||||
assert log_record.type == UserNotificationPolicyLogRecord.TYPE_PERSONAL_NOTIFICATION_FAILED
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_fcm_message_user_settings(
|
||||
make_organization_and_user, make_alert_receive_channel, make_alert_group, make_alert
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ def _send_push_notification_to_fcm_relay(message: Message) -> requests.Response:
|
|||
return response
|
||||
|
||||
|
||||
def send_message_to_fcm_device(device: "FCMDevice", message: Message) -> None:
|
||||
def send_message_to_fcm_device(device: "FCMDevice", message: Message) -> bool:
|
||||
"""
|
||||
https://firebase.google.com/docs/cloud-messaging/http-server-ref#interpret-downstream
|
||||
"""
|
||||
|
|
@ -60,9 +60,10 @@ def send_message_to_fcm_device(device: "FCMDevice", message: Message) -> None:
|
|||
|
||||
if isinstance(response, FIREBASE_ERRORS_TO_NOT_RETRY):
|
||||
logger.warning(f"FCM error {response} is not being retried as we explicitly do not want to retry it")
|
||||
return
|
||||
return False
|
||||
|
||||
raise response
|
||||
return True
|
||||
|
||||
|
||||
def send_push_notification(
|
||||
|
|
@ -97,7 +98,10 @@ def send_push_notification(
|
|||
else:
|
||||
raise
|
||||
else:
|
||||
send_message_to_fcm_device(device_to_notify, message)
|
||||
succeeded = send_message_to_fcm_device(device_to_notify, message)
|
||||
if not succeeded:
|
||||
_error_cb()
|
||||
return False
|
||||
|
||||
# notification succeeded (otherwise raised exception before)
|
||||
return True
|
||||
|
|
|
|||
|
|
@ -1,4 +1,8 @@
|
|||
CLOUD_NOT_SYNCED = 0
|
||||
CLOUD_SYNCED_USER_NOT_FOUND = 1
|
||||
CLOUD_SYNCED_PHONE_NOT_VERIFIED = 2
|
||||
CLOUD_SYNCED_PHONE_VERIFIED = 3
|
||||
from enum import IntEnum
|
||||
|
||||
|
||||
class CloudSyncStatus(IntEnum):
|
||||
NOT_SYNCED = 0
|
||||
SYNCED_USER_NOT_FOUND = 1
|
||||
SYNCED_PHONE_NOT_VERIFIED = 2
|
||||
SYNCED_PHONE_VERIFIED = 3
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ from urllib.parse import urljoin
|
|||
|
||||
from django.utils import timezone
|
||||
|
||||
from apps.oss_installation import constants as oss_constants
|
||||
from apps.oss_installation.constants import CloudSyncStatus
|
||||
from apps.schedules.ical_utils import list_users_to_notify_from_ical_for_period
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
|
@ -70,15 +70,15 @@ def active_oss_users_count():
|
|||
def cloud_user_identity_status(connector, identity):
|
||||
link = None
|
||||
if connector is None:
|
||||
status = oss_constants.CLOUD_NOT_SYNCED
|
||||
status = CloudSyncStatus.NOT_SYNCED
|
||||
elif identity is None:
|
||||
status = oss_constants.CLOUD_SYNCED_USER_NOT_FOUND
|
||||
status = CloudSyncStatus.SYNCED_USER_NOT_FOUND
|
||||
link = connector.cloud_url
|
||||
else:
|
||||
if identity.phone_number_verified:
|
||||
status = oss_constants.CLOUD_SYNCED_PHONE_VERIFIED
|
||||
status = CloudSyncStatus.SYNCED_PHONE_VERIFIED
|
||||
else:
|
||||
status = oss_constants.CLOUD_SYNCED_PHONE_NOT_VERIFIED
|
||||
status = CloudSyncStatus.SYNCED_PHONE_NOT_VERIFIED
|
||||
|
||||
link = urljoin(connector.cloud_url, f"a/grafana-oncall-app/?page=users&p=1&id={identity.cloud_id}")
|
||||
return status, link
|
||||
|
|
|
|||
|
|
@ -20,12 +20,9 @@ class CloudUsersPagination(HundredPageSizePaginator):
|
|||
# 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),
|
||||
"matched_users_count": matched_users_count,
|
||||
}
|
||||
)
|
||||
response = super().get_paginated_response(data)
|
||||
response.data["matched_users_count"] = matched_users_count
|
||||
return response
|
||||
|
||||
|
||||
class CloudUsersView(CloudUsersPagination, APIView):
|
||||
|
|
|
|||
|
|
@ -147,7 +147,7 @@ class SlackUserIdentity(models.Model):
|
|||
)
|
||||
|
||||
@property
|
||||
def slack_verbal(self):
|
||||
def slack_verbal(self) -> str | None:
|
||||
return (
|
||||
self.profile_real_name_normalized
|
||||
or self.profile_real_name
|
||||
|
|
|
|||
|
|
@ -158,6 +158,7 @@ class TelegramToUserConnector(models.Model):
|
|||
)
|
||||
else:
|
||||
self._nudge_about_alert_group_message(telegram_client, old_alert_group_message)
|
||||
TelegramToUserConnector.create_telegram_notification_success(alert_group, self.user, notification_policy)
|
||||
|
||||
# send DM message with the link to the alert group post in channel
|
||||
def send_link_to_channel_message(self, alert_group: AlertGroup, notification_policy: UserNotificationPolicy):
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ from telegram import error
|
|||
|
||||
from apps.base.models import UserNotificationPolicy, UserNotificationPolicyLogRecord
|
||||
from apps.telegram.client import TelegramClient
|
||||
from apps.telegram.models import TelegramMessage
|
||||
from apps.telegram.models import TelegramMessage, TelegramToUserConnector
|
||||
|
||||
|
||||
@patch.object(TelegramClient, "send_raw_message", side_effect=error.BadRequest("Replied message not found"))
|
||||
|
|
@ -188,6 +188,43 @@ def test_personal_connector_send_full_alert_group(
|
|||
assert log_record.type == UserNotificationPolicyLogRecord.TYPE_PERSONAL_NOTIFICATION_SUCCESS
|
||||
|
||||
|
||||
@patch.object(TelegramToUserConnector, "_nudge_about_alert_group_message")
|
||||
@pytest.mark.django_db
|
||||
def test_personal_connector_send_full_alert_group_second_time(
|
||||
mock_nudge_about_alert_group_message,
|
||||
make_organization_and_user,
|
||||
make_telegram_user_connector,
|
||||
make_user_notification_policy,
|
||||
make_alert_receive_channel,
|
||||
make_alert_group,
|
||||
make_alert,
|
||||
make_telegram_message,
|
||||
):
|
||||
# set up a user with Telegram account connected
|
||||
organization, user = make_organization_and_user()
|
||||
connector = make_telegram_user_connector(user)
|
||||
notification_policy = make_user_notification_policy(
|
||||
user,
|
||||
UserNotificationPolicy.Step.NOTIFY,
|
||||
notify_by=UserNotificationPolicy.NotificationChannel.TELEGRAM,
|
||||
important=False,
|
||||
)
|
||||
|
||||
# create an alert group with an existing Telegram message in channel
|
||||
alert_receive_channel = make_alert_receive_channel(organization)
|
||||
alert_group = make_alert_group(alert_receive_channel)
|
||||
make_alert(alert_group=alert_group, raw_request_data=alert_receive_channel.config.example_payload)
|
||||
make_telegram_message(
|
||||
alert_group=alert_group,
|
||||
message_type=TelegramMessage.PERSONAL_MESSAGE,
|
||||
chat_id=connector.telegram_chat_id,
|
||||
)
|
||||
user.telegram_connection.send_full_alert_group(alert_group, notification_policy)
|
||||
mock_nudge_about_alert_group_message.assert_called_once()
|
||||
log_record = notification_policy.personal_log_records.last()
|
||||
assert log_record.type == UserNotificationPolicyLogRecord.TYPE_PERSONAL_NOTIFICATION_SUCCESS
|
||||
|
||||
|
||||
@patch.object(TelegramClient, "send_message", side_effect=[error.BadRequest("error"), None])
|
||||
@pytest.mark.django_db
|
||||
def test_personal_connector_send_full_alert_group_formatting_error(
|
||||
|
|
|
|||
|
|
@ -257,7 +257,7 @@ class User(models.Model):
|
|||
return urljoin(self.organization.grafana_url, self.avatar_url)
|
||||
|
||||
@property
|
||||
def verified_phone_number(self):
|
||||
def verified_phone_number(self) -> str | None:
|
||||
"""
|
||||
Use property to highlight that _verified_phone_number should not be modified directly
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -278,3 +278,21 @@ def test_escaping_payload_with_single_quote_in_string(make_organization, make_cu
|
|||
}
|
||||
request_kwargs = webhook.build_request_kwargs(payload)
|
||||
assert request_kwargs == {"headers": {}, "json": {"data": "{'text': \"Hi, it's alert\"}"}}
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_escaping_unicode_in_string(make_organization, make_custom_webhook):
|
||||
organization = make_organization()
|
||||
webhook = make_custom_webhook(
|
||||
organization=organization,
|
||||
data='{"data" : "{{ alert_payload.text }}"}',
|
||||
forward_all=False,
|
||||
)
|
||||
|
||||
payload = {
|
||||
"alert_payload": {
|
||||
"text": "東京",
|
||||
}
|
||||
}
|
||||
request_kwargs = webhook.build_request_kwargs(payload)
|
||||
assert request_kwargs == {"headers": {}, "json": {"data": "東京"}}
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ def escape_string(string: str):
|
|||
json.dumps is the simples way to escape all special characters in string.
|
||||
First and last chars are quotes from json.dumps(), we don't need them, only escaping.
|
||||
"""
|
||||
return json.dumps(string)[1:-1]
|
||||
return json.dumps(string, ensure_ascii=False)[1:-1]
|
||||
|
||||
|
||||
class EscapeDoubleQuotesDict(dict):
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from drf_spectacular.utils import extend_schema_field
|
||||
from rest_framework import fields, serializers
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.relations import RelatedField
|
||||
|
|
@ -9,6 +10,7 @@ from common.api_helpers.exceptions import BadRequest
|
|||
from common.timezones import raise_exception_if_not_valid_timezone
|
||||
|
||||
|
||||
@extend_schema_field(serializers.CharField)
|
||||
class OrganizationFilteredPrimaryKeyRelatedField(RelatedField):
|
||||
"""
|
||||
This field is used to filter entities by organization
|
||||
|
|
@ -42,6 +44,7 @@ class OrganizationFilteredPrimaryKeyRelatedField(RelatedField):
|
|||
return self.display_func(instance)
|
||||
|
||||
|
||||
@extend_schema_field(serializers.CharField)
|
||||
class TeamPrimaryKeyRelatedField(RelatedField):
|
||||
"""
|
||||
This field is used to get user teams
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ from datetime import datetime
|
|||
from django.db.models import Q
|
||||
from django_filters import rest_framework as filters
|
||||
from django_filters.utils import handle_timezone
|
||||
from drf_spectacular.utils import extend_schema_field
|
||||
from rest_framework import serializers
|
||||
|
||||
from apps.user_management.models import Team
|
||||
from common.api_helpers.exceptions import BadRequest
|
||||
|
|
@ -48,6 +50,13 @@ class DateRangeFilterMixin:
|
|||
return start_date, end_date
|
||||
|
||||
|
||||
@extend_schema_field(serializers.CharField)
|
||||
class MultipleChoiceCharFilter(filters.ModelMultipleChoiceFilter):
|
||||
"""MultipleChoiceCharFilter with an explicit schema. Otherwise, drf-specacular may generate a wrong schema."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class ModelFieldFilterMixin:
|
||||
def filter_model_field(self, queryset, name, value):
|
||||
if not value:
|
||||
|
|
@ -85,8 +94,7 @@ class ByTeamModelFieldFilterMixin:
|
|||
if null_team_lookup is not None:
|
||||
teams_lookup = teams_lookup | null_team_lookup
|
||||
|
||||
queryset = queryset.filter(teams_lookup)
|
||||
return queryset
|
||||
return queryset.filter(teams_lookup).distinct()
|
||||
|
||||
|
||||
def get_team_queryset(request):
|
||||
|
|
@ -107,6 +115,7 @@ class ByTeamFilter(ByTeamModelFieldFilterMixin, filters.FilterSet):
|
|||
)
|
||||
|
||||
|
||||
@extend_schema_field(serializers.CharField)
|
||||
class TeamModelMultipleChoiceFilter(filters.ModelMultipleChoiceFilter):
|
||||
def __init__(
|
||||
self,
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@ from django.core.exceptions import ObjectDoesNotExist
|
|||
from django.db import models
|
||||
from django.db.models import Q
|
||||
from django.utils.functional import cached_property
|
||||
from rest_framework import status
|
||||
from drf_spectacular.utils import extend_schema, inline_serializer
|
||||
from rest_framework import serializers, status
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.exceptions import NotFound, Throttled
|
||||
from rest_framework.request import Request
|
||||
|
|
@ -281,6 +282,21 @@ class PreviewTemplateException(Exception):
|
|||
|
||||
|
||||
class PreviewTemplateMixin:
|
||||
@extend_schema(
|
||||
description="Preview template",
|
||||
request=inline_serializer(
|
||||
name="PreviewTemplateRequest",
|
||||
fields={
|
||||
"template_body": serializers.CharField(required=False, allow_null=True),
|
||||
"template_name": serializers.CharField(required=False, allow_null=True),
|
||||
"payload": serializers.DictField(required=False, allow_null=True),
|
||||
},
|
||||
),
|
||||
responses=inline_serializer(
|
||||
name="PreviewTemplateResponse",
|
||||
fields={"preview": serializers.CharField(allow_null=True)},
|
||||
),
|
||||
)
|
||||
@action(methods=["post"], detail=True)
|
||||
def preview_template(self, request, pk):
|
||||
template_body = request.data.get("template_body", None)
|
||||
|
|
|
|||
|
|
@ -28,40 +28,43 @@ 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:
|
||||
return {
|
||||
"next": self.get_next_link(),
|
||||
"previous": self.get_previous_link(),
|
||||
"results": data,
|
||||
"page_size": self.get_page_size(self.request),
|
||||
}
|
||||
|
||||
|
||||
class PathPrefixedPagePagination(BasePathPrefixedPagination, PageNumberPagination):
|
||||
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))
|
||||
response = super().get_paginated_response(data)
|
||||
response.data.update(
|
||||
{
|
||||
"page_size": self.get_page_size(self.request),
|
||||
"current_page_number": self.page.number,
|
||||
"total_pages": self.page.paginator.num_pages,
|
||||
}
|
||||
)
|
||||
return response
|
||||
|
||||
def get_paginated_response_schema(self, schema):
|
||||
paginated_schema = super().get_paginated_response_schema(schema)
|
||||
paginated_schema["properties"].update(
|
||||
{
|
||||
"page_size": {"type": "integer"},
|
||||
"current_page_number": {"type": "integer"},
|
||||
"total_pages": {"type": "integer"},
|
||||
}
|
||||
)
|
||||
return paginated_schema
|
||||
|
||||
|
||||
class PathPrefixedCursorPagination(BasePathPrefixedPagination, CursorPagination):
|
||||
def get_paginated_response(self, data: PaginatedData) -> Response:
|
||||
return Response(self._get_base_paginated_response_data(data))
|
||||
response = super().get_paginated_response(data)
|
||||
response.data.update({"page_size": self.page_size})
|
||||
return response
|
||||
|
||||
def get_paginated_response_schema(self, schema):
|
||||
paginated_schema = super().get_paginated_response_schema(schema)
|
||||
paginated_schema["properties"].update({"page_size": {"type": "integer"}})
|
||||
return paginated_schema
|
||||
|
||||
|
||||
class HundredPageSizePaginator(PathPrefixedPagePagination):
|
||||
|
|
|
|||
47
engine/engine/schema.py
Normal file
47
engine/engine/schema.py
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
from drf_spectacular.openapi import AutoSchema
|
||||
from drf_spectacular.plumbing import get_view_model
|
||||
|
||||
from common.api_helpers.mixins import PublicPrimaryKeyMixin
|
||||
|
||||
|
||||
class CustomAutoSchema(AutoSchema):
|
||||
def _get_serializer(self):
|
||||
"""Makes so that extra actions (@action on viewset) don't inherit the serializer from the viewset."""
|
||||
if self._is_extra_action:
|
||||
return None
|
||||
return super()._get_serializer()
|
||||
|
||||
def _get_paginator(self):
|
||||
"""Makes so that extra actions (@action on viewset) don't inherit the paginator from the viewset."""
|
||||
if self._is_extra_action:
|
||||
return None
|
||||
return super()._get_paginator()
|
||||
|
||||
def get_filter_backends(self):
|
||||
"""Makes so that extra actions (@action on viewset) don't inherit the filter backends from the viewset."""
|
||||
if self._is_extra_action:
|
||||
return []
|
||||
return super().get_filter_backends()
|
||||
|
||||
def _resolve_path_parameters(self, variables):
|
||||
"""A workaround to make public primary keys appear as strings in the OpenAPI schema."""
|
||||
|
||||
parameters = super()._resolve_path_parameters(variables)
|
||||
if not isinstance(self.view, PublicPrimaryKeyMixin):
|
||||
return parameters
|
||||
|
||||
for parameter in parameters:
|
||||
if parameter["name"] == "id" and parameter["in"] == "path":
|
||||
parameter["schema"]["type"] = "string"
|
||||
model = get_view_model(self.view, emit_warnings=False)
|
||||
model_name = model._meta.verbose_name if model else "resource"
|
||||
parameter["description"] = f"A string identifying this {model_name}."
|
||||
|
||||
return parameters
|
||||
|
||||
@property
|
||||
def _is_extra_action(self) -> bool:
|
||||
try:
|
||||
return self.view.action in [action.__name__ for action in self.view.get_extra_actions()]
|
||||
except AttributeError:
|
||||
return False
|
||||
|
|
@ -287,7 +287,7 @@ REST_FRAMEWORK = {
|
|||
"rest_framework.parsers.MultiPartParser",
|
||||
),
|
||||
"DEFAULT_AUTHENTICATION_CLASSES": [],
|
||||
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
|
||||
"DEFAULT_SCHEMA_CLASS": "engine.schema.CustomAutoSchema",
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -315,6 +315,8 @@ if SWAGGER_UI_SETTINGS_URL:
|
|||
SPECTACULAR_INCLUDED_PATHS = [
|
||||
"/features",
|
||||
"/alertgroups",
|
||||
"/alert_receive_channels",
|
||||
"/users",
|
||||
"/labels",
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -55,5 +55,5 @@ REST_FRAMEWORK = {
|
|||
),
|
||||
"DEFAULT_AUTHENTICATION_CLASSES": [],
|
||||
"DEFAULT_RENDERER_CLASSES": ("rest_framework.renderers.JSONRenderer",),
|
||||
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
|
||||
"DEFAULT_SCHEMA_CLASS": "engine.schema.CustomAutoSchema",
|
||||
}
|
||||
|
|
|
|||
|
|
@ -190,7 +190,6 @@ class GForm extends React.Component<GFormProps, {}> {
|
|||
{({ register, errors, control, getValues, setValue }) => {
|
||||
const renderField = (formItem: FormItem, formIndex: number) => {
|
||||
if (formItem.isVisible && !formItem.isVisible(getValues())) {
|
||||
setValue(formItem.name, undefined); // clear input value on hide
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -59,6 +59,9 @@ const TimeRange = (props: TimeRangeProps) => {
|
|||
|
||||
const handleChangeFrom = useCallback(
|
||||
(value: moment.Moment) => {
|
||||
if (!value.isValid()) {
|
||||
return;
|
||||
}
|
||||
setFrom(value);
|
||||
|
||||
if (value.isSame(to, 'minute')) {
|
||||
|
|
@ -74,6 +77,9 @@ const TimeRange = (props: TimeRangeProps) => {
|
|||
|
||||
const handleChangeTo = useCallback(
|
||||
(value: moment.Moment) => {
|
||||
if (!value.isValid()) {
|
||||
return;
|
||||
}
|
||||
setTo(value);
|
||||
|
||||
if (value.isSame(from, 'minute')) {
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ const UserSettings = observer(({ id, onHide, tab = UserSettingsTab.UserInfo }: U
|
|||
isCurrent && organizationStore.currentOrganization?.slack_team_identity && !storeUser.slack_user_identity,
|
||||
isCurrent && store.hasFeature(AppFeature.Telegram) && !storeUser.telegram_configuration,
|
||||
isCurrent,
|
||||
store.hasFeature(AppFeature.MsTeams),
|
||||
store.hasFeature(AppFeature.MsTeams) && !storeUser.messaging_backends.MSTEAMS,
|
||||
];
|
||||
|
||||
const title = (
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { useStore } from 'state/useStore';
|
|||
|
||||
import ICalConnector from './ICalConnector';
|
||||
// import MobileAppConnector from './MobileAppConnector';
|
||||
import MSTeamsConnector from './MSTeamsConnector';
|
||||
import MobileAppConnector from './MobileAppConnector';
|
||||
import PhoneConnector from './PhoneConnector';
|
||||
import SlackConnector from './SlackConnector';
|
||||
|
|
@ -27,6 +28,7 @@ export const Connectors: FC<ConnectorsProps> = (props) => {
|
|||
<MobileAppConnector {...props} />
|
||||
<SlackConnector {...props} />
|
||||
{store.hasFeature(AppFeature.Telegram) && <TelegramConnector {...props} />}
|
||||
{store.hasFeature(AppFeature.MsTeams) && <MSTeamsConnector {...props} />}
|
||||
<Legend>Calendar export</Legend>
|
||||
<ICalConnector {...props} />
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import { UserSettingsTab } from 'containers/UserSettings/UserSettings.types';
|
|||
import { User } from 'models/user/user.types';
|
||||
import { useStore } from 'state/useStore';
|
||||
|
||||
import styles from 'containers/UserSettings/parts/connectors/index.module.css';
|
||||
import styles from 'containers/UserSettings/parts/connectors/Connectors.module.css';
|
||||
|
||||
const cx = cn.bind(styles);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,35 +0,0 @@
|
|||
import React, { FC } from 'react';
|
||||
|
||||
import { Legend } from '@grafana/ui';
|
||||
|
||||
import { UserSettingsTab } from 'containers/UserSettings/UserSettings.types';
|
||||
import { User } from 'models/user/user.types';
|
||||
import { AppFeature } from 'state/features';
|
||||
import { useStore } from 'state/useStore';
|
||||
|
||||
import ICalConnector from './ICalConnector';
|
||||
import MSTeamsConnector from './MSTeamsConnector';
|
||||
import MobileAppConnector from './MobileAppConnector';
|
||||
import PhoneConnector from './PhoneConnector';
|
||||
import SlackConnector from './SlackConnector';
|
||||
import TelegramConnector from './TelegramConnector';
|
||||
|
||||
interface ConnectorsProps {
|
||||
id: User['pk'];
|
||||
onTabChange: (tab: UserSettingsTab) => void;
|
||||
}
|
||||
|
||||
export const Connectors: FC<ConnectorsProps> = (props) => {
|
||||
const store = useStore();
|
||||
return (
|
||||
<>
|
||||
<PhoneConnector {...props} />
|
||||
<MobileAppConnector {...props} />
|
||||
<SlackConnector {...props} />
|
||||
{store.hasFeature(AppFeature.Telegram) && <TelegramConnector {...props} />}
|
||||
{store.hasFeature(AppFeature.MsTeams) && <MSTeamsConnector {...props} />}
|
||||
<Legend>Calendar export</Legend>
|
||||
<ICalConnector {...props} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
@ -68,7 +68,6 @@ export class AlertReceiveChannelStore extends BaseStore {
|
|||
this.path = '/alert_receive_channels/';
|
||||
}
|
||||
|
||||
@action.bound
|
||||
getSearchResult(_query = '') {
|
||||
if (!this.searchResult) {
|
||||
return undefined;
|
||||
|
|
@ -79,7 +78,6 @@ export class AlertReceiveChannelStore extends BaseStore {
|
|||
);
|
||||
}
|
||||
|
||||
@action.bound
|
||||
getPaginatedSearchResult(_query = '') {
|
||||
if (!this.paginatedSearchResult) {
|
||||
return undefined;
|
||||
|
|
@ -94,7 +92,7 @@ export class AlertReceiveChannelStore extends BaseStore {
|
|||
};
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
async loadItem(id: AlertReceiveChannel['id'], skipErrorHandling = false): Promise<AlertReceiveChannel> {
|
||||
const alertReceiveChannel = await this.getById(id, skipErrorHandling);
|
||||
|
||||
|
|
@ -111,7 +109,7 @@ export class AlertReceiveChannelStore extends BaseStore {
|
|||
return alertReceiveChannel;
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
async updateItems(query: any = '') {
|
||||
const params = typeof query === 'string' ? { search: query } : query;
|
||||
|
||||
|
|
@ -141,7 +139,6 @@ export class AlertReceiveChannelStore extends BaseStore {
|
|||
return results;
|
||||
}
|
||||
|
||||
@action.bound
|
||||
async updatePaginatedItems({
|
||||
filters,
|
||||
page = 1,
|
||||
|
|
@ -189,7 +186,6 @@ export class AlertReceiveChannelStore extends BaseStore {
|
|||
return results;
|
||||
}
|
||||
|
||||
@action.bound
|
||||
populateHearbeats(alertReceiveChannels: AlertReceiveChannel[]) {
|
||||
const heartbeats = alertReceiveChannels.reduce((acc: any, alertReceiveChannel: AlertReceiveChannel) => {
|
||||
if (alertReceiveChannel.heartbeat) {
|
||||
|
|
@ -225,7 +221,7 @@ export class AlertReceiveChannelStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
async updateChannelFilters(alertReceiveChannelId: AlertReceiveChannel['id'], isOverwrite = false) {
|
||||
const response = await makeRequest(`/channel_filters/`, {
|
||||
params: { alert_receive_channel: alertReceiveChannelId },
|
||||
|
|
@ -263,7 +259,7 @@ export class AlertReceiveChannelStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
async updateChannelFilter(channelFilterId: ChannelFilter['id']) {
|
||||
const response = await makeRequest(`/channel_filters/${channelFilterId}/`, {});
|
||||
|
||||
|
|
@ -274,14 +270,13 @@ export class AlertReceiveChannelStore extends BaseStore {
|
|||
};
|
||||
});
|
||||
}
|
||||
@action.bound
|
||||
|
||||
async migrateChannel(id: AlertReceiveChannel['id']) {
|
||||
return await makeRequest(`/alert_receive_channels/${id}/migrate`, {
|
||||
method: 'POST',
|
||||
});
|
||||
}
|
||||
|
||||
@action.bound
|
||||
async createChannelFilter(data: Partial<ChannelFilter>) {
|
||||
return await makeRequest('/channel_filters/', {
|
||||
method: 'POST',
|
||||
|
|
@ -289,7 +284,7 @@ export class AlertReceiveChannelStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
async saveChannelFilter(channelFilterId: ChannelFilter['id'], data: Partial<ChannelFilter>) {
|
||||
const response = await makeRequest(`/channel_filters/${channelFilterId}/`, {
|
||||
method: 'PUT',
|
||||
|
|
@ -306,7 +301,7 @@ export class AlertReceiveChannelStore extends BaseStore {
|
|||
return response;
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
async moveChannelFilterToPosition(
|
||||
alertReceiveChannelId: AlertReceiveChannel['id'],
|
||||
oldIndex: number,
|
||||
|
|
@ -325,7 +320,7 @@ export class AlertReceiveChannelStore extends BaseStore {
|
|||
this.updateChannelFilters(alertReceiveChannelId, true);
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
async deleteChannelFilter(channelFilterId: ChannelFilter['id']) {
|
||||
const channelFilter = this.channelFilters[channelFilterId];
|
||||
|
||||
|
|
@ -350,7 +345,6 @@ export class AlertReceiveChannelStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action.bound
|
||||
getIntegration(alertReceiveChannel: Partial<AlertReceiveChannel>): SelectOption {
|
||||
return (
|
||||
this.alertReceiveChannelOptions &&
|
||||
|
|
@ -374,12 +368,11 @@ export class AlertReceiveChannelStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action.bound
|
||||
async deleteAlertReceiveChannel(id: AlertReceiveChannel['id']) {
|
||||
return await this.delete(id);
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
async updateTemplates(alertReceiveChannelId: AlertReceiveChannel['id'], alertGroupId?: Alert['pk']) {
|
||||
const response = await makeRequest(`/alert_receive_channel_templates/${alertReceiveChannelId}/`, {
|
||||
params: { alert_group_id: alertGroupId },
|
||||
|
|
@ -394,7 +387,7 @@ export class AlertReceiveChannelStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
async updateItem(id: AlertReceiveChannel['id']) {
|
||||
const item = await this.getById(id);
|
||||
|
||||
|
|
@ -406,7 +399,7 @@ export class AlertReceiveChannelStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
async saveTemplates(alertReceiveChannelId: AlertReceiveChannel['id'], data: Partial<AlertTemplatesDTO>) {
|
||||
const response = await makeRequest(`/alert_receive_channel_templates/${alertReceiveChannelId}/`, {
|
||||
method: 'PUT',
|
||||
|
|
@ -422,12 +415,11 @@ export class AlertReceiveChannelStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action.bound
|
||||
async getGrafanaAlertingContactPoints() {
|
||||
return await makeRequest(`${this.path}contact_points/`, {}).catch(showApiError);
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
async updateConnectedContactPoints(alertReceiveChannelId: AlertReceiveChannel['id']) {
|
||||
const response = await makeRequest(`${this.path}${alertReceiveChannelId}/connected_contact_points `, {});
|
||||
|
||||
|
|
@ -451,7 +443,6 @@ export class AlertReceiveChannelStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action.bound
|
||||
async connectContactPoint(
|
||||
alertReceiveChannelId: AlertReceiveChannel['id'],
|
||||
datasource_uid: string,
|
||||
|
|
@ -466,7 +457,6 @@ export class AlertReceiveChannelStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action.bound
|
||||
async disconnectContactPoint(
|
||||
alertReceiveChannelId: AlertReceiveChannel['id'],
|
||||
datasource_uid: string,
|
||||
|
|
@ -481,7 +471,6 @@ export class AlertReceiveChannelStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action.bound
|
||||
async createContactPoint(
|
||||
alertReceiveChannelId: AlertReceiveChannel['id'],
|
||||
datasource_uid: string,
|
||||
|
|
@ -496,14 +485,12 @@ export class AlertReceiveChannelStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action.bound
|
||||
async getAccessLogs(alertReceiveChannelId: AlertReceiveChannel['id']) {
|
||||
const { integration_log } = await makeRequest(`/alert_receive_channel_access_log/${alertReceiveChannelId}/`, {});
|
||||
|
||||
return integration_log;
|
||||
}
|
||||
|
||||
@action.bound
|
||||
async sendDemoAlert(id: AlertReceiveChannel['id'], payload: string = undefined) {
|
||||
const requestConfig: any = {
|
||||
method: 'POST',
|
||||
|
|
@ -518,12 +505,10 @@ export class AlertReceiveChannelStore extends BaseStore {
|
|||
await makeRequest(`${this.path}${id}/send_demo_alert/`, requestConfig).catch(showApiError);
|
||||
}
|
||||
|
||||
@action.bound
|
||||
async sendDemoAlertToParticularRoute(id: ChannelFilter['id']) {
|
||||
await makeRequest(`/channel_filters/${id}/send_demo_alert/`, { method: 'POST' }).catch(showApiError);
|
||||
}
|
||||
|
||||
@action.bound
|
||||
async convertRegexpTemplateToJinja2Template(id: ChannelFilter['id']) {
|
||||
const result = await makeRequest(`/channel_filters/${id}/convert_from_regex_to_jinja2/`, { method: 'POST' }).catch(
|
||||
showApiError
|
||||
|
|
@ -531,7 +516,6 @@ export class AlertReceiveChannelStore extends BaseStore {
|
|||
return result;
|
||||
}
|
||||
|
||||
@action.bound
|
||||
async renderPreview(id: AlertReceiveChannel['id'], template_name: string, template_body: string, payload: JSON) {
|
||||
return await makeRequest(`${this.path}${id}/preview_template/`, {
|
||||
method: 'POST',
|
||||
|
|
@ -539,7 +523,6 @@ export class AlertReceiveChannelStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action.bound
|
||||
async changeTeam(id: AlertReceiveChannel['id'], teamId: GrafanaTeam['id']) {
|
||||
return await makeRequest(`${this.path}${id}/change_team`, {
|
||||
params: { team_id: String(teamId) },
|
||||
|
|
@ -547,7 +530,7 @@ export class AlertReceiveChannelStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
async updateCounters() {
|
||||
const counters = await makeRequest(`${this.path}counters`, {
|
||||
method: 'GET',
|
||||
|
|
@ -558,7 +541,7 @@ export class AlertReceiveChannelStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
async updateCountersForIntegration(id: AlertReceiveChannel['id']): Promise<any> {
|
||||
const counters = await makeRequest(`${this.path}${id}/counters`, {
|
||||
method: 'GET',
|
||||
|
|
@ -576,7 +559,6 @@ export class AlertReceiveChannelStore extends BaseStore {
|
|||
return counters;
|
||||
}
|
||||
|
||||
@action.bound
|
||||
startMaintenanceMode = (id: AlertReceiveChannel['id'], mode: MaintenanceMode, duration: number): Promise<void> =>
|
||||
makeRequest<null>(`${this.path}${id}/start_maintenance/`, {
|
||||
method: 'POST',
|
||||
|
|
@ -586,13 +568,11 @@ export class AlertReceiveChannelStore extends BaseStore {
|
|||
},
|
||||
});
|
||||
|
||||
@action.bound
|
||||
stopMaintenanceMode = (id: AlertReceiveChannel['id']) =>
|
||||
makeRequest<null>(`${this.path}${id}/stop_maintenance/`, {
|
||||
method: 'POST',
|
||||
});
|
||||
|
||||
@action.bound
|
||||
addLabel = (id: AlertReceiveChannel['id'], data) => {
|
||||
makeRequest(`${this.path}${id}/associate_label`, {
|
||||
method: 'POST',
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@ export class AlertReceiveChannelFiltersStore extends BaseStore {
|
|||
this.path = '/alert_receive_channels/';
|
||||
}
|
||||
|
||||
@action.bound
|
||||
getSearchResult() {
|
||||
if (!this.searchResult) {
|
||||
return undefined;
|
||||
|
|
@ -29,7 +28,7 @@ export class AlertReceiveChannelFiltersStore extends BaseStore {
|
|||
return this.searchResult.map((value: SelectOption['value']) => this.items?.[value]);
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
async updateItems(query = '') {
|
||||
const results = await makeRequest(`${this.path}`, {
|
||||
params: { search: query, filters: true },
|
||||
|
|
|
|||
|
|
@ -84,7 +84,6 @@ export class AlertGroupStore extends BaseStore {
|
|||
this.path = '/alertgroups/';
|
||||
}
|
||||
|
||||
@action.bound
|
||||
async attachAlert(pk: Alert['pk'], rootPk: Alert['pk']) {
|
||||
return await makeRequest(`${this.path}${pk}/attach/`, {
|
||||
method: 'POST',
|
||||
|
|
@ -92,14 +91,13 @@ export class AlertGroupStore extends BaseStore {
|
|||
}).catch(showApiError);
|
||||
}
|
||||
|
||||
@action.bound
|
||||
async unattachAlert(pk: Alert['pk']) {
|
||||
return await makeRequest(`${this.path}${pk}/unattach/`, {
|
||||
method: 'POST',
|
||||
}).catch(showApiError);
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
async updateItem(id: Alert['pk']) {
|
||||
const item = await this.getById(id);
|
||||
|
||||
|
|
@ -111,7 +109,6 @@ export class AlertGroupStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action.bound
|
||||
getSearchResult(query = '') {
|
||||
if (!this.searchResult[query]) {
|
||||
return undefined;
|
||||
|
|
@ -120,7 +117,6 @@ export class AlertGroupStore extends BaseStore {
|
|||
return this.searchResult[query].map((id: Alert['pk']) => this.items[id]);
|
||||
}
|
||||
|
||||
@action.bound
|
||||
async getAlertGroupsForIntegration(integrationId: AlertReceiveChannel['id']) {
|
||||
const { results } = await makeRequest(`${this.path}`, {
|
||||
params: { integration: integrationId },
|
||||
|
|
@ -128,12 +124,10 @@ export class AlertGroupStore extends BaseStore {
|
|||
return results;
|
||||
}
|
||||
|
||||
@action.bound
|
||||
async getAlertsFromGroup(pk: Alert['pk']) {
|
||||
return await makeRequest(`${this.path}${pk}`, {});
|
||||
}
|
||||
|
||||
@action.bound
|
||||
async updateSilenceOptions() {
|
||||
const result = await makeRequest(`${this.path}silence_options/`, {});
|
||||
|
||||
|
|
@ -142,7 +136,6 @@ export class AlertGroupStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action.bound
|
||||
async resolve(id: Alert['pk'], delay: number) {
|
||||
await makeRequest(`${this.path}${id}/silence/`, {
|
||||
method: 'POST',
|
||||
|
|
@ -150,28 +143,24 @@ export class AlertGroupStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action.bound
|
||||
async unresolve(id: Alert['pk']) {
|
||||
await makeRequest(`${this.path}${id}/unresolve/`, {
|
||||
method: 'POST',
|
||||
});
|
||||
}
|
||||
|
||||
@action.bound
|
||||
async acknowledge(id: Alert['pk']) {
|
||||
await makeRequest(`${this.path}${id}/acknowledge/`, {
|
||||
method: 'POST',
|
||||
});
|
||||
}
|
||||
|
||||
@action.bound
|
||||
async unacknowledge(id: Alert['pk']) {
|
||||
await makeRequest(`${this.path}${id}/unacknowledge/`, {
|
||||
method: 'POST',
|
||||
});
|
||||
}
|
||||
|
||||
@action.bound
|
||||
async silence(id: Alert['pk'], delay: number) {
|
||||
await makeRequest(`${this.path}${id}/silence/`, {
|
||||
method: 'POST',
|
||||
|
|
@ -179,14 +168,13 @@ export class AlertGroupStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action.bound
|
||||
async unsilence(id: Alert['pk']) {
|
||||
await makeRequest(`${this.path}${id}/unsilence/`, {
|
||||
method: 'POST',
|
||||
});
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
async updateBulkActions() {
|
||||
const response = await makeRequest(`${this.path}bulk_action_options/`, {});
|
||||
|
||||
|
|
@ -201,7 +189,6 @@ export class AlertGroupStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action.bound
|
||||
async bulkAction(data: any) {
|
||||
return await makeRequest(`${this.path}bulk_action/`, {
|
||||
method: 'POST',
|
||||
|
|
@ -209,7 +196,6 @@ export class AlertGroupStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action.bound
|
||||
async renderPreview(id: Alert['pk'], template_name: string, template_body: string) {
|
||||
return await makeRequest(`${this.path}${id}/preview_template/`, {
|
||||
method: 'POST',
|
||||
|
|
@ -219,7 +205,6 @@ export class AlertGroupStore extends BaseStore {
|
|||
|
||||
// methods were moved from rootBaseStore.
|
||||
// TODO check if methods are dublicating existing ones
|
||||
@action.bound
|
||||
async updateIncidents() {
|
||||
await Promise.all([
|
||||
this.getNewIncidentsStats(),
|
||||
|
|
@ -232,12 +217,12 @@ export class AlertGroupStore extends BaseStore {
|
|||
this.setLiveUpdatesPaused(false);
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
setLiveUpdatesPaused(value: boolean) {
|
||||
this.liveUpdatesPaused = value;
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
async updateIncidentFilters(params: any, keepCursor = false) {
|
||||
if (!keepCursor) {
|
||||
this.setIncidentsCursor(undefined);
|
||||
|
|
@ -248,28 +233,28 @@ export class AlertGroupStore extends BaseStore {
|
|||
await this.updateIncidents();
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
async updateIncidentsCursor(cursor: string) {
|
||||
this.setIncidentsCursor(cursor);
|
||||
|
||||
this.updateAlertGroups();
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
async setIncidentsCursor(cursor: string) {
|
||||
this.incidentsCursor = cursor;
|
||||
|
||||
LocationHelper.update({ cursor }, 'partial');
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
async setIncidentsItemsPerPage() {
|
||||
this.setIncidentsCursor(undefined);
|
||||
|
||||
this.updateAlertGroups();
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
async updateAlertGroups() {
|
||||
this.alertGroupsLoading = true;
|
||||
|
||||
|
|
@ -313,7 +298,6 @@ export class AlertGroupStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action.bound
|
||||
getAlertSearchResult(query: string) {
|
||||
const result = this.alertsSearchResult[query];
|
||||
if (!result) {
|
||||
|
|
@ -328,7 +312,6 @@ export class AlertGroupStore extends BaseStore {
|
|||
};
|
||||
}
|
||||
|
||||
@action.bound
|
||||
async getAlert(pk: Alert['pk']) {
|
||||
return await makeRequest(`${this.path}${pk}`, {}).then((alert: Alert) => {
|
||||
runInAction(() => {
|
||||
|
|
@ -343,7 +326,7 @@ export class AlertGroupStore extends BaseStore {
|
|||
return await makeRequest(`/alerts/${pk}`, {});
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
async getNewIncidentsStats() {
|
||||
const result = await makeRequest(`${this.path}stats/`, {
|
||||
params: {
|
||||
|
|
@ -357,7 +340,7 @@ export class AlertGroupStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
async getAcknowledgedIncidentsStats() {
|
||||
const result = await makeRequest(`${this.path}stats/`, {
|
||||
params: {
|
||||
|
|
@ -371,7 +354,7 @@ export class AlertGroupStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
async getResolvedIncidentsStats() {
|
||||
const result = await makeRequest(`${this.path}stats/`, {
|
||||
params: {
|
||||
|
|
@ -385,7 +368,7 @@ export class AlertGroupStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
async getSilencedIncidentsStats() {
|
||||
const result = await makeRequest(`${this.path}stats/`, {
|
||||
params: {
|
||||
|
|
@ -399,7 +382,7 @@ export class AlertGroupStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
async doIncidentAction(alertId: Alert['pk'], action: AlertAction, isUndo = false, data?: any) {
|
||||
this.updateAlert(alertId, { loading: true });
|
||||
|
||||
|
|
@ -446,7 +429,7 @@ export class AlertGroupStore extends BaseStore {
|
|||
}
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
async updateAlert(pk: Alert['pk'], value: Partial<Alert>) {
|
||||
this.alerts.set(pk, {
|
||||
...(this.alerts.get(pk) as Alert),
|
||||
|
|
@ -454,7 +437,6 @@ export class AlertGroupStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action.bound
|
||||
async unpageUser(alertId: Alert['pk'], userId: User['pk']) {
|
||||
return await makeRequest(`${this.path}${alertId}/unpage_user`, {
|
||||
method: 'POST',
|
||||
|
|
@ -462,7 +444,7 @@ export class AlertGroupStore extends BaseStore {
|
|||
}).catch(this.onApiError);
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
async fetchTableSettings(): Promise<void> {
|
||||
const tableSettings = await makeRequest('/alertgroup_table_settings', {});
|
||||
|
||||
|
|
@ -477,7 +459,7 @@ export class AlertGroupStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
@AutoLoadingState(ActionKey.ADD_NEW_COLUMN_TO_ALERT_GROUP)
|
||||
async updateTableSettings(
|
||||
columns: { visible: AlertGroupColumn[]; hidden: AlertGroupColumn[] },
|
||||
|
|
@ -495,21 +477,18 @@ export class AlertGroupStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action.bound
|
||||
async resetTableSettings(): Promise<void> {
|
||||
return await makeRequest('/alertgroup_table_settings/reset', { method: 'POST' }).catch(() =>
|
||||
openErrorNotification('There was an error resetting the table settings')
|
||||
);
|
||||
}
|
||||
|
||||
@action.bound
|
||||
async loadLabelsKeys(): Promise<Array<ApiSchemas['LabelKey']>> {
|
||||
return await makeRequest(`/alertgroups/labels/keys/`, {}).catch(() =>
|
||||
openErrorNotification('There was an error processing your request')
|
||||
);
|
||||
}
|
||||
|
||||
@action.bound
|
||||
async loadValuesForLabelKey(
|
||||
key: ApiSchemas['LabelKey']['id'],
|
||||
search = ''
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ export class ApiTokenStore extends BaseStore {
|
|||
this.path = '/tokens/';
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
async updateItems(query = '') {
|
||||
const results = await makeRequest(`${this.path}`, {
|
||||
params: { search: query },
|
||||
|
|
@ -46,7 +46,6 @@ export class ApiTokenStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action.bound
|
||||
getSearchResult(query = '') {
|
||||
if (!this.searchResult[query]) {
|
||||
return undefined;
|
||||
|
|
@ -55,7 +54,6 @@ export class ApiTokenStore extends BaseStore {
|
|||
return this.searchResult[query].map((apiTokenId: ApiToken['id']) => this.items[apiTokenId]);
|
||||
}
|
||||
|
||||
@action.bound
|
||||
async revokeApiToken(id: ApiToken['id']) {
|
||||
return await makeRequest(`${this.path}${id}/`, {
|
||||
method: 'DELETE',
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ export default class BaseStore {
|
|||
throw error;
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
async getAll(query = '') {
|
||||
return await makeRequest(`${this.path}`, {
|
||||
params: { search: query },
|
||||
|
|
@ -50,7 +50,7 @@ export default class BaseStore {
|
|||
}).catch(this.onApiError);
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
async getById(id: string, skipErrorHandling = false, fromOrganization = false) {
|
||||
return await makeRequest(`${this.path}${id}`, {
|
||||
method: 'GET',
|
||||
|
|
@ -58,7 +58,7 @@ export default class BaseStore {
|
|||
}).catch((error) => this.onApiError(error, skipErrorHandling));
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
async create<RT = any>(data: any, skipErrorHandling = false): Promise<RT | void> {
|
||||
return await makeRequest<RT>(this.path, {
|
||||
method: 'POST',
|
||||
|
|
@ -68,7 +68,7 @@ export default class BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
async update<RT = any>(id: any, data: any, params: any = null, skipErrorHandling = false): Promise<RT | void> {
|
||||
const result = await makeRequest<RT>(`${this.path}${id}/`, {
|
||||
method: 'PUT',
|
||||
|
|
@ -83,7 +83,7 @@ export default class BaseStore {
|
|||
return result;
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
async delete(id: any) {
|
||||
const result = await makeRequest(`${this.path}${id}/`, {
|
||||
method: 'DELETE',
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ export class CloudStore extends BaseStore {
|
|||
this.path = '/cloud_users/';
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
async updateItems(page = 1) {
|
||||
const { matched_users_count, results } = await makeRequest(this.path, {
|
||||
params: { page },
|
||||
|
|
@ -49,7 +49,6 @@ export class CloudStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action.bound
|
||||
getSearchResult() {
|
||||
return {
|
||||
matched_users_count: this.searchResult.matched_users_count,
|
||||
|
|
@ -57,22 +56,18 @@ export class CloudStore extends BaseStore {
|
|||
};
|
||||
}
|
||||
|
||||
@action.bound
|
||||
async syncCloudUsers() {
|
||||
return await makeRequest(`${this.path}`, { method: 'POST' });
|
||||
}
|
||||
|
||||
@action.bound
|
||||
async syncCloudUser(id: string) {
|
||||
return await makeRequest(`${this.path}${id}/sync/`, { method: 'POST' });
|
||||
}
|
||||
|
||||
@action.bound
|
||||
async getCloudHeartbeat() {
|
||||
return await makeRequest(`/cloud_heartbeat/`, { method: 'POST' });
|
||||
}
|
||||
|
||||
@action.bound
|
||||
async getCloudUser(id: string) {
|
||||
return await makeRequest(`${this.path}${id}`, { method: 'GET' });
|
||||
}
|
||||
|
|
@ -86,12 +81,10 @@ export class CloudStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action.bound
|
||||
async getCloudConnectionStatus() {
|
||||
return await makeRequest(`/cloud_connection/`, { method: 'GET' });
|
||||
}
|
||||
|
||||
@action.bound
|
||||
async disconnectToCloud() {
|
||||
return await makeRequest(`/cloud_connection/`, { method: 'DELETE' });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ export class DirectPagingStore extends BaseStore {
|
|||
this.path = '/direct_paging/';
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
addUserToSelectedUsers = (user: UserCurrentlyOnCall) => {
|
||||
this.selectedUserResponders = [
|
||||
...this.selectedUserResponders,
|
||||
|
|
@ -40,22 +40,22 @@ export class DirectPagingStore extends BaseStore {
|
|||
];
|
||||
};
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
resetSelectedUsers = () => {
|
||||
this.selectedUserResponders = [];
|
||||
};
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
updateSelectedTeam = (team: GrafanaTeam) => {
|
||||
this.selectedTeamResponder = team;
|
||||
};
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
resetSelectedTeam = () => {
|
||||
this.selectedTeamResponder = null;
|
||||
};
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
removeSelectedUser(index: number) {
|
||||
this.selectedUserResponders = [
|
||||
...this.selectedUserResponders.slice(0, index),
|
||||
|
|
@ -63,7 +63,7 @@ export class DirectPagingStore extends BaseStore {
|
|||
];
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
updateSelectedUserImportantStatus(index: number, important: boolean) {
|
||||
this.selectedUserResponders = [
|
||||
...this.selectedUserResponders.slice(0, index),
|
||||
|
|
@ -75,7 +75,6 @@ export class DirectPagingStore extends BaseStore {
|
|||
];
|
||||
}
|
||||
|
||||
@action.bound
|
||||
async createManualAlertRule(data: ManualAlertGroupPayload): Promise<DirectPagingResponse | void> {
|
||||
return await makeRequest<DirectPagingResponse>(this.path, {
|
||||
method: 'POST',
|
||||
|
|
@ -83,7 +82,6 @@ export class DirectPagingStore extends BaseStore {
|
|||
}).catch(this.onApiError);
|
||||
}
|
||||
|
||||
@action.bound
|
||||
async updateAlertGroup(alertId: Alert['pk'], data: ManualAlertGroupPayload): Promise<DirectPagingResponse | void> {
|
||||
return await makeRequest<DirectPagingResponse>(this.path, {
|
||||
method: 'POST',
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ export class EscalationChainStore extends BaseStore {
|
|||
this.path = '/escalation_chains/';
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
async loadItem(id: EscalationChain['id'], skipErrorHandling = false): Promise<EscalationChain> {
|
||||
const escalationChain = await this.getById(id, skipErrorHandling);
|
||||
|
||||
|
|
@ -44,7 +44,7 @@ export class EscalationChainStore extends BaseStore {
|
|||
return escalationChain;
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
async updateById(id: EscalationChain['id']) {
|
||||
const response = await this.getById(id);
|
||||
|
||||
|
|
@ -56,7 +56,7 @@ export class EscalationChainStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
async save(id: EscalationChain['id'], data: Partial<EscalationChain>) {
|
||||
const response = await super.update(id, data);
|
||||
|
||||
|
|
@ -68,7 +68,7 @@ export class EscalationChainStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
async updateEscalationChainDetails(id: EscalationChain['id']) {
|
||||
const response = await makeRequest(`${this.path}${id}/details/`, {});
|
||||
|
||||
|
|
@ -80,7 +80,7 @@ export class EscalationChainStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
async updateItem(id: EscalationChain['id'], skipErrorHandling = false): Promise<EscalationChain> {
|
||||
let escalationChain;
|
||||
try {
|
||||
|
|
@ -107,7 +107,7 @@ export class EscalationChainStore extends BaseStore {
|
|||
return escalationChain;
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
async updateItems(query: any = '') {
|
||||
const params = typeof query === 'string' ? { search: query } : query;
|
||||
|
||||
|
|
@ -140,7 +140,6 @@ export class EscalationChainStore extends BaseStore {
|
|||
this.loading = false;
|
||||
}
|
||||
|
||||
@action.bound
|
||||
getSearchResult(query = '') {
|
||||
if (!this.searchResult[query]) {
|
||||
return undefined;
|
||||
|
|
@ -149,7 +148,6 @@ export class EscalationChainStore extends BaseStore {
|
|||
return this.searchResult[query].map((escalationChainId: EscalationChain['id']) => this.items[escalationChainId]);
|
||||
}
|
||||
|
||||
@action.bound
|
||||
clone = (escalationChainId: EscalationChain['id'], data: Partial<EscalationChain>): Promise<EscalationChain> =>
|
||||
makeRequest<EscalationChain>(`${this.path}${escalationChainId}/copy/`, {
|
||||
method: 'POST',
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ export class EscalationPolicyStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
async updateEscalationPolicies(escalationChainId: EscalationChain['id']) {
|
||||
const response = await makeRequest(this.path, {
|
||||
params: { escalation_chain: escalationChainId },
|
||||
|
|
@ -91,7 +91,7 @@ export class EscalationPolicyStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
createEscalationPolicy(escalationChainId: EscalationChain['id'], data: Partial<EscalationPolicy>) {
|
||||
return super.create({
|
||||
...data,
|
||||
|
|
@ -99,7 +99,7 @@ export class EscalationPolicyStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
async saveEscalationPolicy(id: EscalationPolicy['id'], data: Partial<EscalationPolicy>) {
|
||||
this.items[id] = {
|
||||
...this.items[id],
|
||||
|
|
@ -113,7 +113,7 @@ export class EscalationPolicyStore extends BaseStore {
|
|||
}
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
async moveEscalationPolicyToPosition(oldIndex: any, newIndex: any, escalationChainId: EscalationChain['id']) {
|
||||
const escalationPolicyId = this.escalationChainToEscalationPolicy[escalationChainId][oldIndex];
|
||||
|
||||
|
|
@ -130,7 +130,7 @@ export class EscalationPolicyStore extends BaseStore {
|
|||
this.updateEscalationPolicies(escalationChainId);
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
async deleteEscalationPolicy(data: Partial<EscalationPolicy>) {
|
||||
const index = this.escalationChainToEscalationPolicy[data.escalation_chain].findIndex(
|
||||
(escalationPolicyId: EscalationPolicy['id']) => escalationPolicyId === data.id
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ export class FiltersStore extends BaseStore {
|
|||
}
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
setNeedToParseFilters(value: boolean) {
|
||||
this.needToParseFilters = value;
|
||||
}
|
||||
|
|
@ -54,7 +54,7 @@ export class FiltersStore extends BaseStore {
|
|||
return this._globalValues;
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
public async updateOptionsForPage(page: string) {
|
||||
const result = await makeRequest(`/${getApiPathByPage(page)}/filters/`, {});
|
||||
|
||||
|
|
@ -73,7 +73,7 @@ export class FiltersStore extends BaseStore {
|
|||
return result;
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
updateValuesForPage(page: string, value: FiltersValues) {
|
||||
this.values = {
|
||||
...this.values,
|
||||
|
|
@ -81,12 +81,12 @@ export class FiltersStore extends BaseStore {
|
|||
};
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
setCurrentTablePageNum(page: PAGE, currentTablePageNum: number) {
|
||||
this.currentTablePageNum[page] = currentTablePageNum;
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
applyLabelFilter = (label: LabelKeyValue, page: PAGE) => {
|
||||
const currentLabelFilterValues = this.values[page]?.label || [];
|
||||
const labelToAddString = `${label.key.id}:${label.value.id}`;
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ export class GlobalSettingStore extends BaseStore {
|
|||
this.path = '/live_settings/';
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
async updateById(id: GlobalSetting['id']) {
|
||||
const response = await this.getById(id);
|
||||
|
||||
|
|
@ -32,7 +32,7 @@ export class GlobalSettingStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
async updateItems(query = '') {
|
||||
const results = await this.getAll();
|
||||
|
||||
|
|
@ -55,7 +55,6 @@ export class GlobalSettingStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action.bound
|
||||
getSearchResult(query = '') {
|
||||
if (!this.searchResult[query]) {
|
||||
return undefined;
|
||||
|
|
@ -64,7 +63,6 @@ export class GlobalSettingStore extends BaseStore {
|
|||
return this.searchResult[query].map((globalSettingId: GlobalSetting['id']) => this.items[globalSettingId]);
|
||||
}
|
||||
|
||||
@action.bound
|
||||
async getGlobalSettingItemByName(name: string) {
|
||||
const results = await this.getAll();
|
||||
return results.find((element: { name: string }) => element.name === name);
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ export class GrafanaTeamStore extends BaseStore {
|
|||
this.path = '/teams/';
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
async updateTeam(id: GrafanaTeam['id'], data: Partial<GrafanaTeam>) {
|
||||
const result = await this.update(id, data);
|
||||
|
||||
|
|
@ -61,7 +61,6 @@ export class GrafanaTeamStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action.bound
|
||||
getSearchResult() {
|
||||
return this.searchResult.map((teamId: GrafanaTeam['id']) => this.items[teamId]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ export class HeartbeatStore extends BaseStore {
|
|||
this.path = '/heartbeats/';
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
async updateTimeoutOptions() {
|
||||
const result = await makeRequest(`${this.path}timeout_options/`, {});
|
||||
|
||||
|
|
@ -31,7 +31,7 @@ export class HeartbeatStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
async saveHeartbeat(id: Heartbeat['id'], data: Partial<Heartbeat>) {
|
||||
const response = await super.update<Heartbeat>(id, data);
|
||||
|
||||
|
|
@ -47,7 +47,7 @@ export class HeartbeatStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
async createHeartbeat(alertReceiveChannelId: AlertReceiveChannel['id'], data: Partial<Heartbeat>) {
|
||||
const response = await super.create<Heartbeat>({
|
||||
alert_receive_channel: alertReceiveChannelId,
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ class LoaderStoreClass {
|
|||
makeObservable(this);
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
setLoadingAction(actionKey: string, isLoading: boolean) {
|
||||
this.items[actionKey] = isLoading;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ export class MSTeamsChannelStore extends BaseStore {
|
|||
this.path = '/msteams/channels/';
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
async updateMSTeamsChannels() {
|
||||
const response = await makeRequest(this.path, {});
|
||||
|
||||
|
|
@ -48,7 +48,7 @@ export class MSTeamsChannelStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
async updateById(id: MSTeamsChannel['id']) {
|
||||
const response = await this.getById(id);
|
||||
|
||||
|
|
@ -60,7 +60,7 @@ export class MSTeamsChannelStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
async updateItems(query = '') {
|
||||
const result = await this.getAll();
|
||||
|
||||
|
|
@ -83,7 +83,6 @@ export class MSTeamsChannelStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action.bound
|
||||
getSearchResult(query = '') {
|
||||
if (!this.searchResult[query]) {
|
||||
return undefined;
|
||||
|
|
@ -96,38 +95,31 @@ export class MSTeamsChannelStore extends BaseStore {
|
|||
return Boolean(this.getSearchResult('')?.length);
|
||||
}
|
||||
|
||||
@action.bound
|
||||
async startAutoUpdate() {
|
||||
this.autoUpdateTimer = setInterval(this.updateMSTeamsChannels.bind(this), 3000);
|
||||
}
|
||||
|
||||
@action.bound
|
||||
async stopAutoUpdate() {
|
||||
if (this.autoUpdateTimer) {
|
||||
clearInterval(this.autoUpdateTimer);
|
||||
}
|
||||
}
|
||||
|
||||
@action.bound
|
||||
async getMSTeamsChannelVerificationCode() {
|
||||
return await makeRequest(`/current_team/get_channel_verification_code/?backend=MSTEAMS`, {
|
||||
withCredentials: true,
|
||||
});
|
||||
}
|
||||
|
||||
@action.bound
|
||||
async makeMSTeamsChannelDefault(id: MSTeamsChannel['id']) {
|
||||
return makeRequest(`/msteams/channels/${id}/set_default/`, {
|
||||
method: 'POST',
|
||||
});
|
||||
}
|
||||
|
||||
@action.bound
|
||||
async deleteMSTeamsChannel(id: MSTeamsChannel['id']) {
|
||||
return super.delete(id);
|
||||
}
|
||||
|
||||
@action.bound
|
||||
async getMSTeamsChannels() {
|
||||
return super.getAll();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,7 +25,6 @@ export class OrganizationStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action.bound
|
||||
async saveCurrentOrganization(data: Partial<Organization>) {
|
||||
this.currentOrganization = await makeRequest(this.path, {
|
||||
method: 'PUT',
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ export interface Organization {
|
|||
discussion_group_name: string;
|
||||
};
|
||||
name: string;
|
||||
stack_slug: string;
|
||||
slack_team_identity: {
|
||||
general_log_channel_id: string;
|
||||
general_log_channel_pk: string;
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ export class OutgoingWebhookStore extends BaseStore {
|
|||
this.path = '/webhooks/';
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
async loadItem(id: OutgoingWebhook['id'], skipErrorHandling = false): Promise<OutgoingWebhook> {
|
||||
const outgoingWebhook = await this.getById(id, skipErrorHandling);
|
||||
|
||||
|
|
@ -42,7 +42,7 @@ export class OutgoingWebhookStore extends BaseStore {
|
|||
return outgoingWebhook;
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
async updateById(id: OutgoingWebhook['id']) {
|
||||
const response = await this.getById(id);
|
||||
|
||||
|
|
@ -54,7 +54,7 @@ export class OutgoingWebhookStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
async updateItem(id: OutgoingWebhook['id'], fromOrganization = false) {
|
||||
const response = await this.getById(id, false, fromOrganization);
|
||||
|
||||
|
|
@ -66,7 +66,7 @@ export class OutgoingWebhookStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
async updateItems(query: any = '') {
|
||||
const params = typeof query === 'string' ? { search: query } : query;
|
||||
|
||||
|
|
@ -95,7 +95,6 @@ export class OutgoingWebhookStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action.bound
|
||||
getSearchResult(query = '') {
|
||||
if (!this.searchResult[query]) {
|
||||
return undefined;
|
||||
|
|
@ -104,14 +103,12 @@ export class OutgoingWebhookStore extends BaseStore {
|
|||
return this.searchResult[query].map((outgoingWebhookId: OutgoingWebhook['id']) => this.items[outgoingWebhookId]);
|
||||
}
|
||||
|
||||
@action.bound
|
||||
async getLastResponses(id: OutgoingWebhook['id']) {
|
||||
const result = await makeRequest(`${this.path}${id}/responses`, {});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@action.bound
|
||||
async renderPreview(id: OutgoingWebhook['id'], template_name: string, template_body: string, payload) {
|
||||
return await makeRequest(`${this.path}${id}/preview_template/`, {
|
||||
method: 'POST',
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import { action, makeObservable } from 'mobx';
|
||||
|
||||
import { Alert } from 'models/alertgroup/alertgroup.types';
|
||||
import BaseStore from 'models/base_store';
|
||||
import { makeRequest } from 'network';
|
||||
|
|
@ -8,12 +6,10 @@ import { RootStore } from 'state';
|
|||
export class ResolutionNotesStore extends BaseStore {
|
||||
constructor(rootStore: RootStore) {
|
||||
super(rootStore);
|
||||
makeObservable(this);
|
||||
|
||||
this.path = '/resolution_notes/';
|
||||
}
|
||||
|
||||
@action.bound
|
||||
async createResolutionNote(alertGroupId: Alert['pk'], text: string) {
|
||||
return await makeRequest(`${this.path}`, {
|
||||
method: 'POST',
|
||||
|
|
|
|||
|
|
@ -133,7 +133,7 @@ export class ScheduleStore extends BaseStore {
|
|||
this.path = '/schedules/';
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
async loadItem(id: Schedule['id'], skipErrorHandling = false): Promise<Schedule> {
|
||||
const schedule = await this.getById(id, skipErrorHandling);
|
||||
|
||||
|
|
@ -147,7 +147,7 @@ export class ScheduleStore extends BaseStore {
|
|||
return schedule;
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
async updateItems(
|
||||
f: RemoteFiltersType | string = { searchTerm: '', type: undefined, used: undefined },
|
||||
page = 1,
|
||||
|
|
@ -182,7 +182,7 @@ export class ScheduleStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
async updateItem(id: Schedule['id'], fromOrganization = false) {
|
||||
if (id) {
|
||||
let schedule;
|
||||
|
|
@ -211,7 +211,6 @@ export class ScheduleStore extends BaseStore {
|
|||
}
|
||||
}
|
||||
|
||||
@action.bound
|
||||
getSearchResult() {
|
||||
return {
|
||||
page_size: this.searchResult.page_size,
|
||||
|
|
@ -231,28 +230,24 @@ export class ScheduleStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action.bound
|
||||
async reloadIcal(scheduleId: Schedule['id']) {
|
||||
await makeRequest(`/schedules/${scheduleId}/reload_ical/`, {
|
||||
method: 'POST',
|
||||
});
|
||||
}
|
||||
|
||||
@action.bound
|
||||
async getICalLink(scheduleId: Schedule['id']) {
|
||||
return await makeRequest(`/schedules/${scheduleId}/export_token/`, {
|
||||
method: 'GET',
|
||||
});
|
||||
}
|
||||
|
||||
@action.bound
|
||||
async createICalLink(scheduleId: Schedule['id']) {
|
||||
return await makeRequest(`/schedules/${scheduleId}/export_token/`, {
|
||||
method: 'POST',
|
||||
});
|
||||
}
|
||||
|
||||
@action.bound
|
||||
async deleteICalLink(scheduleId: Schedule['id']) {
|
||||
await makeRequest(`/schedules/${scheduleId}/export_token/`, {
|
||||
method: 'DELETE',
|
||||
|
|
@ -261,7 +256,7 @@ export class ScheduleStore extends BaseStore {
|
|||
|
||||
// ------- NEW SCHEDULES API ENDPOINTS ---------
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
async createRotation(scheduleId: Schedule['id'], isOverride: boolean, params: Partial<Shift>) {
|
||||
const type = isOverride ? 3 : 2;
|
||||
|
||||
|
|
@ -282,12 +277,10 @@ export class ScheduleStore extends BaseStore {
|
|||
return response;
|
||||
}
|
||||
|
||||
@action.bound
|
||||
setRotationFormLiveParams(params: RotationFormLiveParams) {
|
||||
this.rotationFormLiveParams = params;
|
||||
}
|
||||
|
||||
@action.bound
|
||||
async updateRotationPreview(
|
||||
scheduleId: Schedule['id'],
|
||||
shiftId: Shift['id'] | 'new',
|
||||
|
|
@ -331,7 +324,7 @@ export class ScheduleStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
async updateShiftsSwapPreview(scheduleId: Schedule['id'], startMoment: dayjs.Dayjs, params: Partial<ShiftSwap>) {
|
||||
const fromString = getFromString(startMoment);
|
||||
|
||||
|
|
@ -357,7 +350,7 @@ export class ScheduleStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
clearPreview() {
|
||||
this.finalPreview = undefined;
|
||||
this.rotationPreview = undefined;
|
||||
|
|
@ -366,7 +359,7 @@ export class ScheduleStore extends BaseStore {
|
|||
this.rotationFormLiveParams = undefined;
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
async updateRotation(shiftId: Shift['id'], params: Partial<Shift>) {
|
||||
const response = await makeRequest(`/oncall_shifts/${shiftId}`, {
|
||||
params: { force: true },
|
||||
|
|
@ -384,7 +377,7 @@ export class ScheduleStore extends BaseStore {
|
|||
return response;
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
async updateRotationAsNew(shiftId: Shift['id'], params: Partial<Shift>) {
|
||||
const response = await makeRequest(`/oncall_shifts/${shiftId}`, {
|
||||
data: { ...params },
|
||||
|
|
@ -401,7 +394,7 @@ export class ScheduleStore extends BaseStore {
|
|||
return response;
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
updateRelatedEscalationChains = async (id: Schedule['id']) => {
|
||||
const response = await makeRequest(`/schedules/${id}/related_escalation_chains`, {
|
||||
method: 'GET',
|
||||
|
|
@ -417,7 +410,7 @@ export class ScheduleStore extends BaseStore {
|
|||
return response;
|
||||
};
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
updateRelatedUsers = async (id: Schedule['id']) => {
|
||||
const { users } = await makeRequest(`/schedules/${id}/next_shifts_per_user`, {
|
||||
method: 'GET',
|
||||
|
|
@ -433,7 +426,7 @@ export class ScheduleStore extends BaseStore {
|
|||
return users;
|
||||
};
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
async updateOncallShifts(scheduleId: Schedule['id']) {
|
||||
const { results } = await makeRequest(`/oncall_shifts/`, {
|
||||
params: {
|
||||
|
|
@ -456,7 +449,7 @@ export class ScheduleStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
async updateOncallShift(shiftId: Shift['id']) {
|
||||
if (this.shiftsCurrentlyUpdating[shiftId]) {
|
||||
return;
|
||||
|
|
@ -478,7 +471,7 @@ export class ScheduleStore extends BaseStore {
|
|||
return response;
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
async saveOncallShift(shiftId: Shift['id'], data: Partial<Shift>) {
|
||||
const response = await makeRequest(`/oncall_shifts/${shiftId}`, { method: 'PUT', data });
|
||||
|
||||
|
|
@ -492,7 +485,6 @@ export class ScheduleStore extends BaseStore {
|
|||
return response;
|
||||
}
|
||||
|
||||
@action.bound
|
||||
async deleteOncallShift(shiftId: Shift['id'], force?: boolean) {
|
||||
return await makeRequest(`/oncall_shifts/${shiftId}`, {
|
||||
method: 'DELETE',
|
||||
|
|
@ -500,7 +492,7 @@ export class ScheduleStore extends BaseStore {
|
|||
}).catch(this.onApiError);
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
async updateEvents(scheduleId: Schedule['id'], startMoment: dayjs.Dayjs, type: RotationType = 'rotation', days = 9) {
|
||||
const dayBefore = startMoment.subtract(1, 'day');
|
||||
|
||||
|
|
@ -556,14 +548,13 @@ export class ScheduleStore extends BaseStore {
|
|||
]);
|
||||
}
|
||||
|
||||
@action.bound
|
||||
async updateFrequencyOptions() {
|
||||
return await makeRequest(`/oncall_shifts/frequency_options/`, {
|
||||
method: 'GET',
|
||||
});
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
async updateDaysOptions() {
|
||||
const result = await makeRequest(`/oncall_shifts/days_options/`, {
|
||||
method: 'GET',
|
||||
|
|
@ -574,22 +565,19 @@ export class ScheduleStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action.bound
|
||||
async createShiftSwap(params: Partial<ShiftSwap>) {
|
||||
return await makeRequest(`/shift_swaps/`, { method: 'POST', data: params }).catch(this.onApiError);
|
||||
}
|
||||
|
||||
@action.bound
|
||||
async deleteShiftSwap(shiftSwapId: ShiftSwap['id']) {
|
||||
return await makeRequest(`/shift_swaps/${shiftSwapId}`, { method: 'DELETE' }).catch(this.onApiError);
|
||||
}
|
||||
|
||||
@action.bound
|
||||
async takeShiftSwap(shiftSwapId: ShiftSwap['id']) {
|
||||
return await makeRequest(`/shift_swaps/${shiftSwapId}/take`, { method: 'POST' }).catch(this.onApiError);
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
async loadShiftSwap(id: ShiftSwap['id']) {
|
||||
const result = await makeRequest(`/shift_swaps/${id}`, { params: { expand_users: true } });
|
||||
|
||||
|
|
@ -600,7 +588,7 @@ export class ScheduleStore extends BaseStore {
|
|||
return result;
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
async updateShiftSwaps(scheduleId: Schedule['id'], startMoment: dayjs.Dayjs, days = 9) {
|
||||
const fromString = getFromString(startMoment);
|
||||
|
||||
|
|
@ -642,7 +630,7 @@ export class ScheduleStore extends BaseStore {
|
|||
}
|
||||
|
||||
@AutoLoadingState(ActionKey.UPDATE_PERSONAL_EVENTS)
|
||||
@action.bound
|
||||
@action
|
||||
async updatePersonalEvents(userPk: User['pk'], startMoment: dayjs.Dayjs, days = 9, isUpdateOnCallNow = false) {
|
||||
const fromString = getFromString(startMoment);
|
||||
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ export class SlackStore extends BaseStore {
|
|||
makeObservable(this);
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
async updateSlackSettings() {
|
||||
const result = await makeRequest('/slack_settings/', {});
|
||||
|
||||
|
|
@ -28,7 +28,7 @@ export class SlackStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
async saveSlackSettings(data: Partial<SlackSettings>) {
|
||||
const result = await makeRequest('/slack_settings/', {
|
||||
data,
|
||||
|
|
@ -40,7 +40,7 @@ export class SlackStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
async setGeneralLogChannelId(id: SlackChannel['id']) {
|
||||
return await makeRequest('/set_general_channel/', {
|
||||
method: 'POST',
|
||||
|
|
@ -48,7 +48,7 @@ export class SlackStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
async updateSlackIntegrationData(slack_id: string) {
|
||||
const result = await makeRequest('/slack_integration/', {
|
||||
params: { slack_id },
|
||||
|
|
@ -61,7 +61,6 @@ export class SlackStore extends BaseStore {
|
|||
return result;
|
||||
}
|
||||
|
||||
@action.bound
|
||||
async reinstallSlackIntegration(slack_id: string) {
|
||||
return await makeRequest('/slack_integration/', {
|
||||
validateStatus: function (status) {
|
||||
|
|
@ -72,19 +71,16 @@ export class SlackStore extends BaseStore {
|
|||
}).catch(this.onApiError);
|
||||
}
|
||||
|
||||
@action.bound
|
||||
async slackLogin() {
|
||||
const url_for_redirect = await makeRequest('/login/slack-login/', {});
|
||||
window.location = url_for_redirect;
|
||||
}
|
||||
|
||||
@action.bound
|
||||
async installSlackIntegration() {
|
||||
const url_for_redirect = await makeRequest('/login/slack-install-free/', {});
|
||||
window.location = url_for_redirect;
|
||||
}
|
||||
|
||||
@action.bound
|
||||
async removeSlackIntegration() {
|
||||
return await makeRequest('/slack/reset_slack/', { method: 'POST' });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ export class SlackChannelStore extends BaseStore {
|
|||
this.path = '/slack_channels/';
|
||||
}
|
||||
|
||||
@action.bound // deprecated, use updateItem instead
|
||||
@action // deprecated, use updateItem instead
|
||||
async updateById(id: SlackChannel['id']) {
|
||||
const response = await this.getById(id);
|
||||
|
||||
|
|
@ -33,7 +33,7 @@ export class SlackChannelStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
async updateItem(id: SlackChannel['id']) {
|
||||
const response = await this.getById(id);
|
||||
|
||||
|
|
@ -45,7 +45,7 @@ export class SlackChannelStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
async updateItems(query = '') {
|
||||
const { results } = await makeRequest(`${this.path}`, {
|
||||
params: { search: query },
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ export class TelegramChannelStore extends BaseStore {
|
|||
this.path = '/telegram_channels/';
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
async updateTelegramChannels() {
|
||||
const response = await makeRequest(this.path, {});
|
||||
|
||||
|
|
@ -48,7 +48,7 @@ export class TelegramChannelStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
async updateById(id: TelegramChannel['id']) {
|
||||
const response = await this.getById(id);
|
||||
|
||||
|
|
@ -60,7 +60,7 @@ export class TelegramChannelStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
async updateItems(query = '') {
|
||||
const result = await this.getAll();
|
||||
|
||||
|
|
@ -83,7 +83,6 @@ export class TelegramChannelStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action.bound
|
||||
getSearchResult(query = '') {
|
||||
if (!this.searchResult[query]) {
|
||||
return undefined;
|
||||
|
|
@ -96,38 +95,34 @@ export class TelegramChannelStore extends BaseStore {
|
|||
return Boolean(this.getSearchResult('')?.length);
|
||||
}
|
||||
|
||||
@action.bound
|
||||
async startAutoUpdate() {
|
||||
this.autoUpdateTimer = setInterval(this.updateTelegramChannels.bind(this), 3000);
|
||||
}
|
||||
|
||||
@action.bound
|
||||
async stopAutoUpdate() {
|
||||
if (this.autoUpdateTimer) {
|
||||
clearInterval(this.autoUpdateTimer);
|
||||
}
|
||||
}
|
||||
|
||||
@action.bound
|
||||
async getTelegramVerificationCode() {
|
||||
return await makeRequest(`/current_team/get_telegram_verification_code/`, {
|
||||
withCredentials: true,
|
||||
});
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
async makeTelegramChannelDefault(id: TelegramChannel['id']) {
|
||||
return makeRequest(`/telegram_channels/${id}/set_default/`, {
|
||||
method: 'POST',
|
||||
});
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
async deleteTelegramChannel(id: TelegramChannel['id']) {
|
||||
return super.delete(id);
|
||||
}
|
||||
|
||||
@action.bound
|
||||
async getTelegramChannels() {
|
||||
return super.getAll();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ describe('UserStore.unlinkBackend', () => {
|
|||
test('it makes the proper API call and returns the response', async () => {
|
||||
makeRequest.mockResolvedValueOnce('hello');
|
||||
|
||||
Object.defineProperty(userStore, 'loadCurrentUser', { value: jest.fn() });
|
||||
userStore.loadCurrentUser = jest.fn();
|
||||
|
||||
await userStore.unlinkBackend(userPk, backend);
|
||||
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ export class UserStore extends BaseStore {
|
|||
return this.items[this.currentUserPk as User['pk']];
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
async loadCurrentUser() {
|
||||
const response = await makeRequest('/user/', {});
|
||||
const timezone = await this.refreshTimezone(response.pk);
|
||||
|
|
@ -74,7 +74,7 @@ export class UserStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
async refreshTimezone(id: User['pk']) {
|
||||
const { timezone: grafanaPreferencesTimezone } = config.bootData.user;
|
||||
const timezone = grafanaPreferencesTimezone === 'browser' ? dayjs.tz.guess() : grafanaPreferencesTimezone;
|
||||
|
|
@ -87,7 +87,7 @@ export class UserStore extends BaseStore {
|
|||
return timezone;
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
async loadUser(userPk: User['pk'], skipErrorHandling = false): Promise<User> {
|
||||
const user = await this.getById(userPk, skipErrorHandling);
|
||||
|
||||
|
|
@ -101,7 +101,7 @@ export class UserStore extends BaseStore {
|
|||
return user;
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
async updateItem(userPk: User['pk']) {
|
||||
if (this.itemsCurrentlyUpdating[userPk]) {
|
||||
return;
|
||||
|
|
@ -124,7 +124,6 @@ export class UserStore extends BaseStore {
|
|||
/**
|
||||
* NOTE: if is_currently_oncall=all the backend will not paginate the results, it will send back an array of ALL users
|
||||
*/
|
||||
@action.bound
|
||||
async search<RT = PaginatedUsersResponse<User>>(f: any = { searchTerm: '' }, page = 1): Promise<RT> {
|
||||
const filters = typeof f === 'string' ? { searchTerm: f } : f; // for GSelect compatibility
|
||||
const { searchTerm: search, ...restFilters } = filters;
|
||||
|
|
@ -133,7 +132,7 @@ export class UserStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
async updateItems(f: any = { searchTerm: '' }, page = 1, invalidateFn?: () => boolean): Promise<any> {
|
||||
const response = await this.search(f, page);
|
||||
|
||||
|
|
@ -168,7 +167,6 @@ export class UserStore extends BaseStore {
|
|||
return response;
|
||||
}
|
||||
|
||||
@action.bound
|
||||
getSearchResult() {
|
||||
return {
|
||||
page_size: this.searchResult.page_size,
|
||||
|
|
@ -177,12 +175,11 @@ export class UserStore extends BaseStore {
|
|||
};
|
||||
}
|
||||
|
||||
@action.bound
|
||||
sendTelegramConfirmationCode = async (userPk: User['pk']) => {
|
||||
return await makeRequest(`/users/${userPk}/get_telegram_verification_code/`, {});
|
||||
};
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
unlinkSlack = async (userPk: User['pk']) => {
|
||||
await makeRequest(`/users/${userPk}/unlink_slack/`, {
|
||||
method: 'POST',
|
||||
|
|
@ -198,7 +195,7 @@ export class UserStore extends BaseStore {
|
|||
});
|
||||
};
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
unlinkTelegram = async (userPk: User['pk']) => {
|
||||
await makeRequest(`/users/${userPk}/unlink_telegram/`, {
|
||||
method: 'POST',
|
||||
|
|
@ -214,13 +211,12 @@ export class UserStore extends BaseStore {
|
|||
});
|
||||
};
|
||||
|
||||
@action.bound
|
||||
sendBackendConfirmationCode = (userPk: User['pk'], backend: string) =>
|
||||
makeRequest<string>(`/users/${userPk}/get_backend_verification_code?backend=${backend}`, {
|
||||
method: 'GET',
|
||||
});
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
unlinkBackend = async (userPk: User['pk'], backend: string) => {
|
||||
await makeRequest(`/users/${userPk}/unlink_backend/?backend=${backend}`, {
|
||||
method: 'POST',
|
||||
|
|
@ -229,7 +225,7 @@ export class UserStore extends BaseStore {
|
|||
this.loadCurrentUser();
|
||||
};
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
async createUser(data: any) {
|
||||
const user = await this.create(data);
|
||||
|
||||
|
|
@ -243,7 +239,7 @@ export class UserStore extends BaseStore {
|
|||
return user;
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
async updateUser(data: Partial<User>) {
|
||||
const user = await makeRequest(`/users/${data.pk}/`, {
|
||||
method: 'PUT',
|
||||
|
|
@ -265,7 +261,7 @@ export class UserStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
async updateCurrentUser(data: Partial<User>) {
|
||||
const user = await makeRequest(`/user/`, {
|
||||
method: 'PUT',
|
||||
|
|
@ -283,7 +279,7 @@ export class UserStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
async fetchVerificationCode(userPk: User['pk'], recaptchaToken: string) {
|
||||
await makeRequest(`/users/${userPk}/get_verification_code/`, {
|
||||
method: 'GET',
|
||||
|
|
@ -291,7 +287,7 @@ export class UserStore extends BaseStore {
|
|||
}).catch(throttlingError);
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
async fetchVerificationCall(userPk: User['pk'], recaptchaToken: string) {
|
||||
await makeRequest(`/users/${userPk}/get_verification_call/`, {
|
||||
method: 'GET',
|
||||
|
|
@ -299,21 +295,21 @@ export class UserStore extends BaseStore {
|
|||
}).catch(throttlingError);
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
async verifyPhone(userPk: User['pk'], token: string) {
|
||||
return await makeRequest(`/users/${userPk}/verify_number/?token=${token}`, {
|
||||
method: 'PUT',
|
||||
}).catch(throttlingError);
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
async forgetPhone(userPk: User['pk']) {
|
||||
return await makeRequest(`/users/${userPk}/forget_number/`, {
|
||||
method: 'PUT',
|
||||
});
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
async updateNotificationPolicies(id: User['pk']) {
|
||||
const importantEPs = await makeRequest('/notification_policies/', {
|
||||
params: { user: id, important: true },
|
||||
|
|
@ -331,7 +327,7 @@ export class UserStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
async moveNotificationPolicyToPosition(userPk: User['pk'], oldIndex: number, newIndex: number, offset: number) {
|
||||
const notificationPolicy = this.notificationPolicies[userPk][oldIndex + offset];
|
||||
|
||||
|
|
@ -346,7 +342,7 @@ export class UserStore extends BaseStore {
|
|||
this.updateItem(userPk); // to update notification_chain_verbal
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
async addNotificationPolicy(userPk: User['pk'], important: NotificationPolicyType['important']) {
|
||||
await makeRequest(`/notification_policies/`, {
|
||||
method: 'POST',
|
||||
|
|
@ -358,7 +354,7 @@ export class UserStore extends BaseStore {
|
|||
this.updateItem(userPk); // to update notification_chain_verbal
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
async updateNotificationPolicy(userPk: User['pk'], id: NotificationPolicyType['id'], value: NotificationPolicyType) {
|
||||
this.notificationPolicies = {
|
||||
...this.notificationPolicies,
|
||||
|
|
@ -384,7 +380,7 @@ export class UserStore extends BaseStore {
|
|||
this.updateItem(userPk); // to update notification_chain_verbal
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
async deleteNotificationPolicy(userPk: User['pk'], id: NotificationPolicyType['id']) {
|
||||
await makeRequest(`/notification_policies/${id}`, { method: 'DELETE' }).catch(this.onApiError);
|
||||
|
||||
|
|
@ -404,7 +400,7 @@ export class UserStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
async sendTestPushNotification(userId: User['pk'], isCritical: boolean) {
|
||||
return await makeRequest(`/users/${userId}/send_test_push`, {
|
||||
method: 'POST',
|
||||
|
|
@ -423,7 +419,7 @@ export class UserStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
async makeTestCall(userPk: User['pk']) {
|
||||
this.isTestCallInProgress = true;
|
||||
|
||||
|
|
@ -438,7 +434,6 @@ export class UserStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action.bound
|
||||
async sendTestSms(userPk: User['pk']) {
|
||||
this.isTestCallInProgress = true;
|
||||
|
||||
|
|
@ -451,21 +446,18 @@ export class UserStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action.bound
|
||||
async getiCalLink(userPk: User['pk']) {
|
||||
return await makeRequest(`/users/${userPk}/export_token/`, {
|
||||
method: 'GET',
|
||||
});
|
||||
}
|
||||
|
||||
@action.bound
|
||||
async createiCalLink(userPk: User['pk']) {
|
||||
return await makeRequest(`/users/${userPk}/export_token/`, {
|
||||
method: 'POST',
|
||||
});
|
||||
}
|
||||
|
||||
@action.bound
|
||||
async deleteiCalLink(userPk: User['pk']) {
|
||||
await makeRequest(`/users/${userPk}/export_token/`, {
|
||||
method: 'DELETE',
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ export class UserGroupStore extends BaseStore {
|
|||
this.path = '/user_groups/';
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
async updateItems(query = '') {
|
||||
const result = await makeRequest(`${this.path}`, {
|
||||
params: { search: query },
|
||||
|
|
@ -46,7 +46,6 @@ export class UserGroupStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action.bound
|
||||
getSearchResult(query = '') {
|
||||
if (!this.searchResult[query]) {
|
||||
return undefined;
|
||||
|
|
|
|||
|
|
@ -39,11 +39,12 @@ import getTotalAlertGroupsScene from './scenes/TotalAlertGroups';
|
|||
import getTotalAlertGroupsByStateScene from './scenes/TotalAlertGroupsByState';
|
||||
import getVariables from './variables';
|
||||
|
||||
const getDefaultStackValue = (isOpenSource: boolean) =>
|
||||
isOpenSource ? 'self_hosted_stack' : location.host.split('.')[0];
|
||||
|
||||
const Insights = observer(() => {
|
||||
const { isOpenSource, insightsDatasource } = useStore();
|
||||
const {
|
||||
isOpenSource,
|
||||
insightsDatasource,
|
||||
organizationStore: { currentOrganization },
|
||||
} = useStore();
|
||||
const [showAllStackInfo, setShowAllStackInfo] = useState(false);
|
||||
const [datasource, setDatasource] = useState<string>();
|
||||
|
||||
|
|
@ -51,7 +52,7 @@ const Insights = observer(() => {
|
|||
() => ({
|
||||
isOpenSource,
|
||||
datasource: { uid: isOpenSource ? '$datasource' : insightsDatasource },
|
||||
stack: getDefaultStackValue(isOpenSource),
|
||||
stack: currentOrganization?.stack_slug,
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
|
@ -72,8 +73,8 @@ const Insights = observer(() => {
|
|||
setDatasource(`${text}`);
|
||||
});
|
||||
return () => {
|
||||
stackListener?.unsubscribe?.();
|
||||
dataSourceListener?.unsubscribe?.();
|
||||
stackListener?.unsubscribe();
|
||||
dataSourceListener?.unsubscribe();
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
|
|
|||
|
|
@ -142,7 +142,7 @@ export class RootBaseStore {
|
|||
this.setIsBasicDataLoaded(true);
|
||||
};
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
loadMasterData = async () => {
|
||||
Promise.all([
|
||||
this.userStore.updateNotificationPolicyOptions(),
|
||||
|
|
@ -151,12 +151,12 @@ export class RootBaseStore {
|
|||
]);
|
||||
};
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
setIsBasicDataLoaded(value: boolean) {
|
||||
this.isBasicDataLoaded = value;
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
setupPluginError(errorMsg: string) {
|
||||
this.initializationError = errorMsg;
|
||||
}
|
||||
|
|
@ -327,12 +327,12 @@ export class RootBaseStore {
|
|||
this.pageTitle = title;
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
async removeSlackIntegration() {
|
||||
await this.slackStore.removeSlackIntegration();
|
||||
}
|
||||
|
||||
@action.bound
|
||||
@action
|
||||
async installSlackIntegration() {
|
||||
await this.slackStore.installSlackIntegration();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -181,7 +181,7 @@ describe('rootBaseStore', () => {
|
|||
});
|
||||
isUserActionAllowed.mockReturnValueOnce(true);
|
||||
PluginState.installPlugin = jest.fn().mockResolvedValueOnce(null);
|
||||
Object.defineProperty(rootBaseStore.userStore, 'loadCurrentUser', { value: mockedLoadCurrentUser });
|
||||
rootBaseStore.userStore.loadCurrentUser = mockedLoadCurrentUser;
|
||||
|
||||
// test
|
||||
await rootBaseStore.setupPlugin(generatePluginData(onCallApiUrl));
|
||||
|
|
@ -224,7 +224,7 @@ describe('rootBaseStore', () => {
|
|||
});
|
||||
isUserActionAllowed.mockReturnValueOnce(true);
|
||||
PluginState.installPlugin = jest.fn().mockResolvedValueOnce(null);
|
||||
Object.defineProperty(rootBaseStore.userStore, 'loadCurrentUser', { value: mockedLoadCurrentUser });
|
||||
rootBaseStore.userStore.loadCurrentUser = mockedLoadCurrentUser;
|
||||
|
||||
// test
|
||||
await rootBaseStore.setupPlugin(generatePluginData(onCallApiUrl));
|
||||
|
|
@ -300,7 +300,7 @@ describe('rootBaseStore', () => {
|
|||
version: 'asdfasdf',
|
||||
license: 'asdfasdf',
|
||||
});
|
||||
Object.defineProperty(rootBaseStore.userStore, 'loadCurrentUser', { value: mockedLoadCurrentUser });
|
||||
rootBaseStore.userStore.loadCurrentUser = mockedLoadCurrentUser;
|
||||
|
||||
// test
|
||||
await rootBaseStore.setupPlugin(generatePluginData(onCallApiUrl));
|
||||
|
|
@ -329,7 +329,7 @@ describe('rootBaseStore', () => {
|
|||
license: 'asdfasdf',
|
||||
});
|
||||
PluginState.updatePluginStatus = jest.fn().mockResolvedValueOnce(updatePluginStatusError);
|
||||
Object.defineProperty(rootBaseStore.userStore, 'loadCurrentUser', { value: mockedLoadCurrentUser });
|
||||
rootBaseStore.userStore.loadCurrentUser = mockedLoadCurrentUser;
|
||||
|
||||
// test
|
||||
await rootBaseStore.setupPlugin(generatePluginData(onCallApiUrl));
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue