Alert list view & caching rework (#216)
* remove cache usage in AlertGroupView * remove CustomSearchFilter * remove caching for alerts * remove readonly db setup * render templates on alert creation * serialize only necessary fields on alert groups list * optimize AlertGroupListSerializer * return on-demand templating for alerts * return on-demand templating for alert groups * use CursorPaginator * remove templating on alert create * pass alert to AlertGroupWebRenderer * alert_count -> alerts_count * make sql joins after pagination * add migration * bring alert.save() back * fix tests * fix tests * fix tests * add perpage query param * add cursor pagination to incidents page * remove cached_render_for_web usage * post merge fix * keep cursor * lint * remove get_alert_groups_and_days_for_previous_same_period * fix pagination on navigate * refine search_fields on AlertGroupView Co-authored-by: Maxim <hello.makson@gmail.com> Co-authored-by: Maxim <maxim.mordasov@grafana.com>
This commit is contained in:
parent
78598fb95b
commit
16bbfbbe73
33 changed files with 438 additions and 653 deletions
|
|
@ -18,9 +18,12 @@ class AlertBaseRenderer(ABC):
|
|||
|
||||
|
||||
class AlertGroupBaseRenderer(ABC):
|
||||
def __init__(self, alert_group):
|
||||
def __init__(self, alert_group, alert=None):
|
||||
if alert is None:
|
||||
alert = alert_group.alerts.first()
|
||||
|
||||
self.alert_group = alert_group
|
||||
self.alert_renderer = self.alert_renderer_class(self.alert_group.alerts.first())
|
||||
self.alert_renderer = self.alert_renderer_class(alert)
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
|
|
|
|||
|
|
@ -20,11 +20,11 @@ class AlertWebRenderer(AlertBaseRenderer):
|
|||
|
||||
|
||||
class AlertGroupWebRenderer(AlertGroupBaseRenderer):
|
||||
def __init__(self, alert_group):
|
||||
super().__init__(alert_group)
|
||||
def __init__(self, alert_group, alert=None):
|
||||
if alert is None:
|
||||
alert = alert_group.alerts.last()
|
||||
|
||||
# use the last alert to render content
|
||||
self.alert_renderer = self.alert_renderer_class(self.alert_group.alerts.last())
|
||||
super().__init__(alert_group, alert)
|
||||
|
||||
@property
|
||||
def alert_renderer_class(self):
|
||||
|
|
|
|||
21
engine/apps/alerts/migrations/0004_auto_20220711_1106.py
Normal file
21
engine/apps/alerts/migrations/0004_auto_20220711_1106.py
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
# Generated by Django 3.2.13 on 2022-07-11 11:06
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('alerts', '0003_grafanaalertingcontactpoint_datasource_uid'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='alertgroup',
|
||||
name='active_cache_for_web_calculation_id',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='alertgroup',
|
||||
name='cached_render_for_web',
|
||||
),
|
||||
]
|
||||
|
|
@ -5,7 +5,7 @@ from uuid import uuid4
|
|||
from django.apps import apps
|
||||
from django.conf import settings
|
||||
from django.core.validators import MinLengthValidator
|
||||
from django.db import models, transaction
|
||||
from django.db import models
|
||||
from django.db.models import JSONField
|
||||
from django.db.models.signals import post_save
|
||||
|
||||
|
|
@ -261,9 +261,6 @@ def listen_for_alert_model_save(sender, instance, created, *args, **kwargs):
|
|||
else:
|
||||
distribute_alert.apply_async((instance.pk,), countdown=TASK_DELAY_SECONDS)
|
||||
|
||||
logger.info(f"Recalculate AG cache. Reason: save alert model {instance.pk}")
|
||||
transaction.on_commit(instance.group.schedule_cache_for_web)
|
||||
|
||||
|
||||
# Connect signal to base Alert class
|
||||
post_save.connect(listen_for_alert_model_save, Alert)
|
||||
|
|
|
|||
|
|
@ -8,12 +8,9 @@ import pytz
|
|||
from celery import uuid as celery_uuid
|
||||
from django.apps import apps
|
||||
from django.conf import settings
|
||||
from django.core.cache import cache
|
||||
from django.core.validators import MinLengthValidator
|
||||
from django.db import IntegrityError, models, transaction
|
||||
from django.db import IntegrityError, models
|
||||
from django.db.models import JSONField, Q, QuerySet
|
||||
from django.db.models.signals import post_save
|
||||
from django.dispatch import receiver
|
||||
from django.utils import timezone
|
||||
from django.utils.functional import cached_property
|
||||
|
||||
|
|
@ -22,16 +19,9 @@ from apps.alerts.incident_appearance.renderers.constants import DEFAULT_BACKUP_T
|
|||
from apps.alerts.incident_appearance.renderers.slack_renderer import AlertGroupSlackRenderer
|
||||
from apps.alerts.incident_log_builder import IncidentLogBuilder
|
||||
from apps.alerts.signals import alert_group_action_triggered_signal
|
||||
from apps.alerts.tasks import (
|
||||
acknowledge_reminder_task,
|
||||
call_ack_url,
|
||||
schedule_cache_for_alert_group,
|
||||
send_alert_group_signal,
|
||||
unsilence_task,
|
||||
)
|
||||
from apps.alerts.tasks import acknowledge_reminder_task, call_ack_url, send_alert_group_signal, unsilence_task
|
||||
from apps.slack.slack_formatter import SlackFormatter
|
||||
from apps.user_management.models import User
|
||||
from common.mixins.use_random_readonly_db_manager_mixin import UseRandomReadonlyDbManagerMixin
|
||||
from common.public_primary_keys import generate_public_primary_key, increase_public_primary_key_length
|
||||
from common.utils import clean_markup, str_or_backup
|
||||
|
||||
|
|
@ -108,10 +98,6 @@ class UnarchivedAlertGroupQuerySet(models.QuerySet):
|
|||
return super().filter(*args, **kwargs, is_archived=False)
|
||||
|
||||
|
||||
class AlertGroupManager(UseRandomReadonlyDbManagerMixin, models.Manager):
|
||||
pass
|
||||
|
||||
|
||||
class AlertGroupSlackRenderingMixin:
|
||||
"""
|
||||
Ideally this mixin should not exist. Instead of this instance of AlertGroupSlackRenderer should be created and used
|
||||
|
|
@ -134,8 +120,8 @@ class AlertGroupSlackRenderingMixin:
|
|||
|
||||
|
||||
class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models.Model):
|
||||
all_objects = AlertGroupManager.from_queryset(AlertGroupQuerySet)()
|
||||
unarchived_objects = AlertGroupManager.from_queryset(UnarchivedAlertGroupQuerySet)()
|
||||
all_objects = AlertGroupQuerySet.as_manager()
|
||||
unarchived_objects = UnarchivedAlertGroupQuerySet.as_manager()
|
||||
|
||||
(
|
||||
NEW,
|
||||
|
|
@ -242,8 +228,6 @@ class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models.
|
|||
|
||||
active_escalation_id = models.CharField(max_length=100, null=True, default=None) # ID generated by celery
|
||||
active_resolve_calculation_id = models.CharField(max_length=100, null=True, default=None) # ID generated by celery
|
||||
# ID generated by celery
|
||||
active_cache_for_web_calculation_id = models.CharField(max_length=100, null=True, default=None)
|
||||
|
||||
SILENCE_DELAY_OPTIONS = (
|
||||
(1800, "30 minutes"),
|
||||
|
|
@ -315,8 +299,6 @@ class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models.
|
|||
related_name="dependent_alert_groups",
|
||||
)
|
||||
|
||||
cached_render_for_web = JSONField(default=dict)
|
||||
|
||||
last_unique_unacknowledge_process_id = models.CharField(max_length=100, null=True, default=None)
|
||||
is_archived = models.BooleanField(default=False)
|
||||
|
||||
|
|
@ -404,18 +386,6 @@ class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models.
|
|||
def is_alert_a_resolve_signal(self, alert):
|
||||
raise NotImplementedError
|
||||
|
||||
def cache_for_web(self, organization):
|
||||
from apps.api.serializers.alert_group import AlertGroupSerializer
|
||||
|
||||
# Re-take object to switch connection from readonly db to master.
|
||||
_self = AlertGroup.all_objects.get(pk=self.pk)
|
||||
_self.cached_render_for_web = AlertGroupSerializer(self, context={"organization": organization}).data
|
||||
self.cached_render_for_web = _self.cached_render_for_web
|
||||
_self.save(update_fields=["cached_render_for_web"])
|
||||
|
||||
def schedule_cache_for_web(self):
|
||||
schedule_cache_for_alert_group.apply_async((self.pk,))
|
||||
|
||||
@property
|
||||
def permalink(self):
|
||||
if self.slack_message is not None:
|
||||
|
|
@ -425,10 +395,6 @@ class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models.
|
|||
def web_link(self):
|
||||
return urljoin(self.channel.organization.web_link, f"?page=incident&id={self.public_primary_key}")
|
||||
|
||||
@property
|
||||
def alerts_count(self):
|
||||
return self.alerts.count()
|
||||
|
||||
@property
|
||||
def happened_while_maintenance(self):
|
||||
return self.root_alert_group is not None and self.root_alert_group.maintenance_uuid is not None
|
||||
|
|
@ -449,10 +415,6 @@ class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models.
|
|||
self.unresolve()
|
||||
self.log_records.create(type=AlertGroupLogRecord.TYPE_UN_RESOLVED, author=user, reason="Acknowledge button")
|
||||
|
||||
# clear resolve report cache
|
||||
cache_key = "render_after_resolve_report_json_{}".format(self.pk)
|
||||
cache.delete(cache_key)
|
||||
|
||||
self.acknowledge(acknowledged_by_user=user, acknowledged_by=AlertGroup.USER)
|
||||
self.stop_escalation()
|
||||
if self.is_root_alert_group:
|
||||
|
|
@ -673,9 +635,6 @@ class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models.
|
|||
self.unresolve()
|
||||
log_record = self.log_records.create(type=AlertGroupLogRecord.TYPE_UN_RESOLVED, author=user)
|
||||
|
||||
# clear resolve report cache
|
||||
self.drop_cached_after_resolve_report_json()
|
||||
|
||||
if self.is_root_alert_group:
|
||||
self.start_escalation_if_needed()
|
||||
|
||||
|
|
@ -848,10 +807,6 @@ class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models.
|
|||
self.unresolve()
|
||||
self.log_records.create(type=AlertGroupLogRecord.TYPE_UN_RESOLVED, author=user, reason="Silence button")
|
||||
|
||||
# clear resolve report cache
|
||||
cache_key = "render_after_resolve_report_json_{}".format(self.pk)
|
||||
cache.delete(cache_key)
|
||||
|
||||
if self.acknowledged:
|
||||
self.unacknowledge()
|
||||
self.log_records.create(type=AlertGroupLogRecord.TYPE_UN_ACK, author=user, reason="Silence button")
|
||||
|
|
@ -1060,8 +1015,6 @@ class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models.
|
|||
author=user,
|
||||
reason="Bulk action acknowledge",
|
||||
)
|
||||
# clear resolve report cache
|
||||
alert_group.drop_cached_after_resolve_report_json()
|
||||
|
||||
for alert_group in alert_groups_to_unsilence_before_acknowledge_list:
|
||||
alert_group.log_records.create(
|
||||
|
|
@ -1194,8 +1147,6 @@ class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models.
|
|||
reason="Bulk action restart",
|
||||
)
|
||||
|
||||
alert_group.drop_cached_after_resolve_report_json()
|
||||
|
||||
if alert_group.is_root_alert_group:
|
||||
alert_group.start_escalation_if_needed()
|
||||
|
||||
|
|
@ -1293,7 +1244,6 @@ class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models.
|
|||
author=user,
|
||||
reason="Bulk action silence",
|
||||
)
|
||||
alert_group.drop_cached_after_resolve_report_json()
|
||||
|
||||
for alert_group in alert_groups_to_unsilence_before_silence_list:
|
||||
alert_group.log_records.create(
|
||||
|
|
@ -1483,7 +1433,7 @@ class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models.
|
|||
else:
|
||||
return "Acknowledged"
|
||||
|
||||
def non_cached_after_resolve_report_json(self):
|
||||
def render_after_resolve_report_json(self):
|
||||
AlertGroupLogRecord = apps.get_model("alerts", "AlertGroupLogRecord")
|
||||
UserNotificationPolicyLogRecord = apps.get_model("base", "UserNotificationPolicyLogRecord")
|
||||
ResolutionNote = apps.get_model("alerts", "ResolutionNote")
|
||||
|
|
@ -1501,21 +1451,6 @@ class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models.
|
|||
result_log_report.append(log_record.render_log_line_json())
|
||||
return result_log_report
|
||||
|
||||
def render_after_resolve_report_json(self):
|
||||
cache_key = "render_after_resolve_report_json_{}".format(self.pk)
|
||||
|
||||
# cache.get_or_set in some cases returns None, so use get and set cache methods separately
|
||||
log_report = cache.get(cache_key)
|
||||
if log_report is None:
|
||||
log_report = self.non_cached_after_resolve_report_json()
|
||||
cache.set(cache_key, log_report)
|
||||
return log_report
|
||||
|
||||
def drop_cached_after_resolve_report_json(self):
|
||||
cache_key = "render_after_resolve_report_json_{}".format(self.pk)
|
||||
if cache_key in cache:
|
||||
cache.delete(cache_key)
|
||||
|
||||
@property
|
||||
def has_resolution_notes(self):
|
||||
return self.resolution_notes.exists()
|
||||
|
|
@ -1595,14 +1530,3 @@ class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models.
|
|||
)
|
||||
|
||||
return stop_escalation_log
|
||||
|
||||
|
||||
@receiver(post_save, sender=AlertGroup)
|
||||
def listen_for_alert_group_model_save(sender, instance, created, *args, **kwargs):
|
||||
if (
|
||||
kwargs is not None
|
||||
and "update_fields" in kwargs
|
||||
and kwargs["update_fields"] is dict
|
||||
and "cached_render_for_web" not in kwargs["update_fields"]
|
||||
):
|
||||
transaction.on_commit(instance.schedule_cache_for_alert_group)
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import logging
|
|||
|
||||
import humanize
|
||||
from django.apps import apps
|
||||
from django.db import models, transaction
|
||||
from django.db import models
|
||||
from django.db.models import JSONField
|
||||
from django.db.models.signals import post_save
|
||||
from django.dispatch import receiver
|
||||
|
|
@ -546,7 +546,6 @@ class AlertGroupLogRecord(models.Model):
|
|||
|
||||
@receiver(post_save, sender=AlertGroupLogRecord)
|
||||
def listen_for_alertgrouplogrecord(sender, instance, created, *args, **kwargs):
|
||||
instance.alert_group.drop_cached_after_resolve_report_json()
|
||||
if instance.type != AlertGroupLogRecord.TYPE_DELETED:
|
||||
if not instance.alert_group.is_maintenance_incident:
|
||||
alert_group_pk = instance.alert_group.pk
|
||||
|
|
@ -555,6 +554,3 @@ def listen_for_alertgrouplogrecord(sender, instance, created, *args, **kwargs):
|
|||
f"alert group event: {instance.get_type_display()}"
|
||||
)
|
||||
send_update_log_report_signal.apply_async(kwargs={"alert_group_pk": alert_group_pk}, countdown=8)
|
||||
|
||||
logger.info(f"Recalculate AG cache. Reason: save alert_group_log_record model {instance.pk}")
|
||||
transaction.on_commit(instance.alert_group.schedule_cache_for_web)
|
||||
|
|
|
|||
|
|
@ -19,11 +19,7 @@ from jinja2 import Template
|
|||
from apps.alerts.grafana_alerting_sync_manager.grafana_alerting_sync import GrafanaAlertingSyncManager
|
||||
from apps.alerts.integration_options_mixin import IntegrationOptionsMixin
|
||||
from apps.alerts.models.maintainable_object import MaintainableObject
|
||||
from apps.alerts.tasks import (
|
||||
disable_maintenance,
|
||||
invalidate_web_cache_for_alert_group,
|
||||
sync_grafana_alerting_contact_points,
|
||||
)
|
||||
from apps.alerts.tasks import disable_maintenance, sync_grafana_alerting_contact_points
|
||||
from apps.base.messaging import get_messaging_backend_from_id
|
||||
from apps.base.utils import live_settings
|
||||
from apps.integrations.metadata import heartbeat
|
||||
|
|
@ -693,16 +689,6 @@ def listen_for_alertreceivechannel_model_save(sender, instance, created, *args,
|
|||
create_organization_log(
|
||||
instance.organization, None, OrganizationLogType.TYPE_HEARTBEAT_CREATED, description
|
||||
)
|
||||
else:
|
||||
logger.info(f"Drop AG cache. Reason: save alert_receive_channel {instance.pk}")
|
||||
if kwargs is not None:
|
||||
if "update_fields" in kwargs:
|
||||
if kwargs["update_fields"] is not None:
|
||||
# Hack to not to invalidate web cache on AlertReceiveChannel.start_send_rate_limit_message_task
|
||||
if "rate_limit_message_task_id" in kwargs["update_fields"]:
|
||||
return
|
||||
|
||||
invalidate_web_cache_for_alert_group.apply_async(kwargs={"channel_pk": instance.pk})
|
||||
|
||||
if instance.integration == AlertReceiveChannel.INTEGRATION_GRAFANA_ALERTING:
|
||||
if created:
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ from .custom_button_result import custom_button_result # noqa: F401
|
|||
from .delete_alert_group import delete_alert_group # noqa: F401
|
||||
from .distribute_alert import distribute_alert # noqa: F401
|
||||
from .escalate_alert_group import escalate_alert_group # noqa: F401
|
||||
from .invalidate_web_cache_for_alert_group import invalidate_web_cache_for_alert_group # noqa: F401
|
||||
from .invalidate_web_cache_for_alert_group import invalidate_web_cache_for_alert_group # noqa: F401, todo: remove
|
||||
from .invite_user_to_join_incident import invite_user_to_join_incident # noqa: F401
|
||||
from .maintenance import disable_maintenance # noqa: F401
|
||||
from .notify_all import notify_all_task # noqa: F401
|
||||
|
|
|
|||
|
|
@ -1,54 +1,19 @@
|
|||
from celery.utils.log import get_task_logger
|
||||
from django.apps import apps
|
||||
from django.conf import settings
|
||||
from django.core.cache import cache
|
||||
|
||||
from common.custom_celery_tasks import shared_dedicated_queue_retry_task
|
||||
|
||||
logger = get_task_logger(__name__)
|
||||
|
||||
|
||||
def get_cache_key_caching_alert_group_for_web(alert_group_pk):
|
||||
CACHE_KEY_PREFIX = "cache_alert_group_for_web"
|
||||
return f"{CACHE_KEY_PREFIX}_{alert_group_pk}"
|
||||
|
||||
|
||||
@shared_dedicated_queue_retry_task(
|
||||
autoretry_for=(Exception,), retry_backoff=True, max_retries=0 if settings.DEBUG else None
|
||||
)
|
||||
def schedule_cache_for_alert_group(alert_group_pk):
|
||||
CACHE_FOR_ALERT_GROUP_LIFETIME = 60
|
||||
START_CACHE_DELAY = 5 # we introduce delay to avoid recaching after each alert.
|
||||
|
||||
task = cache_alert_group_for_web.apply_async(args=[alert_group_pk], countdown=START_CACHE_DELAY)
|
||||
cache_key = get_cache_key_caching_alert_group_for_web(alert_group_pk)
|
||||
cache.set(cache_key, task.id, timeout=CACHE_FOR_ALERT_GROUP_LIFETIME)
|
||||
# todo: remove
|
||||
pass
|
||||
|
||||
|
||||
@shared_dedicated_queue_retry_task(
|
||||
autoretry_for=(Exception,), retry_backoff=True, max_retries=0 if settings.DEBUG else None
|
||||
)
|
||||
def cache_alert_group_for_web(alert_group_pk):
|
||||
"""
|
||||
Async task to re-cache alert_group for web.
|
||||
"""
|
||||
cache_key = get_cache_key_caching_alert_group_for_web(alert_group_pk)
|
||||
cached_task_id = cache.get(cache_key)
|
||||
current_task_id = cache_alert_group_for_web.request.id
|
||||
|
||||
if cached_task_id is None:
|
||||
return (
|
||||
f"cache_alert_group_for_web skipped, because of current task_id ({current_task_id})"
|
||||
f" for alert_group {alert_group_pk} doesn't exist in cache, which means this task is not"
|
||||
f" relevant: cache was dropped by engine restart ot CACHE_FOR_ALERT_GROUP_LIFETIME"
|
||||
)
|
||||
if not current_task_id == cached_task_id or cached_task_id is None:
|
||||
return (
|
||||
f"cache_alert_group_for_web skipped, because of current task_id ({current_task_id})"
|
||||
f" doesn't equal to cached task_id ({cached_task_id}) for alert_group {alert_group_pk},"
|
||||
)
|
||||
else:
|
||||
AlertGroup = apps.get_model("alerts", "AlertGroup")
|
||||
alert_group = AlertGroup.all_objects.using_readonly_db.get(pk=alert_group_pk)
|
||||
alert_group.cache_for_web(alert_group.channel.organization)
|
||||
logger.info(f"cache_alert_group_for_web: cache refreshed for alert_group {alert_group_pk}")
|
||||
# todo: remove
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -1,32 +1,11 @@
|
|||
from django.apps import apps
|
||||
from django.conf import settings
|
||||
|
||||
from common.custom_celery_tasks import shared_dedicated_queue_retry_task
|
||||
|
||||
from .task_logger import task_logger
|
||||
|
||||
|
||||
@shared_dedicated_queue_retry_task(
|
||||
autoretry_for=(Exception,), retry_backoff=True, max_retries=1 if settings.DEBUG else None
|
||||
)
|
||||
def invalidate_web_cache_for_alert_group(org_pk=None, channel_pk=None, alert_group_pk=None, alert_group_pks=None):
|
||||
AlertGroup = apps.get_model("alerts", "AlertGroup")
|
||||
DynamicSetting = apps.get_model("base", "DynamicSetting")
|
||||
|
||||
if channel_pk:
|
||||
task_logger.debug(f"invalidate_web_cache_for_alert_group: Reason - alert_receive_channel {channel_pk}")
|
||||
q = AlertGroup.all_objects.filter(channel__pk=channel_pk)
|
||||
elif org_pk:
|
||||
task_logger.debug(f"invalidate_web_cache_for_alert_group: Reason - organization {org_pk}")
|
||||
q = AlertGroup.all_objects.filter(channel__organization__pk=org_pk)
|
||||
elif alert_group_pk:
|
||||
task_logger.debug(f"invalidate_web_cache_for_alert_group: Reason - alert_group {alert_group_pk}")
|
||||
q = AlertGroup.all_objects.filter(pk=alert_group_pk)
|
||||
elif alert_group_pks:
|
||||
task_logger.debug(f"invalidate_web_cache_for_alert_group: Reason - alert_groups {alert_group_pks}")
|
||||
q = AlertGroup.all_objects.filter(pk__in=alert_group_pks)
|
||||
|
||||
skip_task = DynamicSetting.objects.get_or_create(name="skip_invalidate_web_cache_for_alert_group")[0]
|
||||
if skip_task.boolean_value:
|
||||
return "Task has been skipped because of skip_invalidate_web_cache_for_alert_group DynamicSetting"
|
||||
q.update(cached_render_for_web={})
|
||||
# todo: remove
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
import logging
|
||||
from datetime import datetime
|
||||
|
||||
import humanize
|
||||
from rest_framework import serializers
|
||||
|
||||
from apps.alerts.incident_appearance.renderers.web_renderer import AlertGroupWebRenderer
|
||||
|
|
@ -28,50 +26,30 @@ class ShortAlertGroupSerializer(serializers.ModelSerializer):
|
|||
return AlertGroupWebRenderer(obj).render()
|
||||
|
||||
|
||||
class AlertGroupSerializer(EagerLoadingMixin, serializers.ModelSerializer):
|
||||
"""
|
||||
Attention: It's heavily cached. Make sure to invalidate alertgroup's web cache if you update the format!
|
||||
"""
|
||||
|
||||
class AlertGroupListSerializer(EagerLoadingMixin, serializers.ModelSerializer):
|
||||
pk = serializers.CharField(read_only=True, source="public_primary_key")
|
||||
alert_receive_channel = FastAlertReceiveChannelSerializer(source="channel")
|
||||
alerts = serializers.SerializerMethodField("get_limited_alerts")
|
||||
resolved_by_verbose = serializers.CharField(source="get_resolved_by_display")
|
||||
status = serializers.ReadOnlyField()
|
||||
resolved_by_user = FastUserSerializer(required=False)
|
||||
acknowledged_by_user = FastUserSerializer(required=False)
|
||||
silenced_by_user = FastUserSerializer(required=False)
|
||||
related_users = serializers.SerializerMethodField()
|
||||
|
||||
last_alert_at = serializers.SerializerMethodField()
|
||||
|
||||
started_at_verbose = serializers.SerializerMethodField()
|
||||
acknowledged_at_verbose = serializers.SerializerMethodField()
|
||||
resolved_at_verbose = serializers.SerializerMethodField()
|
||||
silenced_at_verbose = serializers.SerializerMethodField()
|
||||
|
||||
dependent_alert_groups = ShortAlertGroupSerializer(many=True)
|
||||
root_alert_group = ShortAlertGroupSerializer()
|
||||
|
||||
alerts_count = serializers.ReadOnlyField()
|
||||
|
||||
status = serializers.ReadOnlyField()
|
||||
alerts_count = serializers.IntegerField(read_only=True)
|
||||
render_for_web = serializers.SerializerMethodField()
|
||||
|
||||
PREFETCH_RELATED = [
|
||||
"alerts",
|
||||
"dependent_alert_groups",
|
||||
"log_records",
|
||||
"log_records__author",
|
||||
"log_records__escalation_policy",
|
||||
"log_records__invitation__invitee",
|
||||
]
|
||||
|
||||
SELECT_RELATED = [
|
||||
"slack_message",
|
||||
"channel__organization",
|
||||
"slack_message___slack_team_identity",
|
||||
"acknowledged_by_user",
|
||||
"root_alert_group",
|
||||
"resolved_by_user",
|
||||
"acknowledged_by_user",
|
||||
"silenced_by_user",
|
||||
]
|
||||
|
||||
|
|
@ -85,7 +63,6 @@ class AlertGroupSerializer(EagerLoadingMixin, serializers.ModelSerializer):
|
|||
"alert_receive_channel",
|
||||
"resolved",
|
||||
"resolved_by",
|
||||
"resolved_by_verbose",
|
||||
"resolved_by_user",
|
||||
"resolved_at",
|
||||
"acknowledged_at",
|
||||
|
|
@ -96,44 +73,18 @@ class AlertGroupSerializer(EagerLoadingMixin, serializers.ModelSerializer):
|
|||
"silenced",
|
||||
"silenced_by_user",
|
||||
"silenced_at",
|
||||
"silenced_at_verbose",
|
||||
"silenced_until",
|
||||
"started_at",
|
||||
"last_alert_at",
|
||||
"silenced_until",
|
||||
"permalink",
|
||||
"alerts",
|
||||
"related_users",
|
||||
"started_at_verbose",
|
||||
"acknowledged_at_verbose",
|
||||
"resolved_at_verbose",
|
||||
"render_for_web",
|
||||
"render_after_resolve_report_json",
|
||||
"dependent_alert_groups",
|
||||
"root_alert_group",
|
||||
"status",
|
||||
]
|
||||
|
||||
def get_last_alert_at(self, obj):
|
||||
last_alert = obj.alerts.last()
|
||||
# TODO: This is a Hotfix for 0.0.27
|
||||
if last_alert is None:
|
||||
logger.warning(f"obj {obj} doesn't have last_alert!")
|
||||
return ""
|
||||
return str(last_alert.created_at)
|
||||
|
||||
def get_limited_alerts(self, obj):
|
||||
"""
|
||||
Overriding default alerts because there are alert_groups with thousands of them.
|
||||
It's just too slow, we need to cut here.
|
||||
"""
|
||||
alerts = obj.alerts.all()[:100]
|
||||
|
||||
if len(alerts) > 90:
|
||||
for alert in alerts:
|
||||
alert.title = str(alert.title) + " Only last 100 alerts are shown. Use Amixr API to fetch all of them."
|
||||
|
||||
return AlertSerializer(alerts, many=True).data
|
||||
def get_render_for_web(self, obj):
|
||||
return AlertGroupWebRenderer(obj, obj.last_alert).render()
|
||||
|
||||
def get_related_users(self, obj):
|
||||
users_ids = set()
|
||||
|
|
@ -159,37 +110,39 @@ class AlertGroupSerializer(EagerLoadingMixin, serializers.ModelSerializer):
|
|||
users_ids.add(log_record.author.public_primary_key)
|
||||
return users
|
||||
|
||||
def get_started_at_verbose(self, obj):
|
||||
started_at_verbose = None
|
||||
if obj.started_at is not None:
|
||||
started_at_verbose = humanize.naturaltime(
|
||||
datetime.now().replace(tzinfo=None) - obj.started_at.replace(tzinfo=None)
|
||||
)
|
||||
return started_at_verbose
|
||||
|
||||
def get_acknowledged_at_verbose(self, obj):
|
||||
acknowledged_at_verbose = None
|
||||
if obj.acknowledged_at is not None:
|
||||
acknowledged_at_verbose = humanize.naturaltime(
|
||||
datetime.now().replace(tzinfo=None) - obj.acknowledged_at.replace(tzinfo=None)
|
||||
) # TODO: Deal with timezones
|
||||
return acknowledged_at_verbose
|
||||
class AlertGroupSerializer(AlertGroupListSerializer):
|
||||
alerts = serializers.SerializerMethodField("get_limited_alerts")
|
||||
last_alert_at = serializers.SerializerMethodField()
|
||||
|
||||
def get_resolved_at_verbose(self, obj):
|
||||
resolved_at_verbose = None
|
||||
if obj.resolved_at is not None:
|
||||
resolved_at_verbose = humanize.naturaltime(
|
||||
datetime.now().replace(tzinfo=None) - obj.resolved_at.replace(tzinfo=None)
|
||||
) # TODO: Deal with timezones
|
||||
return resolved_at_verbose
|
||||
|
||||
def get_silenced_at_verbose(self, obj):
|
||||
silenced_at_verbose = None
|
||||
if obj.silenced_at is not None:
|
||||
silenced_at_verbose = humanize.naturaltime(
|
||||
datetime.now().replace(tzinfo=None) - obj.silenced_at.replace(tzinfo=None)
|
||||
) # TODO: Deal with timezones
|
||||
return silenced_at_verbose
|
||||
class Meta(AlertGroupListSerializer.Meta):
|
||||
fields = AlertGroupListSerializer.Meta.fields + [
|
||||
"alerts",
|
||||
"render_after_resolve_report_json",
|
||||
"permalink",
|
||||
"last_alert_at",
|
||||
]
|
||||
|
||||
def get_render_for_web(self, obj):
|
||||
return AlertGroupWebRenderer(obj).render()
|
||||
|
||||
def get_last_alert_at(self, obj):
|
||||
last_alert = obj.alerts.last()
|
||||
|
||||
if not last_alert:
|
||||
return obj.started_at
|
||||
|
||||
return last_alert.created_at
|
||||
|
||||
def get_limited_alerts(self, obj):
|
||||
"""
|
||||
Overriding default alerts because there are alert_groups with thousands of them.
|
||||
It's just too slow, we need to cut here.
|
||||
"""
|
||||
alerts = obj.alerts.all()[:100]
|
||||
|
||||
if len(alerts) > 90:
|
||||
for alert in alerts:
|
||||
alert.title = str(alert.title) + " Only last 100 alerts are shown. Use OnCall API to fetch all of them."
|
||||
|
||||
return AlertSerializer(alerts, many=True).data
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
from rest_framework import serializers
|
||||
|
||||
from apps.alerts.models import AlertGroup, ResolutionNote
|
||||
from apps.alerts.tasks import invalidate_web_cache_for_alert_group
|
||||
from apps.api.serializers.user import FastUserSerializer
|
||||
from common.api_helpers.custom_fields import OrganizationFilteredPrimaryKeyRelatedField
|
||||
from common.api_helpers.exceptions import BadRequest
|
||||
|
|
@ -39,9 +38,6 @@ class ResolutionNoteSerializer(EagerLoadingMixin, serializers.ModelSerializer):
|
|||
validated_data["author"] = self.context["request"].user
|
||||
validated_data["source"] = ResolutionNote.Source.WEB
|
||||
created_instance = super().create(validated_data)
|
||||
# Invalidate alert group cache because resolution notes shown in alert group's timeline
|
||||
created_instance.alert_group.drop_cached_after_resolve_report_json()
|
||||
invalidate_web_cache_for_alert_group(alert_group_pk=created_instance.alert_group.pk)
|
||||
return created_instance
|
||||
|
||||
def to_representation(self, instance):
|
||||
|
|
@ -57,8 +53,5 @@ class ResolutionNoteUpdateSerializer(ResolutionNoteSerializer):
|
|||
def update(self, instance, validated_data):
|
||||
if instance.source != ResolutionNote.Source.WEB:
|
||||
raise BadRequest(detail="Cannot update message with this source type")
|
||||
updated_instance = super().update(instance, validated_data)
|
||||
# Invalidate alert group cache because resolution notes shown in alert group's timeline
|
||||
updated_instance.alert_group.drop_cached_after_resolve_report_json()
|
||||
invalidate_web_cache_for_alert_group(alert_group_pk=updated_instance.alert_group.pk)
|
||||
return updated_instance
|
||||
|
||||
return super().update(instance, validated_data)
|
||||
|
|
|
|||
|
|
@ -1,55 +0,0 @@
|
|||
from celery.utils.log import get_task_logger
|
||||
from django.apps import apps
|
||||
from django.conf import settings
|
||||
from django.core.cache import cache
|
||||
|
||||
from common.custom_celery_tasks import shared_dedicated_queue_retry_task
|
||||
|
||||
logger = get_task_logger(__name__)
|
||||
|
||||
|
||||
def get_cache_key_caching_alert_group_for_web(alert_group_pk):
|
||||
CACHE_KEY_PREFIX = "cache_alert_group_for_web"
|
||||
return f"{CACHE_KEY_PREFIX}_{alert_group_pk}"
|
||||
|
||||
|
||||
# TODO: remove this tasks after all of them will be processed in prod
|
||||
@shared_dedicated_queue_retry_task(
|
||||
autoretry_for=(Exception,), retry_backoff=True, max_retries=0 if settings.DEBUG else None
|
||||
)
|
||||
def schedule_cache_for_alert_group(alert_group_pk):
|
||||
CACHE_FOR_ALERT_GROUP_LIFETIME = 60
|
||||
START_CACHE_DELAY = 5 # we introduce delay to avoid recaching after each alert.
|
||||
|
||||
task = cache_alert_group_for_web.apply_async(args=[alert_group_pk], countdown=START_CACHE_DELAY)
|
||||
cache_key = get_cache_key_caching_alert_group_for_web(alert_group_pk)
|
||||
cache.set(cache_key, task.id, timeout=CACHE_FOR_ALERT_GROUP_LIFETIME)
|
||||
|
||||
|
||||
@shared_dedicated_queue_retry_task(
|
||||
autoretry_for=(Exception,), retry_backoff=True, max_retries=0 if settings.DEBUG else None
|
||||
)
|
||||
def cache_alert_group_for_web(alert_group_pk):
|
||||
"""
|
||||
Async task to re-cache alert_group for web.
|
||||
"""
|
||||
cache_key = get_cache_key_caching_alert_group_for_web(alert_group_pk)
|
||||
cached_task_id = cache.get(cache_key)
|
||||
current_task_id = cache_alert_group_for_web.request.id
|
||||
|
||||
if cached_task_id is None:
|
||||
return (
|
||||
f"cache_alert_group_for_web skipped, because of current task_id ({current_task_id})"
|
||||
f" for alert_group {alert_group_pk} doesn't exist in cache, which means this task is not"
|
||||
f" relevant: cache was dropped by engine restart ot CACHE_FOR_ALERT_GROUP_LIFETIME"
|
||||
)
|
||||
if not current_task_id == cached_task_id or cached_task_id is None:
|
||||
return (
|
||||
f"cache_alert_group_for_web skipped, because of current task_id ({current_task_id})"
|
||||
f" doesn't equal to cached task_id ({cached_task_id}) for alert_group {alert_group_pk},"
|
||||
)
|
||||
else:
|
||||
AlertGroup = apps.get_model("alerts", "AlertGroup")
|
||||
alert_group = AlertGroup.all_objects.using_readonly_db.get(pk=alert_group_pk)
|
||||
alert_group.cache_for_web(alert_group.channel.organization)
|
||||
logger.info(f"cache_alert_group_for_web: cache refreshed for alert_group {alert_group_pk}")
|
||||
|
|
@ -63,7 +63,7 @@ def test_get_filter_started_at(alert_group_internal_api_setup, make_user_auth_he
|
|||
)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.data["count"] == 4
|
||||
assert len(response.data["results"]) == 4
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
|
|
@ -78,7 +78,7 @@ def test_get_filter_resolved_at_alertgroup_empty_result(alert_group_internal_api
|
|||
**make_user_auth_headers(user, token),
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.data["count"] == 0
|
||||
assert len(response.data["results"]) == 0
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
|
|
@ -105,7 +105,7 @@ def test_get_filter_resolved_at(alert_group_internal_api_setup, make_user_auth_h
|
|||
**make_user_auth_headers(user, token),
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.data["count"] == 1
|
||||
assert len(response.data["results"]) == 1
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
|
|
@ -117,7 +117,7 @@ def test_status_new(alert_group_internal_api_setup, make_user_auth_headers):
|
|||
url = reverse("api-internal:alertgroup-list")
|
||||
response = client.get(url + "?status=0", format="json", **make_user_auth_headers(user, token))
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.data["count"] == 1
|
||||
assert len(response.data["results"]) == 1
|
||||
assert response.data["results"][0]["pk"] == new_alert_group.public_primary_key
|
||||
|
||||
|
||||
|
|
@ -130,7 +130,7 @@ def test_status_ack(alert_group_internal_api_setup, make_user_auth_headers):
|
|||
url = reverse("api-internal:alertgroup-list")
|
||||
response = client.get(url + "?status=1", format="json", **make_user_auth_headers(user, token))
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.data["count"] == 1
|
||||
assert len(response.data["results"]) == 1
|
||||
assert response.data["results"][0]["pk"] == ack_alert_group.public_primary_key
|
||||
|
||||
|
||||
|
|
@ -143,7 +143,7 @@ def test_status_resolved(alert_group_internal_api_setup, make_user_auth_headers)
|
|||
url = reverse("api-internal:alertgroup-list")
|
||||
response = client.get(url + "?status=2", format="json", **make_user_auth_headers(user, token))
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.data["count"] == 1
|
||||
assert len(response.data["results"]) == 1
|
||||
assert response.data["results"][0]["pk"] == resolved_alert_group.public_primary_key
|
||||
|
||||
|
||||
|
|
@ -156,7 +156,7 @@ def test_status_silenced(alert_group_internal_api_setup, make_user_auth_headers)
|
|||
url = reverse("api-internal:alertgroup-list")
|
||||
response = client.get(url + "?status=3", format="json", **make_user_auth_headers(user, token))
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.data["count"] == 1
|
||||
assert len(response.data["results"]) == 1
|
||||
assert response.data["results"][0]["pk"] == silenced_alert_group.public_primary_key
|
||||
|
||||
|
||||
|
|
@ -171,7 +171,7 @@ def test_all_statuses(alert_group_internal_api_setup, make_user_auth_headers):
|
|||
url + "?status=0&status=1&&status=2&status=3", format="json", **make_user_auth_headers(user, token)
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.data["count"] == 4
|
||||
assert len(response.data["results"]) == 4
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
|
|
@ -213,7 +213,7 @@ def test_get_filter_resolved_by(
|
|||
**make_user_auth_headers(first_user, token),
|
||||
)
|
||||
assert first_response.status_code == status.HTTP_200_OK
|
||||
assert first_response.data["count"] == 1
|
||||
assert len(first_response.data["results"]) == 1
|
||||
|
||||
second_response = client.get(
|
||||
url + f"?resolved_by={second_user.public_primary_key}",
|
||||
|
|
@ -221,7 +221,7 @@ def test_get_filter_resolved_by(
|
|||
**make_user_auth_headers(first_user, token),
|
||||
)
|
||||
assert second_response.status_code == status.HTTP_200_OK
|
||||
assert second_response.data["count"] == 0
|
||||
assert len(second_response.data["results"]) == 0
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
|
|
@ -269,7 +269,7 @@ def test_get_filter_resolved_by_multiple_values(
|
|||
**make_user_auth_headers(first_user, token),
|
||||
)
|
||||
assert first_response.status_code == status.HTTP_200_OK
|
||||
assert first_response.data["count"] == 2
|
||||
assert len(first_response.data["results"]) == 2
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
|
|
@ -309,7 +309,7 @@ def test_get_filter_acknowledged_by(
|
|||
**make_user_auth_headers(first_user, token),
|
||||
)
|
||||
assert first_response.status_code == status.HTTP_200_OK
|
||||
assert first_response.data["count"] == 1
|
||||
assert len(first_response.data["results"]) == 1
|
||||
|
||||
second_response = client.get(
|
||||
url + f"?acknowledged_by={second_user.public_primary_key}",
|
||||
|
|
@ -317,7 +317,7 @@ def test_get_filter_acknowledged_by(
|
|||
**make_user_auth_headers(first_user, token),
|
||||
)
|
||||
assert second_response.status_code == status.HTTP_200_OK
|
||||
assert second_response.data["count"] == 0
|
||||
assert len(second_response.data["results"]) == 0
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
|
|
@ -363,7 +363,7 @@ def test_get_filter_acknowledged_by_multiple_values(
|
|||
**make_user_auth_headers(first_user, token),
|
||||
)
|
||||
assert first_response.status_code == status.HTTP_200_OK
|
||||
assert first_response.data["count"] == 2
|
||||
assert len(first_response.data["results"]) == 2
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
|
|
@ -402,7 +402,7 @@ def test_get_filter_silenced_by(
|
|||
**make_user_auth_headers(first_user, token),
|
||||
)
|
||||
assert first_response.status_code == status.HTTP_200_OK
|
||||
assert first_response.data["count"] == 1
|
||||
assert len(first_response.data["results"]) == 1
|
||||
|
||||
second_response = client.get(
|
||||
url + f"?silenced_by={second_user.public_primary_key}",
|
||||
|
|
@ -410,7 +410,7 @@ def test_get_filter_silenced_by(
|
|||
**make_user_auth_headers(first_user, token),
|
||||
)
|
||||
assert second_response.status_code == status.HTTP_200_OK
|
||||
assert second_response.data["count"] == 0
|
||||
assert len(second_response.data["results"]) == 0
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
|
|
@ -455,7 +455,7 @@ def test_get_filter_silenced_by_multiple_values(
|
|||
**make_user_auth_headers(first_user, token),
|
||||
)
|
||||
assert first_response.status_code == status.HTTP_200_OK
|
||||
assert first_response.data["count"] == 2
|
||||
assert len(first_response.data["results"]) == 2
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
|
|
@ -494,7 +494,7 @@ def test_get_filter_invitees_are(
|
|||
**make_user_auth_headers(first_user, token),
|
||||
)
|
||||
assert first_response.status_code == status.HTTP_200_OK
|
||||
assert first_response.data["count"] == 1
|
||||
assert len(first_response.data["results"]) == 1
|
||||
|
||||
second_response = client.get(
|
||||
url + f"?invitees_are={second_user.public_primary_key}",
|
||||
|
|
@ -502,7 +502,7 @@ def test_get_filter_invitees_are(
|
|||
**make_user_auth_headers(first_user, token),
|
||||
)
|
||||
assert second_response.status_code == status.HTTP_200_OK
|
||||
assert second_response.data["count"] == 0
|
||||
assert len(second_response.data["results"]) == 0
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
|
|
@ -548,7 +548,7 @@ def test_get_filter_invitees_are_multiple_values(
|
|||
**make_user_auth_headers(first_user, token),
|
||||
)
|
||||
assert first_response.status_code == status.HTTP_200_OK
|
||||
assert first_response.data["count"] == 2
|
||||
assert len(first_response.data["results"]) == 2
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
|
|
@ -593,7 +593,7 @@ def test_get_filter_invitees_are_ag_with_multiple_logs(
|
|||
**make_user_auth_headers(first_user, token),
|
||||
)
|
||||
assert first_response.status_code == status.HTTP_200_OK
|
||||
assert first_response.data["count"] == 1
|
||||
assert len(first_response.data["results"]) == 1
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
|
|
@ -611,11 +611,11 @@ def test_get_filter_with_resolution_note(
|
|||
# there are no alert groups with resolution_notes
|
||||
response = client.get(url + "?with_resolution_note=true", format="json", **make_user_auth_headers(user, token))
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.data["count"] == 0
|
||||
assert len(response.data["results"]) == 0
|
||||
|
||||
response = client.get(url + "?with_resolution_note=false", format="json", **make_user_auth_headers(user, token))
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.data["count"] == 4
|
||||
assert len(response.data["results"]) == 4
|
||||
|
||||
# add resolution_notes to two of four alert groups
|
||||
make_resolution_note(res_alert_group)
|
||||
|
|
@ -623,11 +623,11 @@ def test_get_filter_with_resolution_note(
|
|||
|
||||
response = client.get(url + "?with_resolution_note=true", format="json", **make_user_auth_headers(user, token))
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.data["count"] == 2
|
||||
assert len(response.data["results"]) == 2
|
||||
|
||||
response = client.get(url + "?with_resolution_note=false", format="json", **make_user_auth_headers(user, token))
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.data["count"] == 2
|
||||
assert len(response.data["results"]) == 2
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
|
|
@ -653,7 +653,7 @@ def test_get_filter_with_resolution_note_after_delete_resolution_note(
|
|||
|
||||
response = client.get(url + "?with_resolution_note=true", format="json", **make_user_auth_headers(user, token))
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.data["count"] == 1
|
||||
assert len(response.data["results"]) == 1
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
|
|
|
|||
|
|
@ -1,10 +1,6 @@
|
|||
from datetime import datetime, timedelta
|
||||
from datetime import timedelta
|
||||
|
||||
from django import forms
|
||||
from django.db import models
|
||||
from django.db.models import CharField, Q
|
||||
from django.db.models.constants import LOOKUP_SEP
|
||||
from django.db.models.functions import Cast
|
||||
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
|
||||
|
|
@ -15,16 +11,15 @@ from rest_framework.permissions import IsAuthenticated
|
|||
from rest_framework.response import Response
|
||||
|
||||
from apps.alerts.constants import ActionSource
|
||||
from apps.alerts.models import AlertGroup, AlertReceiveChannel
|
||||
from apps.alerts.tasks import invalidate_web_cache_for_alert_group
|
||||
from apps.alerts.models import Alert, AlertGroup, AlertReceiveChannel
|
||||
from apps.api.permissions import MODIFY_ACTIONS, READ_ACTIONS, ActionPermission, AnyRole, IsAdminOrEditor
|
||||
from apps.api.serializers.alert_group import AlertGroupSerializer
|
||||
from apps.api.serializers.alert_group import AlertGroupListSerializer, AlertGroupSerializer
|
||||
from apps.auth_token.auth import MobileAppAuthTokenAuthentication, PluginAuthentication
|
||||
from apps.user_management.models import User
|
||||
from common.api_helpers.exceptions import BadRequest
|
||||
from common.api_helpers.filters import DateRangeFilterMixin, ModelFieldFilterMixin
|
||||
from common.api_helpers.mixins import PreviewTemplateMixin, PublicPrimaryKeyMixin
|
||||
from common.api_helpers.paginators import FiftyPageSizePaginator
|
||||
from common.api_helpers.paginators import TwentyFiveCursorPaginator
|
||||
|
||||
|
||||
def get_integration_queryset(request):
|
||||
|
|
@ -148,34 +143,6 @@ class AlertGroupFilter(DateRangeFilterMixin, ModelFieldFilterMixin, filters.Filt
|
|||
return queryset
|
||||
|
||||
|
||||
class CustomSearchFilter(SearchFilter):
|
||||
def must_call_distinct(self, queryset, search_fields):
|
||||
"""
|
||||
Return True if 'distinct()' should be used to query the given lookups.
|
||||
"""
|
||||
for search_field in search_fields:
|
||||
opts = queryset.model._meta
|
||||
if search_field[0] in self.lookup_prefixes:
|
||||
search_field = search_field[1:]
|
||||
|
||||
# From https://github.com/encode/django-rest-framework/pull/6240/files#diff-01f357e474dd8fd702e4951b9227bffcR88
|
||||
# Annotated fields do not need to be distinct
|
||||
if isinstance(queryset, models.QuerySet) and search_field in queryset.query.annotations:
|
||||
continue
|
||||
|
||||
parts = search_field.split(LOOKUP_SEP)
|
||||
for part in parts:
|
||||
field = opts.get_field(part)
|
||||
if hasattr(field, "get_path_info"):
|
||||
# This field is a relation, update opts to follow the relation
|
||||
path_info = field.get_path_info()
|
||||
opts = path_info[-1].to_opts
|
||||
if any(path.m2m for path in path_info):
|
||||
# This field is a m2m relation so we know we need to call distinct
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class AlertGroupView(
|
||||
PreviewTemplateMixin,
|
||||
PublicPrimaryKeyMixin,
|
||||
|
|
@ -216,90 +183,85 @@ class AlertGroupView(
|
|||
|
||||
serializer_class = AlertGroupSerializer
|
||||
|
||||
pagination_class = FiftyPageSizePaginator
|
||||
pagination_class = TwentyFiveCursorPaginator
|
||||
|
||||
filter_backends = [CustomSearchFilter, filters.DjangoFilterBackend]
|
||||
search_fields = ["cached_render_for_web_str"]
|
||||
filter_backends = [SearchFilter, filters.DjangoFilterBackend]
|
||||
# todo: add ability to search by templated title
|
||||
search_fields = ["public_primary_key", "inside_organization_number"]
|
||||
|
||||
filterset_class = AlertGroupFilter
|
||||
|
||||
def list(self, request, *args, **kwargs):
|
||||
"""
|
||||
It's compute-heavy so we rely on cache here.
|
||||
Attention: Make sure to invalidate cache if you update the format!
|
||||
"""
|
||||
queryset = self.filter_queryset(self.get_queryset(eager=False, readonly=True))
|
||||
def get_serializer_class(self):
|
||||
if self.action == "list":
|
||||
return AlertGroupListSerializer
|
||||
|
||||
page = self.paginate_queryset(queryset)
|
||||
skip_slow_rendering = request.query_params.get("skip_slow_rendering") == "true"
|
||||
data = []
|
||||
return super().get_serializer_class()
|
||||
|
||||
for alert_group in page:
|
||||
if alert_group.cached_render_for_web == {}:
|
||||
# We cannot give empty data to web. So caching synchronously here.
|
||||
if skip_slow_rendering:
|
||||
# We just return dummy data.
|
||||
# Cache is not launched because after skip_slow_rendering request should come usual one
|
||||
# which will start caching
|
||||
data.append({"pk": alert_group.pk, "short": True})
|
||||
else:
|
||||
# Synchronously cache and return. It could be slow.
|
||||
alert_group.cache_for_web(alert_group.channel.organization)
|
||||
data.append(alert_group.cached_render_for_web)
|
||||
else:
|
||||
data.append(alert_group.cached_render_for_web)
|
||||
if not skip_slow_rendering:
|
||||
# Cache is not launched because after skip_slow_rendering request should come usual one
|
||||
# which will start caching
|
||||
alert_group.schedule_cache_for_web()
|
||||
def get_queryset(self):
|
||||
# make a separate query to fetch all the integrations for current organization and team (it's faster)
|
||||
alert_receive_channel_pks = AlertReceiveChannel.objects_with_deleted.filter(
|
||||
organization=self.request.auth.organization, team=self.request.user.current_team
|
||||
).values_list("pk", flat=True)
|
||||
alert_receive_channel_pks = list(alert_receive_channel_pks)
|
||||
|
||||
return self.get_paginated_response(data)
|
||||
# no select_related or prefetch_related is used at this point, it will be done on paginate_queryset.
|
||||
queryset = AlertGroup.unarchived_objects.filter(channel_id__in=alert_receive_channel_pks)
|
||||
|
||||
def get_queryset(self, eager=True, readonly=False, order=True):
|
||||
if readonly:
|
||||
queryset = AlertGroup.unarchived_objects.using_readonly_db
|
||||
else:
|
||||
queryset = AlertGroup.unarchived_objects
|
||||
|
||||
queryset = queryset.filter(
|
||||
channel__organization=self.request.auth.organization,
|
||||
channel__team=self.request.user.current_team,
|
||||
)
|
||||
|
||||
if order:
|
||||
queryset = queryset.order_by("-started_at")
|
||||
|
||||
queryset = queryset.annotate(cached_render_for_web_str=Cast("cached_render_for_web", output_field=CharField()))
|
||||
|
||||
if eager:
|
||||
queryset = self.serializer_class.setup_eager_loading(queryset)
|
||||
return queryset
|
||||
|
||||
def get_alert_groups_and_days_for_previous_same_period(self):
|
||||
prev_alert_groups = AlertGroup.unarchived_objects.none()
|
||||
delta_days = None
|
||||
def paginate_queryset(self, queryset):
|
||||
"""
|
||||
All SQL joins (select_related and prefetch_related) will be performed AFTER pagination, so it only joins tables
|
||||
for 25 alert groups, not the whole table.
|
||||
"""
|
||||
alert_groups = super().paginate_queryset(queryset)
|
||||
alert_groups = self.enrich(alert_groups)
|
||||
return alert_groups
|
||||
|
||||
started_at = self.request.query_params.get("started_at", None)
|
||||
if started_at is not None:
|
||||
started_at_gte, started_at_lte = AlertGroupFilter.parse_custom_datetime_range(started_at)
|
||||
delta_days = None
|
||||
if started_at_lte is not None:
|
||||
started_at_lte = forms.DateTimeField().to_python(started_at_lte)
|
||||
else:
|
||||
started_at_lte = datetime.now()
|
||||
def get_object(self):
|
||||
obj = super().get_object()
|
||||
obj = self.enrich([obj])[0]
|
||||
return obj
|
||||
|
||||
if started_at_gte is not None:
|
||||
started_at_gte = forms.DateTimeField().to_python(value=started_at_gte)
|
||||
delta = started_at_lte.replace(tzinfo=None) - started_at_gte.replace(tzinfo=None)
|
||||
prev_alert_groups = self.get_queryset().filter(
|
||||
started_at__range=[started_at_gte - delta, started_at_gte]
|
||||
)
|
||||
delta_days = delta.days
|
||||
return prev_alert_groups, delta_days
|
||||
def enrich(self, alert_groups):
|
||||
"""
|
||||
This method performs select_related and prefetch_related (using setup_eager_loading) as well as in-memory joins
|
||||
to add additional info like alert_count and last_alert for every alert group efficiently.
|
||||
We need the last_alert because it's used by AlertGroupWebRenderer.
|
||||
"""
|
||||
|
||||
# enrich alert groups with select_related and prefetch_related
|
||||
alert_group_pks = [alert_group.pk for alert_group in alert_groups]
|
||||
queryset = AlertGroup.all_objects.filter(pk__in=alert_group_pks).order_by("-pk")
|
||||
queryset = self.get_serializer_class().setup_eager_loading(queryset)
|
||||
alert_groups = list(queryset)
|
||||
|
||||
# get info on alerts count and last alert ID for every alert group
|
||||
alerts_info = (
|
||||
Alert.objects.values("group_id")
|
||||
.filter(group_id__in=alert_group_pks)
|
||||
.annotate(alerts_count=Count("group_id"), last_alert_id=Max("id"))
|
||||
)
|
||||
alerts_info_map = {info["group_id"]: info for info in alerts_info}
|
||||
|
||||
# fetch last alerts for every alert group
|
||||
last_alert_ids = [info["last_alert_id"] for info in alerts_info_map.values()]
|
||||
last_alerts = Alert.objects.filter(pk__in=last_alert_ids)
|
||||
for alert in last_alerts:
|
||||
# link group back to alert
|
||||
alert.group = [alert_group for alert_group in alert_groups if alert_group.pk == alert.group_id][0]
|
||||
alerts_info_map[alert.group_id].update({"last_alert": alert})
|
||||
|
||||
# add additional "alerts_count" and "last_alert" fields to every alert group
|
||||
for alert_group in alert_groups:
|
||||
alert_group.last_alert = alerts_info_map[alert_group.pk]["last_alert"]
|
||||
alert_group.alerts_count = alerts_info_map[alert_group.pk]["alerts_count"]
|
||||
|
||||
return alert_groups
|
||||
|
||||
@action(detail=False)
|
||||
def stats(self, *args, **kwargs):
|
||||
alert_groups = self.filter_queryset(self.get_queryset(eager=False))
|
||||
alert_groups = self.filter_queryset(self.get_queryset())
|
||||
# Only count field is used, other fields left just in case for the backward compatibility
|
||||
return Response(
|
||||
{
|
||||
|
|
@ -324,7 +286,6 @@ class AlertGroupView(
|
|||
if alert_group.root_alert_group is not None:
|
||||
raise BadRequest(detail="Can't acknowledge an attached alert group")
|
||||
alert_group.acknowledge_by_user(self.request.user, action_source=ActionSource.WEB)
|
||||
invalidate_web_cache_for_alert_group(alert_group_pk=alert_group.pk)
|
||||
|
||||
return Response(AlertGroupSerializer(alert_group, context={"request": self.request}).data)
|
||||
|
||||
|
|
@ -344,7 +305,6 @@ class AlertGroupView(
|
|||
raise BadRequest(detail="Can't unacknowledge a resolved alert group")
|
||||
|
||||
alert_group.un_acknowledge_by_user(self.request.user, action_source=ActionSource.WEB)
|
||||
invalidate_web_cache_for_alert_group(alert_group_pk=alert_group.pk)
|
||||
|
||||
return Response(AlertGroupSerializer(alert_group, context={"request": self.request}).data)
|
||||
|
||||
|
|
@ -365,7 +325,6 @@ class AlertGroupView(
|
|||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
alert_group.resolve_by_user(self.request.user, action_source=ActionSource.WEB)
|
||||
invalidate_web_cache_for_alert_group(alert_group_pk=alert_group.pk)
|
||||
return Response(AlertGroupSerializer(alert_group, context={"request": self.request}).data)
|
||||
|
||||
@action(methods=["post"], detail=True)
|
||||
|
|
@ -381,7 +340,6 @@ class AlertGroupView(
|
|||
raise BadRequest(detail="The alert group is not resolved")
|
||||
|
||||
alert_group.un_resolve_by_user(self.request.user, action_source=ActionSource.WEB)
|
||||
invalidate_web_cache_for_alert_group(alert_group_pk=alert_group.pk)
|
||||
return Response(AlertGroupSerializer(alert_group, context={"request": self.request}).data)
|
||||
|
||||
@action(methods=["post"], detail=True)
|
||||
|
|
@ -404,8 +362,6 @@ class AlertGroupView(
|
|||
return Response(status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
alert_group.attach_by_user(self.request.user, root_alert_group, action_source=ActionSource.WEB)
|
||||
invalidate_web_cache_for_alert_group(alert_group_pk=alert_group.pk)
|
||||
invalidate_web_cache_for_alert_group(alert_group_pk=root_alert_group.pk)
|
||||
return Response(AlertGroupSerializer(alert_group, context={"request": self.request}).data)
|
||||
|
||||
@action(methods=["post"], detail=True)
|
||||
|
|
@ -415,10 +371,8 @@ class AlertGroupView(
|
|||
raise BadRequest(detail="Can't unattach maintenance alert group")
|
||||
if alert_group.is_root_alert_group:
|
||||
raise BadRequest(detail="Can't unattach an alert group because it is not attached")
|
||||
root_alert_group_pk = alert_group.root_alert_group_id
|
||||
|
||||
alert_group.un_attach_by_user(self.request.user, action_source=ActionSource.WEB)
|
||||
invalidate_web_cache_for_alert_group(alert_group_pk=alert_group.pk)
|
||||
invalidate_web_cache_for_alert_group(alert_group_pk=root_alert_group_pk)
|
||||
return Response(AlertGroupSerializer(alert_group, context={"request": self.request}).data)
|
||||
|
||||
@action(methods=["post"], detail=True)
|
||||
|
|
@ -433,7 +387,6 @@ class AlertGroupView(
|
|||
raise BadRequest(detail="Can't silence an attached alert group")
|
||||
|
||||
alert_group.silence_by_user(request.user, silence_delay=delay, action_source=ActionSource.WEB)
|
||||
invalidate_web_cache_for_alert_group(alert_group_pk=alert_group.pk)
|
||||
return Response(AlertGroupSerializer(alert_group, context={"request": request}).data)
|
||||
|
||||
@action(methods=["get"], detail=False)
|
||||
|
|
@ -548,9 +501,9 @@ class AlertGroupView(
|
|||
raise BadRequest(detail="Please specify a delay for silence")
|
||||
kwargs["silence_delay"] = delay
|
||||
|
||||
alert_groups = self.get_queryset(eager=False).filter(public_primary_key__in=alert_group_public_pks)
|
||||
alert_group_pks = list(alert_groups.values_list("id", flat=True))
|
||||
invalidate_web_cache_for_alert_group(alert_group_pks=alert_group_pks)
|
||||
alert_groups = AlertGroup.unarchived_objects.filter(
|
||||
channel__organization=self.request.auth.organization, public_primary_key__in=alert_group_public_pks
|
||||
)
|
||||
|
||||
kwargs["user"] = self.request.user
|
||||
kwargs["alert_groups"] = alert_groups
|
||||
|
|
|
|||
|
|
@ -43,10 +43,7 @@ class RouteRegexDebuggerView(APIView):
|
|||
if len(incidents_matching_regex) < MAX_INCIDENTS_TO_SHOW:
|
||||
first_alert = ag.alerts.all()[0]
|
||||
if re.search(regex, json.dumps(first_alert.raw_request_data)):
|
||||
if ag.cached_render_for_web:
|
||||
title = ag.cached_render_for_web["render_for_web"]["title"]
|
||||
else:
|
||||
title = AlertWebRenderer(first_alert).render()["title"]
|
||||
title = AlertWebRenderer(first_alert).render()["title"]
|
||||
incidents_matching_regex.append(
|
||||
{
|
||||
"title": title,
|
||||
|
|
|
|||
|
|
@ -315,7 +315,6 @@ class UserNotificationPolicyLogRecord(models.Model):
|
|||
|
||||
@receiver(post_save, sender=UserNotificationPolicyLogRecord)
|
||||
def listen_for_usernotificationpolicylogrecord_model_save(sender, instance, created, *args, **kwargs):
|
||||
instance.alert_group.drop_cached_after_resolve_report_json()
|
||||
alert_group_pk = instance.alert_group.pk
|
||||
if instance.type != UserNotificationPolicyLogRecord.TYPE_PERSONAL_NOTIFICATION_FINISHED:
|
||||
logger.debug(
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ def construct_expected_response_from_incidents(incidents):
|
|||
"id": incident.public_primary_key,
|
||||
"integration_id": incident.channel.public_primary_key,
|
||||
"route_id": incident.channel_filter.public_primary_key,
|
||||
"alerts_count": incident.alerts_count,
|
||||
"alerts_count": incident.alerts.count(),
|
||||
"state": incident.state,
|
||||
"created_at": created_at,
|
||||
"resolved_at": resolved_at,
|
||||
|
|
|
|||
|
|
@ -247,10 +247,6 @@ class UpdateAppearanceStep(scenario_step.ScenarioStep):
|
|||
if new_value is None and old_value is not None:
|
||||
setattr(alert_receive_channel, attr_name, None)
|
||||
alert_receive_channel.save()
|
||||
# Drop caches for current alert group
|
||||
if notification_channel == "web":
|
||||
setattr(alert_group, f"cached_render_for_web_{templatizable_attr}", None)
|
||||
alert_group.save()
|
||||
elif new_value is not None:
|
||||
default_values = getattr(
|
||||
AlertReceiveChannel,
|
||||
|
|
@ -265,18 +261,10 @@ class UpdateAppearanceStep(scenario_step.ScenarioStep):
|
|||
jinja_template_env.from_string(new_value)
|
||||
setattr(alert_receive_channel, attr_name, new_value)
|
||||
alert_receive_channel.save()
|
||||
# Drop caches for current alert group
|
||||
if notification_channel == "web":
|
||||
setattr(alert_group, f"cached_render_for_web_{templatizable_attr}", None)
|
||||
alert_group.save()
|
||||
elif default_value is not None and new_value.strip() == default_value.strip():
|
||||
new_value = None
|
||||
setattr(alert_receive_channel, attr_name, new_value)
|
||||
alert_receive_channel.save()
|
||||
# Drop caches for current alert group
|
||||
if notification_channel == "web":
|
||||
setattr(alert_group, f"cached_render_for_web_{templatizable_attr}", None)
|
||||
alert_group.save()
|
||||
except TemplateSyntaxError:
|
||||
return Response(
|
||||
{"response_action": "errors", "errors": {attr_name: "Template has incorrect format"}},
|
||||
|
|
|
|||
|
|
@ -674,7 +674,6 @@ class AddRemoveThreadMessageStep(UpdateResolutionNoteStep, scenario_step.Scenari
|
|||
add_to_resolution_note = True if value["msg_value"].startswith("add") else False
|
||||
slack_thread_message = None
|
||||
resolution_note = None
|
||||
drop_ag_cache = False
|
||||
|
||||
alert_group = AlertGroup.all_objects.get(pk=alert_group_pk)
|
||||
|
||||
|
|
@ -695,7 +694,6 @@ class AddRemoveThreadMessageStep(UpdateResolutionNoteStep, scenario_step.Scenari
|
|||
else:
|
||||
resolution_note.recreate()
|
||||
self.add_resolution_note_reaction(slack_thread_message)
|
||||
drop_ag_cache = True
|
||||
elif not add_to_resolution_note:
|
||||
# Check if resolution_note can be removed
|
||||
if (
|
||||
|
|
@ -720,13 +718,9 @@ class AddRemoveThreadMessageStep(UpdateResolutionNoteStep, scenario_step.Scenari
|
|||
slack_thread_message.added_to_resolution_note = False
|
||||
slack_thread_message.save(update_fields=["added_to_resolution_note"])
|
||||
self.remove_resolution_note_reaction(slack_thread_message)
|
||||
drop_ag_cache = True
|
||||
self.update_alert_group_resolution_note_button(
|
||||
alert_group,
|
||||
)
|
||||
if drop_ag_cache:
|
||||
alert_group.drop_cached_after_resolve_report_json()
|
||||
alert_group.schedule_cache_for_web()
|
||||
resolution_note_data = json.loads(payload["actions"][0]["value"])
|
||||
resolution_note_data["resolution_note_window_action"] = "edit_update"
|
||||
ResolutionNoteModalStep(slack_team_identity, self.organization, self.user).process_scenario(
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ from django.db.models.signals import post_save
|
|||
from django.dispatch import receiver
|
||||
from emoji import demojize
|
||||
|
||||
from apps.alerts.tasks import invalidate_web_cache_for_alert_group
|
||||
from apps.schedules.tasks import drop_cached_ical_for_custom_events_for_organization
|
||||
from common.constants.role import Role
|
||||
from common.public_primary_keys import generate_public_primary_key, increase_public_primary_key_length
|
||||
|
|
@ -264,5 +263,3 @@ def listen_for_user_model_save(sender, instance, created, *args, **kwargs):
|
|||
drop_cached_ical_for_custom_events_for_organization.apply_async(
|
||||
(instance.organization_id,),
|
||||
)
|
||||
logger.info(f"Drop AG cache. Reason: save user {instance.pk}")
|
||||
invalidate_web_cache_for_alert_group.apply_async(kwargs={"org_pk": instance.organization_id})
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
from rest_framework.pagination import PageNumberPagination
|
||||
from rest_framework.pagination import CursorPagination, PageNumberPagination
|
||||
|
||||
|
||||
class HundredPageSizePaginator(PageNumberPagination):
|
||||
|
|
@ -11,3 +11,10 @@ class FiftyPageSizePaginator(PageNumberPagination):
|
|||
|
||||
class TwentyFivePageSizePaginator(PageNumberPagination):
|
||||
page_size = 25
|
||||
|
||||
|
||||
class TwentyFiveCursorPaginator(CursorPagination):
|
||||
page_size = 25
|
||||
max_page_size = 100
|
||||
page_size_query_param = "perpage"
|
||||
ordering = "-pk"
|
||||
|
|
|
|||
|
|
@ -1,21 +0,0 @@
|
|||
import random
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
class UseRandomReadonlyDbManagerMixin:
|
||||
"""
|
||||
Use this Mixin in ModelManagers, when you want to use the random readonly replica
|
||||
"""
|
||||
|
||||
@property
|
||||
def using_readonly_db(self):
|
||||
"""Select one of the readonly databases this QuerySet should execute against."""
|
||||
if hasattr(settings, "READONLY_DATABASES") and len(settings.READONLY_DATABASES) > 0:
|
||||
using_db = random.choice(list(settings.READONLY_DATABASES.keys()))
|
||||
return self.using(using_db)
|
||||
else:
|
||||
# Use "default" database
|
||||
# Django uses the database with the alias of default when no other database has been selected.
|
||||
# https://docs.djangoproject.com/en/3.2/topics/db/multi-db/#defining-your-databases
|
||||
return self.using("default")
|
||||
|
|
@ -32,10 +32,6 @@ DATABASES = {
|
|||
|
||||
TESTING = "pytest" in sys.modules or "unittest" in sys.modules
|
||||
|
||||
READONLY_DATABASES = {}
|
||||
|
||||
# Dictionaries concatenation, introduced in python3.9
|
||||
DATABASES = DATABASES | READONLY_DATABASES
|
||||
|
||||
CACHES = {
|
||||
"default": {
|
||||
|
|
|
|||
|
|
@ -84,12 +84,11 @@ CELERY_TASK_ROUTES = {
|
|||
"apps.alerts.tasks.create_contact_points_for_datasource.create_contact_points_for_datasource": {"queue": "default"},
|
||||
"apps.alerts.tasks.sync_grafana_alerting_contact_points.sync_grafana_alerting_contact_points": {"queue": "default"},
|
||||
"apps.alerts.tasks.delete_alert_group.delete_alert_group": {"queue": "default"},
|
||||
"apps.alerts.tasks.invalidate_web_cache_for_alert_group.invalidate_web_cache_for_alert_group": {"queue": "default"},
|
||||
"apps.alerts.tasks.invalidate_web_cache_for_alert_group.invalidate_web_cache_for_alert_group": {
|
||||
"queue": "default"
|
||||
}, # todo: remove
|
||||
"apps.alerts.tasks.send_alert_group_signal.send_alert_group_signal": {"queue": "default"},
|
||||
"apps.alerts.tasks.wipe.wipe": {"queue": "default"},
|
||||
# TODO: remove cache_alert_group_for_web and schedule_cache_for_alert_group once existing task will be processed
|
||||
"apps.api.tasks.cache_alert_group_for_web": {"queue": "default"},
|
||||
"apps.api.tasks.schedule_cache_for_alert_group": {"queue": "default"},
|
||||
"apps.heartbeat.tasks.heartbeat_checkup": {"queue": "default"},
|
||||
"apps.heartbeat.tasks.integration_heartbeat_checkup": {"queue": "default"},
|
||||
"apps.heartbeat.tasks.process_heartbeat_task": {"queue": "default"},
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
.root {
|
||||
display: block;
|
||||
}
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
import React, { FC, useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { Button, HorizontalGroup, Icon, Select } from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
|
||||
import Text from 'components/Text/Text';
|
||||
|
||||
import styles from './CursorPagination.module.css';
|
||||
|
||||
interface CursorPaginationProps {
|
||||
current: string;
|
||||
onChange: (cursor: string, direction: 'prev' | 'next') => void;
|
||||
itemsPerPageOptions: Array<SelectableValue<number>>;
|
||||
itemsPerPage: number;
|
||||
onChangeItemsPerPage: (value: number) => void;
|
||||
prev: string;
|
||||
next: string;
|
||||
}
|
||||
|
||||
const cx = cn.bind(styles);
|
||||
|
||||
const CursorPagination: FC<CursorPaginationProps> = (props) => {
|
||||
const { current, onChange, prev, next, itemsPerPage, itemsPerPageOptions, onChangeItemsPerPage } = props;
|
||||
|
||||
const [disabled, setDisabled] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
setDisabled(false);
|
||||
}, [prev, next]);
|
||||
|
||||
const onChangeItemsPerPageCallback = useCallback((option) => {
|
||||
setDisabled(true);
|
||||
onChangeItemsPerPage(option.value);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<HorizontalGroup spacing="md" justify="flex-end">
|
||||
<HorizontalGroup>
|
||||
<Text type="secondary">Items per list</Text>
|
||||
<Select
|
||||
isSearchable={false}
|
||||
options={itemsPerPageOptions}
|
||||
value={itemsPerPage}
|
||||
onChange={onChangeItemsPerPageCallback}
|
||||
/>
|
||||
</HorizontalGroup>
|
||||
<HorizontalGroup>
|
||||
<Button
|
||||
aria-label="previous"
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
setDisabled(true);
|
||||
onChange(prev, 'prev');
|
||||
}}
|
||||
disabled={disabled || !prev}
|
||||
>
|
||||
<Icon name="angle-left" />
|
||||
</Button>
|
||||
<Text type="secondary">{current}</Text>
|
||||
<Button
|
||||
aria-label="previous"
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
setDisabled(true);
|
||||
onChange(next, 'next');
|
||||
}}
|
||||
disabled={disabled || !next}
|
||||
>
|
||||
<Icon name="angle-right" />
|
||||
</Button>
|
||||
</HorizontalGroup>
|
||||
</HorizontalGroup>
|
||||
);
|
||||
};
|
||||
|
||||
export default CursorPagination;
|
||||
|
|
@ -37,7 +37,7 @@ const cx = cn.bind(styles);
|
|||
|
||||
interface IncidentsFiltersProps extends WithStoreProps {
|
||||
value: IncidentsFiltersType;
|
||||
onChange: (filters: { [key: string]: any }) => void;
|
||||
onChange: (filters: { [key: string]: any }, isOnMount: boolean) => void;
|
||||
query: { [key: string]: any };
|
||||
}
|
||||
interface IncidentsFiltersState {
|
||||
|
|
@ -79,7 +79,9 @@ class IncidentsFilters extends Component<IncidentsFiltersProps, IncidentsFilters
|
|||
({ filters, values } = parseFilters(newQuery, filterOptions));
|
||||
}
|
||||
|
||||
this.setState({ filterOptions, filters, values }, this.onChange);
|
||||
this.setState({ filterOptions, filters, values }, () => {
|
||||
this.onChange(true);
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
|
|
@ -421,11 +423,11 @@ class IncidentsFilters extends Component<IncidentsFiltersProps, IncidentsFilters
|
|||
};
|
||||
};
|
||||
|
||||
onChange = () => {
|
||||
onChange = (isOnMount = false) => {
|
||||
const { onChange } = this.props;
|
||||
const { values } = this.state;
|
||||
|
||||
onChange(values);
|
||||
onChange(values, isOnMount);
|
||||
};
|
||||
|
||||
debouncedOnChange = debounce(this.onChange, 500);
|
||||
|
|
|
|||
|
|
@ -36,7 +36,10 @@ export class AlertGroupStore extends BaseStore {
|
|||
initialQuery = qs.parse(window.location.search);
|
||||
|
||||
@observable
|
||||
incidentsPage: any = this.initialQuery.p ? Number(this.initialQuery.p) : 1;
|
||||
incidentsCursor?: string;
|
||||
|
||||
@observable
|
||||
incidentsItemsPerPage?: number;
|
||||
|
||||
@observable
|
||||
alertsSearchResult: any = {};
|
||||
|
|
@ -215,54 +218,63 @@ export class AlertGroupStore extends BaseStore {
|
|||
}
|
||||
|
||||
@action
|
||||
async updateIncidentFilters(params: any, resetPage = true) {
|
||||
if (resetPage) {
|
||||
this.incidentsPage = 1;
|
||||
async updateIncidentFilters(params: any, keepCursor = false) {
|
||||
if (!keepCursor) {
|
||||
this.incidentsCursor = undefined;
|
||||
}
|
||||
|
||||
this.incidentFilters = params;
|
||||
|
||||
this.updateIncidents();
|
||||
}
|
||||
|
||||
@action
|
||||
async setIncidentsPage(page: number) {
|
||||
this.incidentsPage = page;
|
||||
async setIncidentsCursor(cursor: string) {
|
||||
this.incidentsCursor = cursor;
|
||||
|
||||
this.updateAlertGroups();
|
||||
}
|
||||
|
||||
@action
|
||||
async updateAlertGroups(skip_slow_rendering = true) {
|
||||
this.alertGroupsLoading = skip_slow_rendering;
|
||||
async setIncidentsItemsPerPage(value: number) {
|
||||
this.incidentsCursor = undefined;
|
||||
this.incidentsItemsPerPage = value;
|
||||
|
||||
const result = await makeRequest(`${this.path}`, {
|
||||
this.updateAlertGroups();
|
||||
}
|
||||
|
||||
@action
|
||||
async updateAlertGroups() {
|
||||
this.alertGroupsLoading = true;
|
||||
|
||||
const {
|
||||
results,
|
||||
next: nextRaw,
|
||||
previous: previousRaw,
|
||||
} = await makeRequest(`${this.path}`, {
|
||||
params: {
|
||||
...this.incidentFilters,
|
||||
page: this.incidentsPage,
|
||||
cursor: this.incidentsCursor,
|
||||
perpage: this.incidentsItemsPerPage,
|
||||
is_root: true,
|
||||
skip_slow_rendering,
|
||||
},
|
||||
}).catch(refreshPageError);
|
||||
|
||||
const newAlerts = new Map(result.results.map((alert: Alert) => [alert.pk, alert]));
|
||||
const prevCursor = previousRaw ? qs.parse(qs.extract(previousRaw)).cursor : previousRaw;
|
||||
const nextCursor = nextRaw ? qs.parse(qs.extract(nextRaw)).cursor : nextRaw;
|
||||
|
||||
const newAlerts = new Map(results.map((alert: Alert) => [alert.pk, alert]));
|
||||
|
||||
// @ts-ignore
|
||||
this.alerts = new Map<number, Alert>([...this.alerts, ...newAlerts]);
|
||||
|
||||
this.alertsSearchResult['default'] = {
|
||||
count: result.count,
|
||||
results: result.results.map((alert: Alert) => alert.pk),
|
||||
prev: prevCursor,
|
||||
next: nextCursor,
|
||||
results: results.map((alert: Alert) => alert.pk),
|
||||
};
|
||||
|
||||
this.alertGroupsLoading = false;
|
||||
|
||||
if (skip_slow_rendering) {
|
||||
const hasShortened = result.results.some((alert: Alert) => alert.short);
|
||||
|
||||
if (hasShortened) {
|
||||
this.updateAlertGroups(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getAlertSearchResult(query: string) {
|
||||
|
|
@ -273,27 +285,6 @@ export class AlertGroupStore extends BaseStore {
|
|||
return this.alertsSearchResult[query].results.map((pk: Alert['pk']) => this.alerts.get(pk));
|
||||
}
|
||||
|
||||
@action
|
||||
async searchIncidents(search: string) {
|
||||
const result = await makeRequest(`${this.path}`, {
|
||||
params: {
|
||||
search,
|
||||
resolved: false,
|
||||
is_root: true,
|
||||
},
|
||||
});
|
||||
|
||||
const newAlerts = new Map(result.results.map((alert: Alert) => [alert.pk, alert]));
|
||||
|
||||
// @ts-ignore
|
||||
this.alerts = new Map<number, Alert>([...this.alerts, ...newAlerts]);
|
||||
|
||||
this.alertsSearchResult[search] = {
|
||||
count: result.count,
|
||||
results: result.results.map((alert: Alert) => alert.pk),
|
||||
};
|
||||
}
|
||||
|
||||
@action
|
||||
getAlert(pk: Alert['pk']) {
|
||||
return makeRequest(`${this.path}${pk}`, {}).then((alert: Alert) => {
|
||||
|
|
|
|||
|
|
@ -42,17 +42,16 @@ export interface Alert {
|
|||
title: string;
|
||||
message: string;
|
||||
image_url: string;
|
||||
alerts: any[];
|
||||
alerts?: any[];
|
||||
acknowledged: boolean;
|
||||
created_at: string;
|
||||
acknowledged_at: string;
|
||||
acknowledged_at_verbose: string;
|
||||
acknowledged_by_user: User;
|
||||
acknowledged_on_source: boolean;
|
||||
channel: Channel;
|
||||
permalink: string;
|
||||
permalink?: string;
|
||||
related_users: User[];
|
||||
render_after_resolve_report_json: TimeLineItem[];
|
||||
render_after_resolve_report_json?: TimeLineItem[];
|
||||
render_for_slack: { attachments: any[] };
|
||||
render_for_web: {
|
||||
message: any;
|
||||
|
|
@ -63,17 +62,13 @@ export interface Alert {
|
|||
inside_organization_number: number;
|
||||
resolved: boolean;
|
||||
resolved_at: string;
|
||||
resolved_at_verbose: string;
|
||||
resolved_by: number;
|
||||
resolved_by_user: User;
|
||||
resolved_by_verbose: string;
|
||||
silenced: boolean;
|
||||
silenced_at: string;
|
||||
silenced_at_verbose: string;
|
||||
silenced_by_user: Partial<User>;
|
||||
silenced_until: string;
|
||||
started_at: string;
|
||||
started_at_verbose: string;
|
||||
last_alert_at: string;
|
||||
verbose_name: string;
|
||||
dependent_alert_groups: Alert[];
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@ class IncidentPage extends React.Component<IncidentPageProps, IncidentPageState>
|
|||
render() {
|
||||
const {
|
||||
store,
|
||||
query: { id },
|
||||
query: { id, cursor, start, perpage },
|
||||
} = this.props;
|
||||
|
||||
const { showIntegrationSettings, showAttachIncidentForm, notFound } = this.state;
|
||||
|
|
@ -112,7 +112,7 @@ class IncidentPage extends React.Component<IncidentPageProps, IncidentPageState>
|
|||
<VerticalGroup spacing="lg" align="center">
|
||||
<Text.Title level={1}>404</Text.Title>
|
||||
<Text.Title level={4}>Incident not found</Text.Title>
|
||||
<PluginLink query={{ page: 'incidents' }}>
|
||||
<PluginLink query={{ page: 'incidents', cursor, start, perpage }}>
|
||||
<Button variant="secondary" icon="arrow-left" size="md">
|
||||
Go to incidents page
|
||||
</Button>
|
||||
|
|
@ -182,7 +182,7 @@ class IncidentPage extends React.Component<IncidentPageProps, IncidentPageState>
|
|||
renderHeader = () => {
|
||||
const {
|
||||
store,
|
||||
query: { id },
|
||||
query: { id, cursor, start, perpage },
|
||||
} = this.props;
|
||||
|
||||
const { alerts } = store.alertGroupStore;
|
||||
|
|
@ -197,7 +197,7 @@ class IncidentPage extends React.Component<IncidentPageProps, IncidentPageState>
|
|||
<Block withBackground>
|
||||
<VerticalGroup>
|
||||
<HorizontalGroup className={cx('title')}>
|
||||
<PluginLink query={{ page: 'incidents' }}>
|
||||
<PluginLink query={{ page: 'incidents', cursor, start, perpage }}>
|
||||
<IconButton name="arrow-left" size="xxl" />
|
||||
</PluginLink>
|
||||
{/* @ts-ignore*/}
|
||||
|
|
@ -293,7 +293,13 @@ class IncidentPage extends React.Component<IncidentPageProps, IncidentPageState>
|
|||
};
|
||||
|
||||
renderIncident = (incident: Alert) => {
|
||||
const m = moment(incident.last_alert_at || incident.created_at);
|
||||
let datetimeReference;
|
||||
|
||||
if (incident.last_alert_at || incident.created_at) {
|
||||
const m = moment(incident.last_alert_at || incident.created_at);
|
||||
datetimeReference = `(${m.fromNow()}, ${m.toString()})`;
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={incident.pk} className={cx('incident')}>
|
||||
<HorizontalGroup wrap>
|
||||
|
|
@ -302,9 +308,7 @@ class IncidentPage extends React.Component<IncidentPageProps, IncidentPageState>
|
|||
? `#${incident.inside_organization_number} ${incident.render_for_web.title}`
|
||||
: incident.render_for_web.title}
|
||||
</Text.Title>
|
||||
<Text type="secondary">
|
||||
({m.fromNow()}, {m.toString()})
|
||||
</Text>
|
||||
<Text type="secondary">{datetimeReference}</Text>
|
||||
</HorizontalGroup>
|
||||
<div
|
||||
className={cx('message')}
|
||||
|
|
@ -326,6 +330,9 @@ class IncidentPage extends React.Component<IncidentPageProps, IncidentPageState>
|
|||
const incident = store.alertGroupStore.alerts.get(id);
|
||||
|
||||
const alerts = incident.alerts;
|
||||
if (!alerts) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const latestAlert = alerts[alerts.length - 1];
|
||||
const latestAlertMoment = moment(latestAlert.created_at);
|
||||
|
|
@ -407,6 +414,10 @@ class IncidentPage extends React.Component<IncidentPageProps, IncidentPageState>
|
|||
|
||||
const incident = store.alertGroupStore.alerts.get(id);
|
||||
|
||||
if (!incident.render_after_resolve_report_json) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const timeline = this.filterTimeline(incident.render_after_resolve_report_json);
|
||||
const { timelineFilter, resolutionNoteText } = this.state;
|
||||
const isResolutionNoteTextEmpty = resolutionNoteText === '';
|
||||
|
|
|
|||
|
|
@ -34,3 +34,8 @@
|
|||
height: 24px;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
width: 100%;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import moment from 'moment';
|
|||
import Emoji from 'react-emoji-render';
|
||||
|
||||
import CardButton from 'components/CardButton/CardButton';
|
||||
import CursorPagination from 'components/CursorPagination/CursorPagination';
|
||||
import GTable from 'components/GTable/GTable';
|
||||
import IntegrationLogo from 'components/IntegrationLogo/IntegrationLogo';
|
||||
import PluginLink from 'components/PluginLink/PluginLink';
|
||||
|
|
@ -35,7 +36,10 @@ import styles from './Incidents.module.css';
|
|||
|
||||
const cx = cn.bind(styles);
|
||||
|
||||
const ITEMS_PER_PAGE = 50;
|
||||
interface Pagination {
|
||||
start: number;
|
||||
end: number;
|
||||
}
|
||||
|
||||
function withSkeleton(fn: (alert: AlertType) => ReactElement | ReactElement[]) {
|
||||
return (alert: AlertType) => {
|
||||
|
|
@ -53,28 +57,41 @@ interface IncidentsPageState {
|
|||
selectedIncidentIds: Array<Alert['pk']>;
|
||||
affectedRows: { [key: string]: boolean };
|
||||
filters?: IncidentsFiltersType;
|
||||
pagination: Pagination;
|
||||
}
|
||||
|
||||
const ITEMS_PER_PAGE = 25;
|
||||
|
||||
@observer
|
||||
class Incidents extends React.Component<IncidentsPageProps, IncidentsPageState> {
|
||||
constructor(props: IncidentsPageProps) {
|
||||
super(props);
|
||||
|
||||
const { store } = props;
|
||||
const {
|
||||
store,
|
||||
query: { id, cursor: cursorQuery, start: startQuery, perpage: perpageQuery },
|
||||
} = props;
|
||||
|
||||
const cursor = cursorQuery || undefined;
|
||||
const start = !isNaN(startQuery) ? Number(startQuery) : 1;
|
||||
const itemsPerPage = !isNaN(perpageQuery) ? Number(perpageQuery) : ITEMS_PER_PAGE;
|
||||
|
||||
store.alertGroupStore.incidentsCursor = cursor;
|
||||
store.alertGroupStore.incidentsItemsPerPage = itemsPerPage;
|
||||
|
||||
this.state = {
|
||||
selectedIncidentIds: [],
|
||||
affectedRows: {},
|
||||
pagination: {
|
||||
start,
|
||||
end: start + itemsPerPage - 1,
|
||||
},
|
||||
};
|
||||
|
||||
store.alertGroupStore.updateBulkActions();
|
||||
store.alertGroupStore.updateSilenceOptions();
|
||||
}
|
||||
|
||||
async componentDidMount() {}
|
||||
|
||||
componentDidUpdate() {}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className={cx('root')}>
|
||||
|
|
@ -95,24 +112,55 @@ class Incidents extends React.Component<IncidentsPageProps, IncidentsPageState>
|
|||
);
|
||||
}
|
||||
|
||||
handleFiltersChange = (filters: IncidentsFiltersType) => {
|
||||
handleFiltersChange = (filters: IncidentsFiltersType, isOnMount: boolean) => {
|
||||
const { store } = this.props;
|
||||
|
||||
this.setState({ filters, selectedIncidentIds: [] });
|
||||
this.setState({
|
||||
filters,
|
||||
selectedIncidentIds: [],
|
||||
});
|
||||
|
||||
store.alertGroupStore.updateIncidentFilters(filters, true);
|
||||
if (!isOnMount) {
|
||||
this.setState({
|
||||
pagination: {
|
||||
start: 1,
|
||||
end: store.alertGroupStore.incidentsItemsPerPage,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
getLocationSrv().update({ query: { page: 'incidents', ...store.incidentFilters, p: store.incidentsPage } }); // todo fix
|
||||
store.alertGroupStore.updateIncidentFilters(filters, isOnMount);
|
||||
|
||||
getLocationSrv().update({ query: { page: 'incidents', ...store.alertGroupStore.incidentFilters } });
|
||||
};
|
||||
|
||||
onChangePagination = (page: number) => {
|
||||
onChangeCursor = (cursor: string, direction: 'prev' | 'next') => {
|
||||
const { store } = this.props;
|
||||
|
||||
store.alertGroupStore.setIncidentsPage(page);
|
||||
store.alertGroupStore.setIncidentsCursor(cursor);
|
||||
|
||||
this.setState({ selectedIncidentIds: [] });
|
||||
this.setState({
|
||||
selectedIncidentIds: [],
|
||||
pagination: {
|
||||
start:
|
||||
this.state.pagination.start + store.alertGroupStore.incidentsItemsPerPage * (direction === 'prev' ? -1 : 1),
|
||||
end: this.state.pagination.end + store.alertGroupStore.incidentsItemsPerPage * (direction === 'prev' ? -1 : 1),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
getLocationSrv().update({ partial: true, query: { p: store.incidentsPage } });
|
||||
handleChangeItemsPerPage = (value: number) => {
|
||||
const { store } = this.props;
|
||||
|
||||
store.alertGroupStore.setIncidentsItemsPerPage(value);
|
||||
|
||||
this.setState({
|
||||
selectedIncidentIds: [],
|
||||
pagination: {
|
||||
start: 1,
|
||||
end: store.alertGroupStore.incidentsItemsPerPage,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
renderBulkActions = () => {
|
||||
|
|
@ -214,7 +262,7 @@ class Incidents extends React.Component<IncidentsPageProps, IncidentsPageState>
|
|||
};
|
||||
|
||||
renderTable() {
|
||||
const { selectedIncidentIds, affectedRows } = this.state;
|
||||
const { selectedIncidentIds, affectedRows, pagination } = this.state;
|
||||
const { store } = this.props;
|
||||
const {
|
||||
teamStore: { currentTeam },
|
||||
|
|
@ -222,7 +270,8 @@ class Incidents extends React.Component<IncidentsPageProps, IncidentsPageState>
|
|||
const { alertGroupsLoading } = store.alertGroupStore;
|
||||
|
||||
const results = store.alertGroupStore.getAlertSearchResult('default');
|
||||
const count = get(store.alertGroupStore.alertsSearchResult, `default.count`);
|
||||
const prev = get(store.alertGroupStore.alertsSearchResult, `default.prev`);
|
||||
const next = get(store.alertGroupStore.alertsSearchResult, `default.next`);
|
||||
|
||||
if (results && !results.length) {
|
||||
return (
|
||||
|
|
@ -319,12 +368,22 @@ class Incidents extends React.Component<IncidentsPageProps, IncidentsPageState>
|
|||
data={results}
|
||||
columns={columns}
|
||||
// rowClassName={getUserRowClassNameFn(userPkToEdit, userStore.currentUserPk)}
|
||||
pagination={{
|
||||
page: store.incidentsPage,
|
||||
total: Math.ceil((count || 0) / ITEMS_PER_PAGE),
|
||||
onChange: this.onChangePagination,
|
||||
}}
|
||||
/>
|
||||
<div className={cx('pagination')}>
|
||||
<CursorPagination
|
||||
current={`${pagination.start}-${pagination.end}`}
|
||||
itemsPerPage={store.alertGroupStore.incidentsItemsPerPage}
|
||||
itemsPerPageOptions={[
|
||||
{ label: '25', value: 25 },
|
||||
{ label: '50', value: 50 },
|
||||
{ label: '100', value: 100 },
|
||||
]}
|
||||
prev={prev}
|
||||
next={next}
|
||||
onChange={this.onChangeCursor}
|
||||
onChangeItemsPerPage={this.handleChangeItemsPerPage}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -338,9 +397,20 @@ class Incidents extends React.Component<IncidentsPageProps, IncidentsPageState>
|
|||
}
|
||||
|
||||
renderTitle = (record: AlertType) => {
|
||||
const { store } = this.props;
|
||||
const {
|
||||
pagination: { start },
|
||||
} = this.state;
|
||||
|
||||
const { incidentsItemsPerPage, incidentsCursor } = store.alertGroupStore;
|
||||
|
||||
return (
|
||||
<VerticalGroup spacing="none" justify="center">
|
||||
<PluginLink query={{ page: 'incident', id: record.pk }}>{record.render_for_web.title}</PluginLink>
|
||||
<PluginLink
|
||||
query={{ page: 'incident', id: record.pk, cursor: incidentsCursor, perpage: incidentsItemsPerPage, start }}
|
||||
>
|
||||
{record.render_for_web.title}
|
||||
</PluginLink>
|
||||
{Boolean(record.dependent_alert_groups.length) && `+ ${record.dependent_alert_groups.length} attached`}
|
||||
</VerticalGroup>
|
||||
);
|
||||
|
|
@ -366,48 +436,6 @@ class Incidents extends React.Component<IncidentsPageProps, IncidentsPageState>
|
|||
|
||||
renderStatus(record: AlertType) {
|
||||
return getIncidentStatusTag(record);
|
||||
|
||||
/*if (record.resolved) {
|
||||
return (
|
||||
<div className={cx('status')}>
|
||||
<Tooltip title={`Resolved ${record.resolved_at_verbose}`}>
|
||||
<CheckCircleOutlined className={cx('icon-small')} style={{ color: '#52c41a' }} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (record.acknowledged) {
|
||||
return (
|
||||
<div className={cx('status')}>
|
||||
<Tooltip title={`Acknowledged ${record.acknowledged_at_verbose}`}>
|
||||
<Icon className={cx('icon-small')} component={AcknowledgedIncidentIcon} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (record.silenced) {
|
||||
const silencedUntilText = record.silenced_until
|
||||
? `Silenced until ${moment(record.silenced_until).toLocaleString()}`
|
||||
: 'Silenced forever';
|
||||
|
||||
return (
|
||||
<div className={cx('status')}>
|
||||
<Tooltip title={silencedUntilText}>
|
||||
<Icon className={cx('icon-small')} component={SilencedIncidentIcon} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cx('status')}>
|
||||
<Tooltip title={`Started ${record.started_at_verbose}`}>
|
||||
<Icon className={cx('icon-small')} component={NewIncidentIcon} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
);*/
|
||||
}
|
||||
|
||||
renderStartedAt(alert: AlertType) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue