From a0efa4e025ce5d81910de642018a42c2158bd5f2 Mon Sep 17 00:00:00 2001 From: Julia Date: Tue, 16 Aug 2022 15:38:52 +0300 Subject: [PATCH 01/77] Improve update shift logic using rotation start and until dates --- engine/apps/schedules/constants.py | 14 ++++++++ .../amixr_recurring_ical_events_adapter.py | 18 +++++++++- engine/apps/schedules/ical_utils.py | 34 +++++++++++++------ .../schedules/models/custom_on_call_shift.py | 13 +++---- 4 files changed, 61 insertions(+), 18 deletions(-) create mode 100644 engine/apps/schedules/constants.py diff --git a/engine/apps/schedules/constants.py b/engine/apps/schedules/constants.py new file mode 100644 index 00000000..719aa0b2 --- /dev/null +++ b/engine/apps/schedules/constants.py @@ -0,0 +1,14 @@ +import re + +ICAL_DATETIME_START = "DTSTART" +ICAL_DATETIME_END = "DTEND" +ICAL_DATETIME_STAMP = "DTSTAMP" +ICAL_SUMMARY = "SUMMARY" +ICAL_DESCRIPTION = "DESCRIPTION" +ICAL_ATTENDEE = "ATTENDEE" +ICAL_UID = "UID" +ICAL_RRULE = "RRULE" +ICAL_UNTIL = "UNTIL" +RE_PRIORITY = re.compile(r"^\[L(\d)\]") +RE_EVENT_UID_V1 = re.compile(r"amixr-([\w\d-]+)-U(\d+)-E(\d+)-S(\d+)") +RE_EVENT_UID_V2 = re.compile(r"oncall-([\w\d-]+)-PK([\w\d]+)-U(\d+)-E(\d+)-S(\d+)") diff --git a/engine/apps/schedules/ical_events/adapter/amixr_recurring_ical_events_adapter.py b/engine/apps/schedules/ical_events/adapter/amixr_recurring_ical_events_adapter.py index 4d17f497..5ad36c26 100644 --- a/engine/apps/schedules/ical_events/adapter/amixr_recurring_ical_events_adapter.py +++ b/engine/apps/schedules/ical_events/adapter/amixr_recurring_ical_events_adapter.py @@ -6,6 +6,7 @@ from django.utils import timezone from icalendar import Calendar, Event from recurring_ical_events import UnfoldableCalendar, compare_greater, is_event, time_span_contains_event +from apps.schedules.constants import ICAL_DATETIME_END, ICAL_DATETIME_STAMP, ICAL_DATETIME_START, ICAL_RRULE, ICAL_UNTIL from apps.schedules.ical_events.proxy.ical_proxy import IcalService EXTRA_LOOKUP_DAYS = 16 @@ -19,6 +20,17 @@ class AmixrUnfoldableCalendar(UnfoldableCalendar): So i took part of code from 0.1.20b0 but leave 0.1.16b in requirements. """ + class RepeatedEvent(UnfoldableCalendar.RepeatedEvent): + class Repetition(UnfoldableCalendar.RepeatedEvent.Repetition): + """ + A repetition of an event. Overridden version of + recurring_ical_events.UnfoldableCalendar.RepeatedEvent.Repetition. This is overridden to remove the 'RRULE' + param from ATTRIBUTES_TO_DELETE_ON_COPY, because the 'UNTIL' param must be stored in repetition events to + calculate its end date. + """ + + ATTRIBUTES_TO_DELETE_ON_COPY = ["RDATE", "EXDATE"] + def between(self, start, stop): """Return events at a time between start (inclusive) and end (inclusive)""" span_start = self.to_datetime(start) @@ -83,6 +95,10 @@ class AmixrRecurringIcalEventsAdapter(IcalService): ) def filter_extra_days(event): - return time_span_contains_event(start_date, end_date, event["DTSTART"].dt, event["DTEND"].dt) + event_start = max(event[ICAL_DATETIME_START].dt, event[ICAL_DATETIME_STAMP].dt) + event_end = event[ICAL_DATETIME_END].dt + if event.get(ICAL_RRULE, {}).get(ICAL_UNTIL): + event_end = min(event[ICAL_RRULE][ICAL_UNTIL][0], event[ICAL_DATETIME_END].dt) + return time_span_contains_event(start_date, end_date, event_start, event_end) return list(filter(filter_extra_days, events)) diff --git a/engine/apps/schedules/ical_utils.py b/engine/apps/schedules/ical_utils.py index 93092cc3..1c88828e 100644 --- a/engine/apps/schedules/ical_utils.py +++ b/engine/apps/schedules/ical_utils.py @@ -13,6 +13,20 @@ from django.db.models import Q from django.utils import timezone from icalendar import Calendar +from apps.schedules.constants import ( + ICAL_ATTENDEE, + ICAL_DATETIME_END, + ICAL_DATETIME_STAMP, + ICAL_DATETIME_START, + ICAL_DESCRIPTION, + ICAL_RRULE, + ICAL_SUMMARY, + ICAL_UID, + ICAL_UNTIL, + RE_EVENT_UID_V1, + RE_EVENT_UID_V2, + RE_PRIORITY, +) from apps.schedules.ical_events import ical_events from common.constants.role import Role from common.utils import timed_lru_cache @@ -68,15 +82,6 @@ def memoized_users_in_ical(usernames_from_ical, organization): return users_in_ical(usernames_from_ical, organization) -ICAL_DATETIME_START = "DTSTART" -ICAL_DATETIME_END = "DTEND" -ICAL_SUMMARY = "SUMMARY" -ICAL_DESCRIPTION = "DESCRIPTION" -ICAL_ATTENDEE = "ATTENDEE" -ICAL_UID = "UID" -RE_PRIORITY = re.compile(r"^\[L(\d)\]") -RE_EVENT_UID_V1 = re.compile(r"amixr-([\w\d-]+)-U(\d+)-E(\d+)-S(\d+)") -RE_EVENT_UID_V2 = re.compile(r"oncall-([\w\d-]+)-PK([\w\d]+)-U(\d+)-E(\d+)-S(\d+)") logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) @@ -171,8 +176,10 @@ def get_shifts_dict(calendar, calendar_type, schedule, datetime_start, datetime_ # Define on-call shift out of ical event that has the actual user if len(users) > 0 or with_empty_shifts: if type(event[ICAL_DATETIME_START].dt) == datetime.date: - start = event[ICAL_DATETIME_START].dt + start = max(event[ICAL_DATETIME_START].dt, event[ICAL_DATETIME_STAMP].dt.date()) end = event[ICAL_DATETIME_END].dt + if event.get(ICAL_RRULE, {}).get(ICAL_UNTIL): + end = min(event[ICAL_DATETIME_END].dt, event[ICAL_RRULE][ICAL_UNTIL][0].date()) if start <= date < end: result_date.append( { @@ -187,8 +194,13 @@ def get_shifts_dict(calendar, calendar_type, schedule, datetime_start, datetime_ } ) else: - start = event[ICAL_DATETIME_START].dt.astimezone(pytz.UTC) + start = max( + event[ICAL_DATETIME_START].dt.astimezone(pytz.UTC), + event[ICAL_DATETIME_STAMP].dt.astimezone(pytz.UTC), + ) end = event[ICAL_DATETIME_END].dt.astimezone(pytz.UTC) + if event.get(ICAL_RRULE, {}).get(ICAL_UNTIL): + end = min(event[ICAL_DATETIME_END].dt.astimezone(pytz.UTC), event[ICAL_RRULE][ICAL_UNTIL][0]) result_datetime.append( { diff --git a/engine/apps/schedules/models/custom_on_call_shift.py b/engine/apps/schedules/models/custom_on_call_shift.py index fe5aa46c..92bc1e7c 100644 --- a/engine/apps/schedules/models/custom_on_call_shift.py +++ b/engine/apps/schedules/models/custom_on_call_shift.py @@ -280,7 +280,7 @@ class CustomOnCallShift(models.Model): # rolling_users shift converts to several ical events if self.type in (CustomOnCallShift.TYPE_ROLLING_USERS_EVENT, CustomOnCallShift.TYPE_OVERRIDE): # generate initial iCal for counting rotation start date - event_ical = self.generate_ical(self.start, user_counter=0) + event_ical = self.generate_ical(self.start) rotations_created = 0 all_rotation_checked = False @@ -301,13 +301,14 @@ class CustomOnCallShift(models.Model): if not start: # means that rotation ends before next event starts all_rotation_checked = True break - elif start >= self.rotation_start: # event has already started, generate iCal for each user + elif start + self.duration > self.rotation_start: + # event has already started, generate iCal for each user for user_counter, user in enumerate(users, start=1): event_ical = self.generate_ical(start, user_counter, user, counter, time_zone) result += event_ical rotations_created += 1 else: # generate default iCal to calculate the date for the next rotation - event_ical = self.generate_ical(start, user_counter=0) + event_ical = self.generate_ical(start) if rotations_created == len(users_queue): # means that we generated iCal for every user group all_rotation_checked = True @@ -319,14 +320,14 @@ class CustomOnCallShift(models.Model): result += self.generate_ical(self.start, user_counter, user, time_zone=time_zone) return result - def generate_ical(self, start, user_counter, user=None, counter=1, time_zone="UTC"): + def generate_ical(self, start, user_counter=0, user=None, counter=1, time_zone="UTC"): event = Event() event["uid"] = f"oncall-{self.uuid}-PK{self.public_primary_key}-U{user_counter}-E{counter}-S{self.source}" if user: event.add("summary", self.get_summary_with_user_for_ical(user)) event.add("dtstart", self.convert_dt_to_schedule_timezone(start, time_zone)) event.add("dtend", self.convert_dt_to_schedule_timezone(start + self.duration, time_zone)) - event.add("dtstamp", timezone.now()) + event.add("dtstamp", self.rotation_start) if self.event_ical_rules: event.add("rrule", self.event_ical_rules) try: @@ -407,7 +408,7 @@ class CustomOnCallShift(models.Model): for event in ical_iter: if end_date: # end_date exists for long events with frequency weekly and monthly if end_date >= event.start >= next_event_start: - if event.start >= self.rotation_start: + if event.stop > self.rotation_start: next_event = event break else: From 1e3e39d5e027830b11f3fe0d73ce60f5e604d8b0 Mon Sep 17 00:00:00 2001 From: Julia Date: Wed, 17 Aug 2022 13:17:28 +0300 Subject: [PATCH 02/77] Add tests for events datetime calculation --- .../tests/test_custom_on_call_shift.py | 228 +++++++++++++++++- 1 file changed, 227 insertions(+), 1 deletion(-) diff --git a/engine/apps/schedules/tests/test_custom_on_call_shift.py b/engine/apps/schedules/tests/test_custom_on_call_shift.py index ab77dc1b..f62c7582 100644 --- a/engine/apps/schedules/tests/test_custom_on_call_shift.py +++ b/engine/apps/schedules/tests/test_custom_on_call_shift.py @@ -477,7 +477,7 @@ def test_rolling_users_with_diff_start_and_rotation_start_daily( "duration": timezone.timedelta(seconds=1800), "frequency": CustomOnCallShift.FREQUENCY_DAILY, "schedule": schedule, - "until": now + timezone.timedelta(days=6, minutes=1), + "until": now + timezone.timedelta(days=6, minutes=10), } rolling_users = [[user_1], [user_2], [user_3]] on_call_shift = make_on_call_shift( @@ -767,6 +767,232 @@ def test_rolling_users_with_diff_start_and_rotation_start_monthly_by_monthday( assert len(users_on_call) == 0 +@pytest.mark.django_db +def test_get_oncall_users_with_respect_to_rotation_start_and_until_dates_hourly( + make_organization_and_user, + make_on_call_shift, + make_schedule, +): + """Test calculation start and end event dates for one event with respect to rotation start and until""" + organization, user = make_organization_and_user() + + schedule = make_schedule(organization, schedule_class=OnCallScheduleWeb) + now = timezone.now().replace(microsecond=0) + + data = { + "priority_level": 1, + "start": now, + "rotation_start": now + timezone.timedelta(minutes=10), + "duration": timezone.timedelta(hours=1), + "frequency": CustomOnCallShift.FREQUENCY_HOURLY, + "schedule": schedule, + "until": now + timezone.timedelta(minutes=40), + } + rolling_users = [[user]] + on_call_shift = make_on_call_shift( + organization=organization, shift_type=CustomOnCallShift.TYPE_ROLLING_USERS_EVENT, **data + ) + on_call_shift.add_rolling_users(rolling_users) + + date = now + timezone.timedelta(minutes=2) + + user_on_call_dates = [date + timezone.timedelta(minutes=10), date + timezone.timedelta(minutes=35)] + nobody_on_call_dates = [ + date, # less than rotation start + date + timezone.timedelta(minutes=5), # less than rotation start + date + timezone.timedelta(minutes=40), # higher than until + ] + for dt in user_on_call_dates: + users_on_call = list_users_to_notify_from_ical(schedule, dt) + assert len(users_on_call) == 1 + assert user in users_on_call + + for dt in nobody_on_call_dates: + users_on_call = list_users_to_notify_from_ical(schedule, dt) + assert len(users_on_call) == 0 + + +@pytest.mark.django_db +def test_get_oncall_users_with_respect_to_rotation_start_and_until_dates_daily( + make_organization_and_user, + make_on_call_shift, + make_schedule, +): + """Test calculation start and end event dates for one event with respect to rotation start and until""" + organization, user = make_organization_and_user() + + schedule = make_schedule(organization, schedule_class=OnCallScheduleWeb) + now = timezone.now().replace(microsecond=0) + + data = { + "priority_level": 1, + "start": now, + "rotation_start": now + timezone.timedelta(hours=5), + "duration": timezone.timedelta(days=1), + "frequency": CustomOnCallShift.FREQUENCY_DAILY, + "schedule": schedule, + "until": now + timezone.timedelta(hours=15), + } + rolling_users = [[user]] + on_call_shift = make_on_call_shift( + organization=organization, shift_type=CustomOnCallShift.TYPE_ROLLING_USERS_EVENT, **data + ) + on_call_shift.add_rolling_users(rolling_users) + + date = now + timezone.timedelta(minutes=5) + + user_on_call_dates = [date + timezone.timedelta(hours=5), date + timezone.timedelta(hours=10)] + nobody_on_call_dates = [ + date, # less than rotation start + date + timezone.timedelta(hours=4), # less than rotation start + date + timezone.timedelta(hours=15), # higher than until + ] + + for dt in user_on_call_dates: + users_on_call = list_users_to_notify_from_ical(schedule, dt) + assert len(users_on_call) == 1 + assert user in users_on_call + + for dt in nobody_on_call_dates: + users_on_call = list_users_to_notify_from_ical(schedule, dt) + assert len(users_on_call) == 0 + + +@pytest.mark.django_db +def test_get_oncall_users_with_respect_to_rotation_start_and_until_dates_weekly( + make_organization_and_user, + make_on_call_shift, + make_schedule, +): + """Test calculation start and end event dates for one event with respect to rotation start and until""" + organization, user = make_organization_and_user() + + # simple weekly event + schedule = make_schedule(organization, schedule_class=OnCallScheduleWeb) + now = timezone.now().replace(microsecond=0) + + data = { + "priority_level": 1, + "start": now, + "rotation_start": now + timezone.timedelta(days=1), + "duration": timezone.timedelta(days=7), + "frequency": CustomOnCallShift.FREQUENCY_WEEKLY, + "schedule": schedule, + "until": now + timezone.timedelta(days=6), + "week_start": now.weekday(), + } + rolling_users = [[user]] + on_call_shift = make_on_call_shift( + organization=organization, shift_type=CustomOnCallShift.TYPE_ROLLING_USERS_EVENT, **data + ) + on_call_shift.add_rolling_users(rolling_users) + + date = now + timezone.timedelta(minutes=5) + + user_on_call_dates = [date + timezone.timedelta(days=1), date + timezone.timedelta(days=5)] + nobody_on_call_dates = [ + date, # less than rotation start + date + timezone.timedelta(hours=23), # less than rotation start + date + timezone.timedelta(days=6), # higher than until + ] + + for dt in user_on_call_dates: + users_on_call = list_users_to_notify_from_ical(schedule, dt) + assert len(users_on_call) == 1 + assert user in users_on_call + + for dt in nobody_on_call_dates: + users_on_call = list_users_to_notify_from_ical(schedule, dt) + assert len(users_on_call) == 0 + + # weekly event with by_day + schedule_2 = make_schedule(organization, schedule_class=OnCallScheduleWeb) + today_weekday = now.weekday() + weekdays = [today_weekday, (today_weekday + 1) % 7, (today_weekday + 2) % 7, (today_weekday + 5) % 7] + by_day = [CustomOnCallShift.ICAL_WEEKDAY_MAP[day] for day in weekdays] + data = { + "priority_level": 1, + "start": now, + "rotation_start": now + timezone.timedelta(days=1), + "duration": timezone.timedelta(hours=12), + "frequency": CustomOnCallShift.FREQUENCY_WEEKLY, + "schedule": schedule_2, + "until": now + timezone.timedelta(days=4, hours=23), + "week_start": today_weekday, + "by_day": by_day, + } + on_call_shift_2 = make_on_call_shift( + organization=organization, shift_type=CustomOnCallShift.TYPE_ROLLING_USERS_EVENT, **data + ) + on_call_shift_2.add_rolling_users(rolling_users) + + date = now + timezone.timedelta(minutes=5) + + user_on_call_dates = [date + timezone.timedelta(days=1), date + timezone.timedelta(days=2)] + nobody_on_call_dates = [ + date, # less than rotation start + date + timezone.timedelta(hours=23), # less than rotation start + date + timezone.timedelta(days=3), # out of by_day + date + timezone.timedelta(days=4), # out of by_day + date + timezone.timedelta(days=5), # higher than until + ] + + for dt in user_on_call_dates: + users_on_call = list_users_to_notify_from_ical(schedule_2, dt) + assert len(users_on_call) == 1 + assert user in users_on_call + + for dt in nobody_on_call_dates: + users_on_call = list_users_to_notify_from_ical(schedule_2, dt) + assert len(users_on_call) == 0 + + +@pytest.mark.django_db +def test_get_oncall_users_with_respect_to_rotation_start_and_until_dates_monthly( + make_organization_and_user, + make_on_call_shift, + make_schedule, +): + """Test calculation start and end event dates for one event with respect to rotation start and until""" + organization, user = make_organization_and_user() + + schedule = make_schedule(organization, schedule_class=OnCallScheduleWeb) + now = timezone.now().replace(microsecond=0) + + data = { + "priority_level": 1, + "start": now, + "rotation_start": now + timezone.timedelta(days=5), + "duration": timezone.timedelta(days=30), + "frequency": CustomOnCallShift.FREQUENCY_MONTHLY, + "schedule": schedule, + "until": now + timezone.timedelta(days=15), + } + rolling_users = [[user]] + on_call_shift = make_on_call_shift( + organization=organization, shift_type=CustomOnCallShift.TYPE_ROLLING_USERS_EVENT, **data + ) + on_call_shift.add_rolling_users(rolling_users) + + date = now + timezone.timedelta(minutes=5) + + user_on_call_dates = [date + timezone.timedelta(days=5), date + timezone.timedelta(days=10)] + nobody_on_call_dates = [ + date, # less than rotation start + date + timezone.timedelta(days=4), # less than rotation start + date + timezone.timedelta(days=15), # higher than until + ] + + for dt in user_on_call_dates: + users_on_call = list_users_to_notify_from_ical(schedule, dt) + assert len(users_on_call) == 1 + assert user in users_on_call + + for dt in nobody_on_call_dates: + users_on_call = list_users_to_notify_from_ical(schedule, dt) + assert len(users_on_call) == 0 + + @pytest.mark.django_db def test_get_oncall_users_for_empty_schedule( make_organization, From 5405af359843041ee015cca7998d714277535f6c Mon Sep 17 00:00:00 2001 From: Julia Date: Wed, 17 Aug 2022 13:54:56 +0300 Subject: [PATCH 03/77] Fix test --- engine/apps/schedules/tests/test_custom_on_call_shift.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/engine/apps/schedules/tests/test_custom_on_call_shift.py b/engine/apps/schedules/tests/test_custom_on_call_shift.py index f62c7582..c7967ca3 100644 --- a/engine/apps/schedules/tests/test_custom_on_call_shift.py +++ b/engine/apps/schedules/tests/test_custom_on_call_shift.py @@ -535,7 +535,7 @@ def test_rolling_users_with_diff_start_and_rotation_start_weekly( "duration": timezone.timedelta(seconds=1800), "frequency": CustomOnCallShift.FREQUENCY_WEEKLY, "schedule": schedule, - "until": now + timezone.timedelta(days=42, minutes=1), + "until": now + timezone.timedelta(days=42, minutes=10), } rolling_users = [[user_1], [user_2], [user_3]] on_call_shift = make_on_call_shift( From 736bc3b3485391a8c886b48459768b9d7f7e9895 Mon Sep 17 00:00:00 2001 From: Julia Date: Wed, 17 Aug 2022 17:04:27 +0300 Subject: [PATCH 04/77] Fix events dates calculation for old shifts and ical schedules --- .../amixr_recurring_ical_events_adapter.py | 27 +++++++++++++--- engine/apps/schedules/ical_utils.py | 31 ++++++++++++------- .../schedules/models/custom_on_call_shift.py | 8 +++-- .../tests/test_custom_on_call_shift.py | 5 +++ 4 files changed, 53 insertions(+), 18 deletions(-) diff --git a/engine/apps/schedules/ical_events/adapter/amixr_recurring_ical_events_adapter.py b/engine/apps/schedules/ical_events/adapter/amixr_recurring_ical_events_adapter.py index 5ad36c26..f1d74cff 100644 --- a/engine/apps/schedules/ical_events/adapter/amixr_recurring_ical_events_adapter.py +++ b/engine/apps/schedules/ical_events/adapter/amixr_recurring_ical_events_adapter.py @@ -2,11 +2,21 @@ from collections import defaultdict from datetime import datetime from typing import List +from django.apps import apps from django.utils import timezone from icalendar import Calendar, Event from recurring_ical_events import UnfoldableCalendar, compare_greater, is_event, time_span_contains_event -from apps.schedules.constants import ICAL_DATETIME_END, ICAL_DATETIME_STAMP, ICAL_DATETIME_START, ICAL_RRULE, ICAL_UNTIL +from apps.schedules.constants import ( + ICAL_DATETIME_END, + ICAL_DATETIME_STAMP, + ICAL_DATETIME_START, + ICAL_RRULE, + ICAL_UID, + ICAL_UNTIL, + RE_EVENT_UID_V1, + RE_EVENT_UID_V2, +) from apps.schedules.ical_events.proxy.ical_proxy import IcalService EXTRA_LOOKUP_DAYS = 16 @@ -95,10 +105,17 @@ class AmixrRecurringIcalEventsAdapter(IcalService): ) def filter_extra_days(event): - event_start = max(event[ICAL_DATETIME_START].dt, event[ICAL_DATETIME_STAMP].dt) - event_end = event[ICAL_DATETIME_END].dt - if event.get(ICAL_RRULE, {}).get(ICAL_UNTIL): - event_end = min(event[ICAL_RRULE][ICAL_UNTIL][0], event[ICAL_DATETIME_END].dt) + CustomOnCallShift = apps.get_model("schedules", "CustomOnCallShift") + match = RE_EVENT_UID_V2.match(event[ICAL_UID]) or RE_EVENT_UID_V1.match(event[ICAL_UID]) + # use different calculation rule for events from custom shifts generated at web + if match and int(match.groups()[-1]) == CustomOnCallShift.SOURCE_WEB: + event_start = max(event[ICAL_DATETIME_START].dt, event[ICAL_DATETIME_STAMP].dt) + event_end = event[ICAL_DATETIME_END].dt + if event.get(ICAL_RRULE, {}).get(ICAL_UNTIL): + event_end = min(event[ICAL_RRULE][ICAL_UNTIL][0], event[ICAL_DATETIME_END].dt) + else: + event_start = event[ICAL_DATETIME_START].dt + event_end = event[ICAL_DATETIME_END].dt return time_span_contains_event(start_date, end_date, event_start, event_end) return list(filter(filter_extra_days, events)) diff --git a/engine/apps/schedules/ical_utils.py b/engine/apps/schedules/ical_utils.py index 1c88828e..e44fa9ae 100644 --- a/engine/apps/schedules/ical_utils.py +++ b/engine/apps/schedules/ical_utils.py @@ -165,6 +165,7 @@ def list_of_oncall_shifts_from_ical( def get_shifts_dict(calendar, calendar_type, schedule, datetime_start, datetime_end, date, with_empty_shifts=False): + OnCallScheduleWeb = apps.get_model("schedules", "OnCallScheduleWeb") events = ical_events.get_events_from_ical_between(calendar, datetime_start, datetime_end) result_datetime = [] result_date = [] @@ -176,10 +177,14 @@ def get_shifts_dict(calendar, calendar_type, schedule, datetime_start, datetime_ # Define on-call shift out of ical event that has the actual user if len(users) > 0 or with_empty_shifts: if type(event[ICAL_DATETIME_START].dt) == datetime.date: - start = max(event[ICAL_DATETIME_START].dt, event[ICAL_DATETIME_STAMP].dt.date()) - end = event[ICAL_DATETIME_END].dt - if event.get(ICAL_RRULE, {}).get(ICAL_UNTIL): - end = min(event[ICAL_DATETIME_END].dt, event[ICAL_RRULE][ICAL_UNTIL][0].date()) + if isinstance(schedule, OnCallScheduleWeb): + start = max(event[ICAL_DATETIME_START].dt, event[ICAL_DATETIME_STAMP].dt.date()) + end = event[ICAL_DATETIME_END].dt + if event.get(ICAL_RRULE, {}).get(ICAL_UNTIL): + end = min(event[ICAL_DATETIME_END].dt, event[ICAL_RRULE][ICAL_UNTIL][0].date()) + else: + start = event[ICAL_DATETIME_START].dt + end = event[ICAL_DATETIME_END].dt if start <= date < end: result_date.append( { @@ -194,13 +199,17 @@ def get_shifts_dict(calendar, calendar_type, schedule, datetime_start, datetime_ } ) else: - start = max( - event[ICAL_DATETIME_START].dt.astimezone(pytz.UTC), - event[ICAL_DATETIME_STAMP].dt.astimezone(pytz.UTC), - ) - end = event[ICAL_DATETIME_END].dt.astimezone(pytz.UTC) - if event.get(ICAL_RRULE, {}).get(ICAL_UNTIL): - end = min(event[ICAL_DATETIME_END].dt.astimezone(pytz.UTC), event[ICAL_RRULE][ICAL_UNTIL][0]) + if isinstance(schedule, OnCallScheduleWeb): + start = max( + event[ICAL_DATETIME_START].dt.astimezone(pytz.UTC), + event[ICAL_DATETIME_STAMP].dt.astimezone(pytz.UTC), + ) + end = event[ICAL_DATETIME_END].dt.astimezone(pytz.UTC) + if event.get(ICAL_RRULE, {}).get(ICAL_UNTIL): + end = min(event[ICAL_DATETIME_END].dt.astimezone(pytz.UTC), event[ICAL_RRULE][ICAL_UNTIL][0]) + else: + start = event[ICAL_DATETIME_START].dt.astimezone(pytz.UTC) + end = event[ICAL_DATETIME_END].dt.astimezone(pytz.UTC) result_datetime.append( { diff --git a/engine/apps/schedules/models/custom_on_call_shift.py b/engine/apps/schedules/models/custom_on_call_shift.py index 92bc1e7c..3cc34397 100644 --- a/engine/apps/schedules/models/custom_on_call_shift.py +++ b/engine/apps/schedules/models/custom_on_call_shift.py @@ -301,7 +301,9 @@ class CustomOnCallShift(models.Model): if not start: # means that rotation ends before next event starts all_rotation_checked = True break - elif start + self.duration > self.rotation_start: + elif ( + self.source == CustomOnCallShift.SOURCE_WEB and start + self.duration > self.rotation_start + ) or start >= self.rotation_start: # event has already started, generate iCal for each user for user_counter, user in enumerate(users, start=1): event_ical = self.generate_ical(start, user_counter, user, counter, time_zone) @@ -408,7 +410,9 @@ class CustomOnCallShift(models.Model): for event in ical_iter: if end_date: # end_date exists for long events with frequency weekly and monthly if end_date >= event.start >= next_event_start: - if event.stop > self.rotation_start: + if ( + self.source == CustomOnCallShift.SOURCE_WEB and event.stop > self.rotation_start + ) or event.start >= self.rotation_start: next_event = event break else: diff --git a/engine/apps/schedules/tests/test_custom_on_call_shift.py b/engine/apps/schedules/tests/test_custom_on_call_shift.py index c7967ca3..45d516b6 100644 --- a/engine/apps/schedules/tests/test_custom_on_call_shift.py +++ b/engine/apps/schedules/tests/test_custom_on_call_shift.py @@ -787,6 +787,7 @@ def test_get_oncall_users_with_respect_to_rotation_start_and_until_dates_hourly( "frequency": CustomOnCallShift.FREQUENCY_HOURLY, "schedule": schedule, "until": now + timezone.timedelta(minutes=40), + "source": CustomOnCallShift.SOURCE_WEB, } rolling_users = [[user]] on_call_shift = make_on_call_shift( @@ -832,6 +833,7 @@ def test_get_oncall_users_with_respect_to_rotation_start_and_until_dates_daily( "frequency": CustomOnCallShift.FREQUENCY_DAILY, "schedule": schedule, "until": now + timezone.timedelta(hours=15), + "source": CustomOnCallShift.SOURCE_WEB, } rolling_users = [[user]] on_call_shift = make_on_call_shift( @@ -880,6 +882,7 @@ def test_get_oncall_users_with_respect_to_rotation_start_and_until_dates_weekly( "schedule": schedule, "until": now + timezone.timedelta(days=6), "week_start": now.weekday(), + "source": CustomOnCallShift.SOURCE_WEB, } rolling_users = [[user]] on_call_shift = make_on_call_shift( @@ -920,6 +923,7 @@ def test_get_oncall_users_with_respect_to_rotation_start_and_until_dates_weekly( "until": now + timezone.timedelta(days=4, hours=23), "week_start": today_weekday, "by_day": by_day, + "source": CustomOnCallShift.SOURCE_WEB, } on_call_shift_2 = make_on_call_shift( organization=organization, shift_type=CustomOnCallShift.TYPE_ROLLING_USERS_EVENT, **data @@ -967,6 +971,7 @@ def test_get_oncall_users_with_respect_to_rotation_start_and_until_dates_monthly "frequency": CustomOnCallShift.FREQUENCY_MONTHLY, "schedule": schedule, "until": now + timezone.timedelta(days=15), + "source": CustomOnCallShift.SOURCE_WEB, } rolling_users = [[user]] on_call_shift = make_on_call_shift( From bcf6ccb865a89e53cfc73de3a86576cdd349de65 Mon Sep 17 00:00:00 2001 From: Julia Date: Thu, 18 Aug 2022 13:55:07 +0300 Subject: [PATCH 05/77] Move calculation of event start and end dates to separate method --- .../amixr_recurring_ical_events_adapter.py | 24 +-------- engine/apps/schedules/ical_utils.py | 52 +++++++++++-------- 2 files changed, 31 insertions(+), 45 deletions(-) diff --git a/engine/apps/schedules/ical_events/adapter/amixr_recurring_ical_events_adapter.py b/engine/apps/schedules/ical_events/adapter/amixr_recurring_ical_events_adapter.py index f1d74cff..b5729313 100644 --- a/engine/apps/schedules/ical_events/adapter/amixr_recurring_ical_events_adapter.py +++ b/engine/apps/schedules/ical_events/adapter/amixr_recurring_ical_events_adapter.py @@ -2,22 +2,12 @@ from collections import defaultdict from datetime import datetime from typing import List -from django.apps import apps from django.utils import timezone from icalendar import Calendar, Event from recurring_ical_events import UnfoldableCalendar, compare_greater, is_event, time_span_contains_event -from apps.schedules.constants import ( - ICAL_DATETIME_END, - ICAL_DATETIME_STAMP, - ICAL_DATETIME_START, - ICAL_RRULE, - ICAL_UID, - ICAL_UNTIL, - RE_EVENT_UID_V1, - RE_EVENT_UID_V2, -) from apps.schedules.ical_events.proxy.ical_proxy import IcalService +from apps.schedules.ical_utils import get_start_and_end_with_respect_to_event_type EXTRA_LOOKUP_DAYS = 16 @@ -105,17 +95,7 @@ class AmixrRecurringIcalEventsAdapter(IcalService): ) def filter_extra_days(event): - CustomOnCallShift = apps.get_model("schedules", "CustomOnCallShift") - match = RE_EVENT_UID_V2.match(event[ICAL_UID]) or RE_EVENT_UID_V1.match(event[ICAL_UID]) - # use different calculation rule for events from custom shifts generated at web - if match and int(match.groups()[-1]) == CustomOnCallShift.SOURCE_WEB: - event_start = max(event[ICAL_DATETIME_START].dt, event[ICAL_DATETIME_STAMP].dt) - event_end = event[ICAL_DATETIME_END].dt - if event.get(ICAL_RRULE, {}).get(ICAL_UNTIL): - event_end = min(event[ICAL_RRULE][ICAL_UNTIL][0], event[ICAL_DATETIME_END].dt) - else: - event_start = event[ICAL_DATETIME_START].dt - event_end = event[ICAL_DATETIME_END].dt + event_start, event_end = get_start_and_end_with_respect_to_event_type(event) return time_span_contains_event(start_date, end_date, event_start, event_end) return list(filter(filter_extra_days, events)) diff --git a/engine/apps/schedules/ical_utils.py b/engine/apps/schedules/ical_utils.py index e44fa9ae..49206b8b 100644 --- a/engine/apps/schedules/ical_utils.py +++ b/engine/apps/schedules/ical_utils.py @@ -165,7 +165,6 @@ def list_of_oncall_shifts_from_ical( def get_shifts_dict(calendar, calendar_type, schedule, datetime_start, datetime_end, date, with_empty_shifts=False): - OnCallScheduleWeb = apps.get_model("schedules", "OnCallScheduleWeb") events = ical_events.get_events_from_ical_between(calendar, datetime_start, datetime_end) result_datetime = [] result_date = [] @@ -177,14 +176,7 @@ def get_shifts_dict(calendar, calendar_type, schedule, datetime_start, datetime_ # Define on-call shift out of ical event that has the actual user if len(users) > 0 or with_empty_shifts: if type(event[ICAL_DATETIME_START].dt) == datetime.date: - if isinstance(schedule, OnCallScheduleWeb): - start = max(event[ICAL_DATETIME_START].dt, event[ICAL_DATETIME_STAMP].dt.date()) - end = event[ICAL_DATETIME_END].dt - if event.get(ICAL_RRULE, {}).get(ICAL_UNTIL): - end = min(event[ICAL_DATETIME_END].dt, event[ICAL_RRULE][ICAL_UNTIL][0].date()) - else: - start = event[ICAL_DATETIME_START].dt - end = event[ICAL_DATETIME_END].dt + start, end = get_start_and_end_with_respect_to_event_type(event, full_day_event=True) if start <= date < end: result_date.append( { @@ -199,22 +191,11 @@ def get_shifts_dict(calendar, calendar_type, schedule, datetime_start, datetime_ } ) else: - if isinstance(schedule, OnCallScheduleWeb): - start = max( - event[ICAL_DATETIME_START].dt.astimezone(pytz.UTC), - event[ICAL_DATETIME_STAMP].dt.astimezone(pytz.UTC), - ) - end = event[ICAL_DATETIME_END].dt.astimezone(pytz.UTC) - if event.get(ICAL_RRULE, {}).get(ICAL_UNTIL): - end = min(event[ICAL_DATETIME_END].dt.astimezone(pytz.UTC), event[ICAL_RRULE][ICAL_UNTIL][0]) - else: - start = event[ICAL_DATETIME_START].dt.astimezone(pytz.UTC) - end = event[ICAL_DATETIME_END].dt.astimezone(pytz.UTC) - + start, end = get_start_and_end_with_respect_to_event_type(event) result_datetime.append( { - "start": start, - "end": end, + "start": start.astimezone(pytz.UTC), + "end": end.astimezone(pytz.UTC), "users": users, "missing_users": missing_users, "priority": priority, @@ -762,3 +743,28 @@ def convert_windows_timezone_to_iana(tz_name): logger.debug("Converting the timezone from Windows to IANA. '{}' -> '{}'".format(tz_name, result)) return result + + +def get_start_and_end_with_respect_to_event_type(event, full_day_event=False): + """ + Calculate start and end datetime (or dates for full_day_event) + """ + CustomOnCallShift = apps.get_model("schedules", "CustomOnCallShift") + + start = event[ICAL_DATETIME_START].dt + end = event[ICAL_DATETIME_END].dt + + match = RE_EVENT_UID_V2.match(event[ICAL_UID]) or RE_EVENT_UID_V1.match(event[ICAL_UID]) + # use different calculation rule for events from custom shifts generated at web + if match and int(match.groups()[-1]) == CustomOnCallShift.SOURCE_WEB: + rotation_start = event[ICAL_DATETIME_STAMP] + until = event.get(ICAL_RRULE, {}).get(ICAL_UNTIL) + + if full_day_event: + rotation_start = rotation_start.date() + until = until.date() if until else None + + start = max(start, rotation_start) + end = min(end, until) if until else end + + return start, end From 0fae8b7dcfbd95c26568a165a97825c1992c4b05 Mon Sep 17 00:00:00 2001 From: Julia Date: Thu, 18 Aug 2022 14:28:48 +0300 Subject: [PATCH 06/77] Fix circular import --- .../amixr_recurring_ical_events_adapter.py | 38 +++++++++++++++++-- .../schedules/ical_events/proxy/ical_proxy.py | 10 ++++- engine/apps/schedules/ical_utils.py | 33 ++-------------- 3 files changed, 47 insertions(+), 34 deletions(-) diff --git a/engine/apps/schedules/ical_events/adapter/amixr_recurring_ical_events_adapter.py b/engine/apps/schedules/ical_events/adapter/amixr_recurring_ical_events_adapter.py index b5729313..24f56ee5 100644 --- a/engine/apps/schedules/ical_events/adapter/amixr_recurring_ical_events_adapter.py +++ b/engine/apps/schedules/ical_events/adapter/amixr_recurring_ical_events_adapter.py @@ -1,13 +1,23 @@ from collections import defaultdict from datetime import datetime -from typing import List +from typing import List, Tuple +from django.apps import apps from django.utils import timezone from icalendar import Calendar, Event from recurring_ical_events import UnfoldableCalendar, compare_greater, is_event, time_span_contains_event +from apps.schedules.constants import ( + ICAL_DATETIME_END, + ICAL_DATETIME_STAMP, + ICAL_DATETIME_START, + ICAL_RRULE, + ICAL_UID, + ICAL_UNTIL, + RE_EVENT_UID_V1, + RE_EVENT_UID_V2, +) from apps.schedules.ical_events.proxy.ical_proxy import IcalService -from apps.schedules.ical_utils import get_start_and_end_with_respect_to_event_type EXTRA_LOOKUP_DAYS = 16 @@ -95,7 +105,29 @@ class AmixrRecurringIcalEventsAdapter(IcalService): ) def filter_extra_days(event): - event_start, event_end = get_start_and_end_with_respect_to_event_type(event) + event_start, event_end = self.get_start_and_end_with_respect_to_event_type(event) return time_span_contains_event(start_date, end_date, event_start, event_end) return list(filter(filter_extra_days, events)) + + def get_start_and_end_with_respect_to_event_type(self, event: Event) -> Tuple[timezone.datetime, timezone.datetime]: + """ + Calculate start and end datetime + """ + CustomOnCallShift = apps.get_model("schedules", "CustomOnCallShift") + + start = event[ICAL_DATETIME_START].dt + end = event[ICAL_DATETIME_END].dt + + match = RE_EVENT_UID_V2.match(event[ICAL_UID]) or RE_EVENT_UID_V1.match(event[ICAL_UID]) + # use different calculation rule for events from custom shifts generated at web + if match and int(match.groups()[-1]) == CustomOnCallShift.SOURCE_WEB: + rotation_start = event[ICAL_DATETIME_STAMP].dt + until_rrule = event.get(ICAL_RRULE, {}).get(ICAL_UNTIL) + if until_rrule: + until = until_rrule[0] + end = min(end, until) + + start = max(start, rotation_start) + + return start, end diff --git a/engine/apps/schedules/ical_events/proxy/ical_proxy.py b/engine/apps/schedules/ical_events/proxy/ical_proxy.py index a569c905..7418a7d1 100644 --- a/engine/apps/schedules/ical_events/proxy/ical_proxy.py +++ b/engine/apps/schedules/ical_events/proxy/ical_proxy.py @@ -1,7 +1,8 @@ from abc import ABC, abstractmethod from datetime import datetime -from typing import List +from typing import List, Tuple +from django.utils import timezone from icalendar import Calendar, Event @@ -10,6 +11,10 @@ class IcalService(ABC): def get_events_from_ical_between(self, calendar: Calendar, start_date: datetime, end_date: datetime) -> List[Event]: raise NotImplementedError + @abstractmethod + def get_start_and_end_with_respect_to_event_type(self, event: Event) -> Tuple[timezone.datetime, timezone.datetime]: + raise NotImplementedError + class IcalProxy(IcalService): def __init__(self, ical_adapter: IcalService): @@ -17,3 +22,6 @@ class IcalProxy(IcalService): def get_events_from_ical_between(self, calendar: Calendar, start_date: datetime, end_date: datetime) -> List[Event]: return self.ical_adapter.get_events_from_ical_between(calendar, start_date, end_date) + + def get_start_and_end_with_respect_to_event_type(self, event: Event) -> Tuple[timezone.datetime, timezone.datetime]: + return self.ical_adapter.get_start_and_end_with_respect_to_event_type(event) diff --git a/engine/apps/schedules/ical_utils.py b/engine/apps/schedules/ical_utils.py index 49206b8b..d78b99af 100644 --- a/engine/apps/schedules/ical_utils.py +++ b/engine/apps/schedules/ical_utils.py @@ -16,13 +16,10 @@ from icalendar import Calendar from apps.schedules.constants import ( ICAL_ATTENDEE, ICAL_DATETIME_END, - ICAL_DATETIME_STAMP, ICAL_DATETIME_START, ICAL_DESCRIPTION, - ICAL_RRULE, ICAL_SUMMARY, ICAL_UID, - ICAL_UNTIL, RE_EVENT_UID_V1, RE_EVENT_UID_V2, RE_PRIORITY, @@ -176,7 +173,8 @@ def get_shifts_dict(calendar, calendar_type, schedule, datetime_start, datetime_ # Define on-call shift out of ical event that has the actual user if len(users) > 0 or with_empty_shifts: if type(event[ICAL_DATETIME_START].dt) == datetime.date: - start, end = get_start_and_end_with_respect_to_event_type(event, full_day_event=True) + start = event[ICAL_DATETIME_START].dt + end = event[ICAL_DATETIME_END].dt if start <= date < end: result_date.append( { @@ -191,7 +189,7 @@ def get_shifts_dict(calendar, calendar_type, schedule, datetime_start, datetime_ } ) else: - start, end = get_start_and_end_with_respect_to_event_type(event) + start, end = ical_events.get_start_and_end_with_respect_to_event_type(event) result_datetime.append( { "start": start.astimezone(pytz.UTC), @@ -743,28 +741,3 @@ def convert_windows_timezone_to_iana(tz_name): logger.debug("Converting the timezone from Windows to IANA. '{}' -> '{}'".format(tz_name, result)) return result - - -def get_start_and_end_with_respect_to_event_type(event, full_day_event=False): - """ - Calculate start and end datetime (or dates for full_day_event) - """ - CustomOnCallShift = apps.get_model("schedules", "CustomOnCallShift") - - start = event[ICAL_DATETIME_START].dt - end = event[ICAL_DATETIME_END].dt - - match = RE_EVENT_UID_V2.match(event[ICAL_UID]) or RE_EVENT_UID_V1.match(event[ICAL_UID]) - # use different calculation rule for events from custom shifts generated at web - if match and int(match.groups()[-1]) == CustomOnCallShift.SOURCE_WEB: - rotation_start = event[ICAL_DATETIME_STAMP] - until = event.get(ICAL_RRULE, {}).get(ICAL_UNTIL) - - if full_day_event: - rotation_start = rotation_start.date() - until = until.date() if until else None - - start = max(start, rotation_start) - end = min(end, until) if until else end - - return start, end From 590650cd8f9ffd5ff616599a0fac6e9f717eed4d Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Thu, 18 Aug 2022 16:14:15 +0300 Subject: [PATCH 07/77] show GitHub star on navbar --- grafana-plugin/src/GrafanaPluginRootPage.tsx | 4 +-- .../AlertTemplates/AlertTemplatesForm.tsx | 9 ------- .../NavBar/NavBarSubtitle.module.css | 20 +++++++++++++++ .../src/components/NavBar/NavBarSubtitle.tsx | 25 +++++++++++++++++++ .../src/img/grafanaGlobalStyles.css | 4 +++ grafana-plugin/src/{ => style}/index.css | 0 grafana-plugin/src/{ => style}/vars.css | 0 .../src/utils/{hooks.ts => hooks.tsx} | 7 +++--- 8 files changed, 55 insertions(+), 14 deletions(-) create mode 100644 grafana-plugin/src/components/NavBar/NavBarSubtitle.module.css create mode 100644 grafana-plugin/src/components/NavBar/NavBarSubtitle.tsx rename grafana-plugin/src/{ => style}/index.css (100%) rename grafana-plugin/src/{ => style}/vars.css (100%) rename grafana-plugin/src/utils/{hooks.ts => hooks.tsx} (92%) diff --git a/grafana-plugin/src/GrafanaPluginRootPage.tsx b/grafana-plugin/src/GrafanaPluginRootPage.tsx index e258d30e..1115e2e5 100644 --- a/grafana-plugin/src/GrafanaPluginRootPage.tsx +++ b/grafana-plugin/src/GrafanaPluginRootPage.tsx @@ -14,8 +14,8 @@ import { rootStore } from 'state'; import { useStore } from 'state/useStore'; import { useNavModel } from 'utils/hooks'; -import './vars.css'; -import './index.css'; +import './style/vars.css'; +import './style/index.css'; import { AppFeature } from './state/features'; diff --git a/grafana-plugin/src/components/AlertTemplates/AlertTemplatesForm.tsx b/grafana-plugin/src/components/AlertTemplates/AlertTemplatesForm.tsx index ac749fa5..c23f16a8 100644 --- a/grafana-plugin/src/components/AlertTemplates/AlertTemplatesForm.tsx +++ b/grafana-plugin/src/components/AlertTemplates/AlertTemplatesForm.tsx @@ -111,15 +111,6 @@ const AlertTemplatesForm = (props: AlertTemplatesFormProps) => { return groups; }, [filteredTemplatesToRender]); - const handleChangeActiveTemplate = useCallback( - (templateName) => { - const template = groups[activeGroup].find((template: Template) => template.name === templateName); - - setActiveTemplate(template); - }, - [groups, activeGroup] - ); - const getGroupByTemplateName = (templateName: string) => { Object.values(groups).find((group) => { const foundTemplate = group.find((obj: any) => { diff --git a/grafana-plugin/src/components/NavBar/NavBarSubtitle.module.css b/grafana-plugin/src/components/NavBar/NavBarSubtitle.module.css new file mode 100644 index 00000000..360f2acc --- /dev/null +++ b/grafana-plugin/src/components/NavBar/NavBarSubtitle.module.css @@ -0,0 +1,20 @@ +:root { + --orange: #F5B73D; +} + +.navbar-container { + display: flex; + align-items: center; +} + +.navbar-star-icon { + fill: var(--orange); +} + +.navbar-heading { + padding: 4px; + margin-left: 8px; + border: 1px solid var(--gray-9); + width: initial; + font-size: 12px; +} diff --git a/grafana-plugin/src/components/NavBar/NavBarSubtitle.tsx b/grafana-plugin/src/components/NavBar/NavBarSubtitle.tsx new file mode 100644 index 00000000..8edf9c3d --- /dev/null +++ b/grafana-plugin/src/components/NavBar/NavBarSubtitle.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import cn from 'classnames/bind'; + +import { Card, Icon } from '@grafana/ui'; + +import { APP_SUBTITLE } from 'utils/consts'; + +import styles from './NavBarSubtitle.module.css'; + +const cx = cn.bind(styles); + +function NavBarSubtitle() { + return ( +
+ {APP_SUBTITLE} + + + Star us on GitHub + + +
+ ); +} + +export default NavBarSubtitle; diff --git a/grafana-plugin/src/img/grafanaGlobalStyles.css b/grafana-plugin/src/img/grafanaGlobalStyles.css index 4da2ec43..8eab37df 100644 --- a/grafana-plugin/src/img/grafanaGlobalStyles.css +++ b/grafana-plugin/src/img/grafanaGlobalStyles.css @@ -33,3 +33,7 @@ white-space: nowrap; } } + +.page-header__info-block { + flex-grow: 1; /* Stretch the navigation subtitle panel */ +} \ No newline at end of file diff --git a/grafana-plugin/src/index.css b/grafana-plugin/src/style/index.css similarity index 100% rename from grafana-plugin/src/index.css rename to grafana-plugin/src/style/index.css diff --git a/grafana-plugin/src/vars.css b/grafana-plugin/src/style/vars.css similarity index 100% rename from grafana-plugin/src/vars.css rename to grafana-plugin/src/style/vars.css diff --git a/grafana-plugin/src/utils/hooks.ts b/grafana-plugin/src/utils/hooks.tsx similarity index 92% rename from grafana-plugin/src/utils/hooks.ts rename to grafana-plugin/src/utils/hooks.tsx index b26ff31e..1757dc2a 100644 --- a/grafana-plugin/src/utils/hooks.ts +++ b/grafana-plugin/src/utils/hooks.tsx @@ -1,11 +1,12 @@ -import { useEffect, useRef, useState } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import { useMemo } from 'react'; import { AppRootProps, NavModelItem } from '@grafana/data'; +import NavBarSubtitle from 'components/NavBar/NavBarSubtitle'; import { PageDefinition } from 'pages'; -import { APP_TITLE, APP_SUBTITLE } from './consts'; +import { APP_TITLE } from './consts'; type Args = { meta: AppRootProps['meta']; @@ -54,7 +55,7 @@ export function useNavModel({ meta, pages, path, page, grafanaUser, enableLiveSe const node = { text: APP_TITLE, img: meta.info.logos.large, - subTitle: APP_SUBTITLE, + subTitle: , url: path, children: tabs, }; From dcba5ba5430e49b131d02ffe8915600ea0fa67c4 Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Thu, 18 Aug 2022 16:29:20 +0300 Subject: [PATCH 08/77] polished display --- grafana-plugin/src/assets/img/github_star.svg | 3 +++ .../src/components/NavBar/NavBarSubtitle.module.css | 13 +++++++------ .../src/components/NavBar/NavBarSubtitle.tsx | 9 +++++---- 3 files changed, 15 insertions(+), 10 deletions(-) create mode 100644 grafana-plugin/src/assets/img/github_star.svg diff --git a/grafana-plugin/src/assets/img/github_star.svg b/grafana-plugin/src/assets/img/github_star.svg new file mode 100644 index 00000000..9a69aed0 --- /dev/null +++ b/grafana-plugin/src/assets/img/github_star.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/grafana-plugin/src/components/NavBar/NavBarSubtitle.module.css b/grafana-plugin/src/components/NavBar/NavBarSubtitle.module.css index 360f2acc..61c1346a 100644 --- a/grafana-plugin/src/components/NavBar/NavBarSubtitle.module.css +++ b/grafana-plugin/src/components/NavBar/NavBarSubtitle.module.css @@ -1,20 +1,21 @@ -:root { - --orange: #F5B73D; -} - .navbar-container { display: flex; align-items: center; } .navbar-star-icon { - fill: var(--orange); + margin-right: 4px; } .navbar-heading { padding: 4px; - margin-left: 8px; + margin: 0 0 0 8px; border: 1px solid var(--gray-9); width: initial; font-size: 12px; } + +.navbar-link { + display: flex; + align-items: center; +} \ No newline at end of file diff --git a/grafana-plugin/src/components/NavBar/NavBarSubtitle.tsx b/grafana-plugin/src/components/NavBar/NavBarSubtitle.tsx index 8edf9c3d..283e7807 100644 --- a/grafana-plugin/src/components/NavBar/NavBarSubtitle.tsx +++ b/grafana-plugin/src/components/NavBar/NavBarSubtitle.tsx @@ -1,10 +1,11 @@ import React from 'react'; + import cn from 'classnames/bind'; - import { Card, Icon } from '@grafana/ui'; - import { APP_SUBTITLE } from 'utils/consts'; +import gitHubStarSVG from 'assets/img/github_star.svg'; + import styles from './NavBarSubtitle.module.css'; const cx = cn.bind(styles); @@ -14,8 +15,8 @@ function NavBarSubtitle() { From f7a11386ee1a09e03494254b463192a950c91c08 Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Thu, 18 Aug 2022 16:34:27 +0300 Subject: [PATCH 09/77] linter --- grafana-plugin/src/components/NavBar/NavBarSubtitle.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/grafana-plugin/src/components/NavBar/NavBarSubtitle.tsx b/grafana-plugin/src/components/NavBar/NavBarSubtitle.tsx index 283e7807..b4ed66bf 100644 --- a/grafana-plugin/src/components/NavBar/NavBarSubtitle.tsx +++ b/grafana-plugin/src/components/NavBar/NavBarSubtitle.tsx @@ -1,10 +1,10 @@ import React from 'react'; +import { Card } from '@grafana/ui'; import cn from 'classnames/bind'; -import { Card, Icon } from '@grafana/ui'; -import { APP_SUBTITLE } from 'utils/consts'; import gitHubStarSVG from 'assets/img/github_star.svg'; +import { APP_SUBTITLE } from 'utils/consts'; import styles from './NavBarSubtitle.module.css'; @@ -15,8 +15,8 @@ function NavBarSubtitle() { From 09cae43d1c5392c07200adf7dbd8a0bcaf93d860 Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Thu, 18 Aug 2022 18:01:09 +0300 Subject: [PATCH 10/77] conditional on OSS license, refactored to separate const --- grafana-plugin/src/components/NavBar/NavBarSubtitle.tsx | 8 +++++++- .../containers/DefaultPageLayout/DefaultPageLayout.tsx | 5 +++-- .../src/containers/PluginConfigPage/PluginConfigPage.tsx | 4 ++-- grafana-plugin/src/utils/consts.ts | 2 ++ 4 files changed, 14 insertions(+), 5 deletions(-) diff --git a/grafana-plugin/src/components/NavBar/NavBarSubtitle.tsx b/grafana-plugin/src/components/NavBar/NavBarSubtitle.tsx index b4ed66bf..083ecd3b 100644 --- a/grafana-plugin/src/components/NavBar/NavBarSubtitle.tsx +++ b/grafana-plugin/src/components/NavBar/NavBarSubtitle.tsx @@ -4,13 +4,19 @@ import { Card } from '@grafana/ui'; import cn from 'classnames/bind'; import gitHubStarSVG from 'assets/img/github_star.svg'; -import { APP_SUBTITLE } from 'utils/consts'; +import { useStore } from 'state/useStore'; +import { APP_SUBTITLE, GRAFANA_LICENSE_OSS } from 'utils/consts'; import styles from './NavBarSubtitle.module.css'; const cx = cn.bind(styles); function NavBarSubtitle() { + const store = useStore(); + if (store.backendLicense === GRAFANA_LICENSE_OSS) { + return APP_SUBTITLE; + } + return (
{APP_SUBTITLE} diff --git a/grafana-plugin/src/containers/DefaultPageLayout/DefaultPageLayout.tsx b/grafana-plugin/src/containers/DefaultPageLayout/DefaultPageLayout.tsx index 74665f5b..5cefb3b1 100644 --- a/grafana-plugin/src/containers/DefaultPageLayout/DefaultPageLayout.tsx +++ b/grafana-plugin/src/containers/DefaultPageLayout/DefaultPageLayout.tsx @@ -3,13 +3,14 @@ import React, { FC, useEffect, useState, useCallback } from 'react'; import { AppRootProps } from '@grafana/data'; import { getLocationSrv } from '@grafana/runtime'; -import { Alert, Button } from '@grafana/ui'; +import { Alert } from '@grafana/ui'; import cn from 'classnames/bind'; import { observer } from 'mobx-react'; import PluginLink from 'components/PluginLink/PluginLink'; import { useStore } from 'state/useStore'; import { UserAction } from 'state/userAction'; +import { GRAFANA_LICENSE_OSS } from 'utils/consts'; import { useForceUpdate } from 'utils/hooks'; import { getItem, setItem } from 'utils/localStorage'; import sanitize from 'utils/sanitize'; @@ -89,7 +90,7 @@ const DefaultPageLayout: FC = observer((props) => { /> )} - {store.backendLicense === 'OpenSource' && + {store.backendLicense === GRAFANA_LICENSE_OSS && store.backendVersion && plugin?.version && store.backendVersion !== plugin?.version && ( diff --git a/grafana-plugin/src/containers/PluginConfigPage/PluginConfigPage.tsx b/grafana-plugin/src/containers/PluginConfigPage/PluginConfigPage.tsx index 0f7090ec..60c0086f 100644 --- a/grafana-plugin/src/containers/PluginConfigPage/PluginConfigPage.tsx +++ b/grafana-plugin/src/containers/PluginConfigPage/PluginConfigPage.tsx @@ -25,7 +25,7 @@ import WithConfirm from 'components/WithConfirm/WithConfirm'; import logo from 'img/logo.svg'; import { makeRequest } from 'network'; import { createGrafanaToken, getPluginSyncStatus, startPluginSync, updateGrafanaToken } from 'state/plugin'; -import { openNotification } from 'utils'; +import { GRAFANA_LICENSE_OSS } from 'utils/consts'; import { getItem, setItem } from 'utils/localStorage'; import styles from './PluginConfigPage.module.css'; @@ -152,7 +152,7 @@ export const PluginConfigPage = (props: Props) => { setPluginStatusMessage( `Connected to OnCall${versionInfo}\n - OnCall URL: ${plugin.meta.jsonData.onCallApiUrl}\n - Grafana URL: ${plugin.meta.jsonData.grafanaUrl}` ); - setIsSelfHostedInstall(plugin.meta.jsonData?.license === 'OpenSource'); + setIsSelfHostedInstall(plugin.meta.jsonData?.license === GRAFANA_LICENSE_OSS); setPluginStatusOk(true); } else { setPluginStatusMessage( diff --git a/grafana-plugin/src/utils/consts.ts b/grafana-plugin/src/utils/consts.ts index a7d5b987..3a5a2f79 100644 --- a/grafana-plugin/src/utils/consts.ts +++ b/grafana-plugin/src/utils/consts.ts @@ -3,5 +3,7 @@ import plugin from '../../package.json'; // eslint-disable-line export const APP_TITLE = 'Grafana OnCall'; export const APP_SUBTITLE = `Developer-friendly incident response (${plugin?.version})`; +export const GRAFANA_LICENSE_OSS = 'OpenSource'; + // Reusable breakpoint sizes export const BREAKPOINT_TABS = 1024; From 64bb33d31e1f3837aa3ef4bfb047b40ab1d3a1be Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Fri, 19 Aug 2022 12:41:00 +0300 Subject: [PATCH 11/77] pass backendLicense to NavBarSubtitle --- grafana-plugin/src/GrafanaPluginRootPage.tsx | 3 ++ .../src/components/NavBar/NavBarSubtitle.tsx | 28 +++++++++---------- grafana-plugin/src/utils/hooks.tsx | 14 ++++++++-- 3 files changed, 28 insertions(+), 17 deletions(-) diff --git a/grafana-plugin/src/GrafanaPluginRootPage.tsx b/grafana-plugin/src/GrafanaPluginRootPage.tsx index 1115e2e5..870c1cae 100644 --- a/grafana-plugin/src/GrafanaPluginRootPage.tsx +++ b/grafana-plugin/src/GrafanaPluginRootPage.tsx @@ -91,6 +91,7 @@ export const Root = observer((props: AppRootProps) => { const pathWithoutLeadingSlash = path.replace(/^\//, ''); const store = useStore(); + const { backendLicense } = store; useEffect(() => { store.updateBasicData(); @@ -120,11 +121,13 @@ export const Root = observer((props: AppRootProps) => { grafanaUser: window.grafanaBootData.user, enableLiveSettings: store.hasFeature(AppFeature.LiveSettings), enableCloudPage: store.hasFeature(AppFeature.CloudConnection), + backendLicense, }), [meta, pathWithoutLeadingSlash, page, store.features] ) ); useEffect(() => { + /* @ts-ignore */ onNavChanged(navModel); }, [navModel, onNavChanged]); diff --git a/grafana-plugin/src/components/NavBar/NavBarSubtitle.tsx b/grafana-plugin/src/components/NavBar/NavBarSubtitle.tsx index 083ecd3b..e9fe292e 100644 --- a/grafana-plugin/src/components/NavBar/NavBarSubtitle.tsx +++ b/grafana-plugin/src/components/NavBar/NavBarSubtitle.tsx @@ -4,29 +4,27 @@ import { Card } from '@grafana/ui'; import cn from 'classnames/bind'; import gitHubStarSVG from 'assets/img/github_star.svg'; -import { useStore } from 'state/useStore'; import { APP_SUBTITLE, GRAFANA_LICENSE_OSS } from 'utils/consts'; import styles from './NavBarSubtitle.module.css'; const cx = cn.bind(styles); -function NavBarSubtitle() { - const store = useStore(); - if (store.backendLicense === GRAFANA_LICENSE_OSS) { - return APP_SUBTITLE; +function NavBarSubtitle({ backendLicense }: { backendLicense: string }) { + if (backendLicense === GRAFANA_LICENSE_OSS) { + return ( +
+ {APP_SUBTITLE} + + + Star us on GitHub + + +
+ ); } - return ( -
- {APP_SUBTITLE} - - - Star us on GitHub - - -
- ); + return <>{APP_SUBTITLE}; } export default NavBarSubtitle; diff --git a/grafana-plugin/src/utils/hooks.tsx b/grafana-plugin/src/utils/hooks.tsx index 1757dc2a..93052831 100644 --- a/grafana-plugin/src/utils/hooks.tsx +++ b/grafana-plugin/src/utils/hooks.tsx @@ -18,6 +18,7 @@ type Args = { }; enableLiveSettings: boolean; enableCloudPage: boolean; + backendLicense: string; }; export function useForceUpdate() { @@ -25,7 +26,16 @@ export function useForceUpdate() { return () => setValue((value) => value + 1); } -export function useNavModel({ meta, pages, path, page, grafanaUser, enableLiveSettings, enableCloudPage }: Args) { +export function useNavModel({ + meta, + pages, + path, + page, + grafanaUser, + enableLiveSettings, + enableCloudPage, + backendLicense, +}: Args) { return useMemo(() => { const tabs: NavModelItem[] = []; @@ -55,7 +65,7 @@ export function useNavModel({ meta, pages, path, page, grafanaUser, enableLiveSe const node = { text: APP_TITLE, img: meta.info.logos.large, - subTitle: , + subTitle: , url: path, children: tabs, }; From a0f1fb36eb4743c99fec6cdffa5025f703899ffa Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Mon, 22 Aug 2022 12:55:31 +0300 Subject: [PATCH 12/77] display incident raw request data --- .../src/models/alertgroup/alertgroup.ts | 6 +++ .../src/pages/incident/Incident.module.css | 10 ++++ .../src/pages/incident/Incident.tsx | 54 +++++++++++++++---- 3 files changed, 61 insertions(+), 9 deletions(-) diff --git a/grafana-plugin/src/models/alertgroup/alertgroup.ts b/grafana-plugin/src/models/alertgroup/alertgroup.ts index d7c6fa4f..66496780 100644 --- a/grafana-plugin/src/models/alertgroup/alertgroup.ts +++ b/grafana-plugin/src/models/alertgroup/alertgroup.ts @@ -298,6 +298,12 @@ export class AlertGroupStore extends BaseStore { }); } + @action + async getRawResponseForIncident(pk: Alert['pk']) { + const result = await makeRequest(`/alerts/${pk}`, {}) + return result + } + @action async getNewIncidentsStats() { const result = await makeRequest(`${this.path}stats/`, { diff --git a/grafana-plugin/src/pages/incident/Incident.module.css b/grafana-plugin/src/pages/incident/Incident.module.css index 5c22cf6d..80e8bb71 100644 --- a/grafana-plugin/src/pages/incident/Incident.module.css +++ b/grafana-plugin/src/pages/incident/Incident.module.css @@ -44,6 +44,7 @@ .collapse { margin-top: 16px; + position: relative; } .column { @@ -103,3 +104,12 @@ .timeline-filter { margin-bottom: 24px; } + +.view-response-button { + margin-left: auto; +} + +.incident-group-row > div { + display: flex; + flex-grow: 1; +} \ No newline at end of file diff --git a/grafana-plugin/src/pages/incident/Incident.tsx b/grafana-plugin/src/pages/incident/Incident.tsx index 73690b9e..6d5a0ce5 100644 --- a/grafana-plugin/src/pages/incident/Incident.tsx +++ b/grafana-plugin/src/pages/incident/Incident.tsx @@ -13,6 +13,8 @@ import { ToolbarButton, VerticalGroup, Field, + Modal, + Label, } from '@grafana/ui'; import cn from 'classnames/bind'; import { observer } from 'mobx-react'; @@ -48,6 +50,7 @@ import sanitize from 'utils/sanitize'; import { getActionButtons, getIncidentStatusTag, renderRelatedUsers } from './Incident.helpers'; import styles from './Incident.module.css'; +import SourceCode from 'components/SourceCode/SourceCode'; const cx = cn.bind(styles); @@ -62,6 +65,8 @@ interface IncidentPageState { teamToSwitch?: { name: string; id: string }; timelineFilter: string; resolutionNoteText: string; + currentModalIncidental: Alert; + currentIncidentRawResponse: { id: string, raw_request_data: any }; } @observer @@ -71,6 +76,8 @@ class IncidentPage extends React.Component resolutionNoteText: '', wrongTeamError: false, wrongTeamNoPermissions: false, + currentModalIncidental: undefined, + currentIncidentRawResponse: undefined, }; componentDidMount() { @@ -93,6 +100,8 @@ class IncidentPage extends React.Component query: { id }, } = this.props; + console.log('network request'); + store.alertGroupStore.getAlert(id).catch((error) => { if (error.response) { if (error.response.status === 404) { @@ -129,8 +138,6 @@ class IncidentPage extends React.Component const { alertReceiveChannelStore } = store; - const { isMobile } = store; - const { alerts } = store.alertGroupStore; const incident = alerts.get(id); @@ -174,15 +181,14 @@ class IncidentPage extends React.Component ); } - const integration = store.alertReceiveChannelStore.getIntegration(incident.alert_receive_channel); - return ( <> + {this.renderModalForIncident()}
{this.renderHeader()}
- {this.renderIncident(incident)} + {this.renderIncident(incident, true)} {this.renderGroupedIncidents()} {this.renderAttachedIncidents()}
@@ -335,7 +341,7 @@ class IncidentPage extends React.Component this.setState({ showAttachIncidentForm: true }); }; - renderIncident = (incident: Alert) => { + renderIncident = (incident: Alert, isMainIncident: boolean) => { let datetimeReference; if (incident.last_alert_at || incident.created_at) { @@ -344,14 +350,19 @@ class IncidentPage extends React.Component } return ( -
- +
+ {incident.inside_organization_number ? `#${incident.inside_organization_number} ${incident.render_for_web.title}` : incident.render_for_web.title} {datetimeReference} + {!isMainIncident && ( + + this.openIncidentResponse(ev, incident)} /> + + )}
} contentClassName={cx('incidents-content')} > - {alerts.map(this.renderIncident)} + {alerts.map((alert) => this.renderIncident(alert, false))} ); } + // @ts-ignore + openIncidentResponse = async (e: React.SyntheticEvent, incident: AlertType) => { + const currentIncidentRawResponse = await this.props.store.alertGroupStore.getRawResponseForIncident(incident['pk']); + + this.setState({ currentModalIncidental: incident, currentIncidentRawResponse }); + }; + + renderModalForIncident() { + const { currentModalIncidental, currentIncidentRawResponse: { raw_request_data: incidentRawRequestData } } = this.state; + + return ( + this.setState({ currentModalIncidental: undefined })} + closeOnEscape + isOpen={!!currentModalIncidental} + title="Incident Payload" + > + + + {JSON.stringify(incidentRawRequestData, null, 4)} + + + ); + } + renderAttachedIncidents = () => { const { store, From 32e14c165b76632806c54faae54cd40e735e493c Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Mon, 22 Aug 2022 16:47:24 +0300 Subject: [PATCH 13/77] refactored incidents inside IncidentsPage --- .../src/models/alertgroup/alertgroup.types.ts | 20 +- .../src/pages/incident/Incident.tsx | 305 +++++++++--------- .../src/pages/incidents/Incidents.tsx | 1 - 3 files changed, 172 insertions(+), 154 deletions(-) diff --git a/grafana-plugin/src/models/alertgroup/alertgroup.types.ts b/grafana-plugin/src/models/alertgroup/alertgroup.types.ts index 2ae7498c..8f5e231b 100644 --- a/grafana-plugin/src/models/alertgroup/alertgroup.types.ts +++ b/grafana-plugin/src/models/alertgroup/alertgroup.types.ts @@ -37,12 +37,18 @@ export interface TimeLineItem { type: number; } +export interface GroupedAlert { + created_at: string; + id: string; + render_for_web: RenderForWeb; +} + export interface Alert { pk: string; title: string; message: string; image_url: string; - alerts?: any[]; + alerts?: GroupedAlert[]; acknowledged: boolean; created_at: string; acknowledged_at: string; @@ -53,11 +59,7 @@ export interface Alert { related_users: User[]; render_after_resolve_report_json?: TimeLineItem[]; render_for_slack: { attachments: any[] }; - render_for_web: { - message: any; - title: any; - image_url: string; - }; + render_for_web: RenderForWeb; alerts_count: number; inside_organization_number: number; resolved: boolean; @@ -83,3 +85,9 @@ export interface Alert { has_pormortem?: boolean; // not implemented yet } + +interface RenderForWeb { + message: any; + title: any; + image_url: string; +} diff --git a/grafana-plugin/src/pages/incident/Incident.tsx b/grafana-plugin/src/pages/incident/Incident.tsx index 6d5a0ce5..d6cd1818 100644 --- a/grafana-plugin/src/pages/incident/Incident.tsx +++ b/grafana-plugin/src/pages/incident/Incident.tsx @@ -1,4 +1,4 @@ -import React, { SyntheticEvent } from 'react'; +import React, { useState, SyntheticEvent } from 'react'; import { AppRootProps } from '@grafana/data'; import { getLocationSrv } from '@grafana/runtime'; @@ -39,6 +39,7 @@ import { AlertAction, TimeLineItem, TimeLineRealm, + GroupedAlert, } from 'models/alertgroup/alertgroup.types'; import { ResolutionNoteSourceTypesToDisplayName } from 'models/resolution_note/resolution_note.types'; import { WithStoreProps } from 'state/types'; @@ -51,6 +52,7 @@ import { getActionButtons, getIncidentStatusTag, renderRelatedUsers } from './In import styles from './Incident.module.css'; import SourceCode from 'components/SourceCode/SourceCode'; +import { useStore } from 'state/useStore'; const cx = cn.bind(styles); @@ -65,8 +67,6 @@ interface IncidentPageState { teamToSwitch?: { name: string; id: string }; timelineFilter: string; resolutionNoteText: string; - currentModalIncidental: Alert; - currentIncidentRawResponse: { id: string, raw_request_data: any }; } @observer @@ -76,8 +76,6 @@ class IncidentPage extends React.Component resolutionNoteText: '', wrongTeamError: false, wrongTeamNoPermissions: false, - currentModalIncidental: undefined, - currentIncidentRawResponse: undefined, }; componentDidMount() { @@ -100,8 +98,6 @@ class IncidentPage extends React.Component query: { id }, } = this.props; - console.log('network request'); - store.alertGroupStore.getAlert(id).catch((error) => { if (error.response) { if (error.response.status === 404) { @@ -183,14 +179,13 @@ class IncidentPage extends React.Component return ( <> - {this.renderModalForIncident()}
{this.renderHeader()}
- {this.renderIncident(incident, true)} - {this.renderGroupedIncidents()} - {this.renderAttachedIncidents()} + + +
{this.renderTimeline()}
@@ -341,142 +336,6 @@ class IncidentPage extends React.Component this.setState({ showAttachIncidentForm: true }); }; - renderIncident = (incident: Alert, isMainIncident: boolean) => { - 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 ( -
- - - {incident.inside_organization_number - ? `#${incident.inside_organization_number} ${incident.render_for_web.title}` - : incident.render_for_web.title} - - {datetimeReference} - {!isMainIncident && ( - - this.openIncidentResponse(ev, incident)} /> - - )} - -
- {incident.render_for_web.image_url && } -
- ); - }; - - renderGroupedIncidents() { - const { - store, - query: { id }, - } = this.props; - - 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); - - return ( - - {incident.alerts_count} Grouped Alerts - - (latest {latestAlertMoment.fromNow()}, {latestAlertMoment.toString()}) - - - } - contentClassName={cx('incidents-content')} - > - {alerts.map((alert) => this.renderIncident(alert, false))} - - ); - } - - // @ts-ignore - openIncidentResponse = async (e: React.SyntheticEvent, incident: AlertType) => { - const currentIncidentRawResponse = await this.props.store.alertGroupStore.getRawResponseForIncident(incident['pk']); - - this.setState({ currentModalIncidental: incident, currentIncidentRawResponse }); - }; - - renderModalForIncident() { - const { currentModalIncidental, currentIncidentRawResponse: { raw_request_data: incidentRawRequestData } } = this.state; - - return ( - this.setState({ currentModalIncidental: undefined })} - closeOnEscape - isOpen={!!currentModalIncidental} - title="Incident Payload" - > - - - {JSON.stringify(incidentRawRequestData, null, 4)} - - - ); - } - - renderAttachedIncidents = () => { - const { - store, - query: { id }, - } = this.props; - - const incident = store.alertGroupStore.alerts.get(id); - - if (!incident.dependent_alert_groups.length) { - return null; - } - - const alerts = incident.dependent_alert_groups; - - return ( - {incident.dependent_alert_groups.length} Attached Incidents} - contentClassName={cx('incidents-content')} - > - {alerts.map((incident) => { - return ( - - - #{incident.inside_organization_number} {incident.render_for_web.title} - - {/* */} - - - - - ); - })} - - ); - }; - getUnattachClickHandler = (pk: Alert['pk']) => { const { store } = this.props; @@ -650,6 +509,158 @@ class IncidentPage extends React.Component store.alertGroupStore.doIncidentAction(alert.pk, AlertAction.unSilence, false); }; }; + + getIncidentDatetimeReference = (incident: Alert | GroupedAlert): string => { + let datetimeReference; + if ((incident as Alert).last_alert_at || incident.created_at) { + const m = moment((incident as Alert).last_alert_at || incident.created_at); + datetimeReference = `(${m.fromNow()}, ${m.toString()})`; + } + + return datetimeReference; + }; +} + +function Incident({ incident, datetimeReference }: { incident: Alert; datetimeReference: string }) { + return ( +
+ + + {incident.inside_organization_number + ? `#${incident.inside_organization_number} ${incident.render_for_web.title}` + : incident.render_for_web.title} + + {datetimeReference} + +
+ {incident.render_for_web.image_url && } +
+ ); +} + +function GroupedIncidentsList({ + id, + getIncidentDatetimeReference, +}: { + id: string; + getIncidentDatetimeReference: (incident: GroupedAlert) => string; +}) { + const store = useStore(); + 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); + + return ( + + {incident.alerts_count} Grouped Alerts + + (latest {latestAlertMoment.fromNow()}, {latestAlertMoment.toString()}) + + + } + contentClassName={cx('incidents-content')} + > + {alerts.map((alert) => ( + + ))} + + ); +} + +function GroupedIncident({ incident, datetimeReference }: { incident: GroupedAlert; datetimeReference: string }) { + const store = useStore(); + const [incidentRawResponse, setIncidentRawResponse] = useState<{ id: string; raw_request_data: any }>(undefined); + const [isModalOpen, setIsModalOpen] = useState(false); + + return ( + <> + {isModalOpen && ( + setIsModalOpen(false)} closeOnEscape isOpen={!!isModalOpen} title="Incident Payload"> + + {JSON.stringify(incidentRawResponse.raw_request_data, null, 4)} + + + )} + +
+ + + {incident.render_for_web.title} + + {datetimeReference} + + openIncidentResponse(incident)} /> + + +
+ {incident.render_for_web.image_url && } +
+ + ); + + async function openIncidentResponse(incident: GroupedAlert) { + const currentIncidentRawResponse = await store.alertGroupStore.getRawResponseForIncident(incident.id); + setIncidentRawResponse(currentIncidentRawResponse); + setIsModalOpen(true); + } +} + +function AttachedIncidents({ id, getUnattachClickHandler }: { id: string; getUnattachClickHandler(pk: string): void }) { + const store = useStore(); + const incident = store.alertGroupStore.alerts.get(id); + + if (!incident.dependent_alert_groups.length) { + return null; + } + + const alerts = incident.dependent_alert_groups; + + return ( + {incident.dependent_alert_groups.length} Attached Incidents} + contentClassName={cx('incidents-content')} + > + {alerts.map((incident) => { + return ( + + + #{incident.inside_organization_number} {incident.render_for_web.title} + + {/* */} + + + + + ); + })} + + ); } export default withMobXProviderContext(IncidentPage); diff --git a/grafana-plugin/src/pages/incidents/Incidents.tsx b/grafana-plugin/src/pages/incidents/Incidents.tsx index 18d718af..e2839858 100644 --- a/grafana-plugin/src/pages/incidents/Incidents.tsx +++ b/grafana-plugin/src/pages/incidents/Incidents.tsx @@ -103,7 +103,6 @@ class Incidents extends React.Component renderIncidentFilters() { const { query } = this.props; - const { filters } = this.state; return (
From 3172de1a4d2f5fd396988517e11244215a7fe568 Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Mon, 22 Aug 2022 17:11:15 +0300 Subject: [PATCH 14/77] renaming --- grafana-plugin/src/pages/incident/Incident.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/grafana-plugin/src/pages/incident/Incident.tsx b/grafana-plugin/src/pages/incident/Incident.tsx index d6cd1818..1c99410f 100644 --- a/grafana-plugin/src/pages/incident/Incident.tsx +++ b/grafana-plugin/src/pages/incident/Incident.tsx @@ -185,7 +185,7 @@ class IncidentPage extends React.Component
- +
{this.renderTimeline()}
@@ -591,7 +591,7 @@ function GroupedIncident({ incident, datetimeReference }: { incident: GroupedAle return ( <> {isModalOpen && ( - setIsModalOpen(false)} closeOnEscape isOpen={!!isModalOpen} title="Incident Payload"> + setIsModalOpen(false)} closeOnEscape isOpen={!!isModalOpen} title="Alert Payload"> {JSON.stringify(incidentRawResponse.raw_request_data, null, 4)} @@ -626,7 +626,7 @@ function GroupedIncident({ incident, datetimeReference }: { incident: GroupedAle } } -function AttachedIncidents({ id, getUnattachClickHandler }: { id: string; getUnattachClickHandler(pk: string): void }) { +function AttachedIncidentsList({ id, getUnattachClickHandler }: { id: string; getUnattachClickHandler(pk: string): void }) { const store = useStore(); const incident = store.alertGroupStore.alerts.get(id); From 9e7d651d01304e4fa85b225bf4c7d4d46b379130 Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Mon, 22 Aug 2022 18:36:57 +0300 Subject: [PATCH 15/77] linter --- .../src/models/alertgroup/alertgroup.ts | 6 ++--- .../src/pages/incident/Incident.tsx | 22 ++++++++++++------- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/grafana-plugin/src/models/alertgroup/alertgroup.ts b/grafana-plugin/src/models/alertgroup/alertgroup.ts index 66496780..a91ad33f 100644 --- a/grafana-plugin/src/models/alertgroup/alertgroup.ts +++ b/grafana-plugin/src/models/alertgroup/alertgroup.ts @@ -299,9 +299,9 @@ export class AlertGroupStore extends BaseStore { } @action - async getRawResponseForIncident(pk: Alert['pk']) { - const result = await makeRequest(`/alerts/${pk}`, {}) - return result + async getPayloadForIncident(pk: Alert['pk']) { + const result = await makeRequest(`/alerts/${pk}`, {}); + return result; } @action diff --git a/grafana-plugin/src/pages/incident/Incident.tsx b/grafana-plugin/src/pages/incident/Incident.tsx index 1c99410f..c9f0daec 100644 --- a/grafana-plugin/src/pages/incident/Incident.tsx +++ b/grafana-plugin/src/pages/incident/Incident.tsx @@ -14,7 +14,7 @@ import { VerticalGroup, Field, Modal, - Label, + Tooltip, } from '@grafana/ui'; import cn from 'classnames/bind'; import { observer } from 'mobx-react'; @@ -28,6 +28,7 @@ import Block from 'components/GBlock/Block'; import IntegrationLogo from 'components/IntegrationLogo/IntegrationLogo'; import WrongTeamStub from 'components/NotFoundInTeam/WrongTeamStub'; import PluginLink from 'components/PluginLink/PluginLink'; +import SourceCode from 'components/SourceCode/SourceCode'; import Text from 'components/Text/Text'; import AttachIncidentForm from 'containers/AttachIncidentForm/AttachIncidentForm'; import IntegrationSettings from 'containers/IntegrationSettings/IntegrationSettings'; @@ -43,6 +44,7 @@ import { } from 'models/alertgroup/alertgroup.types'; import { ResolutionNoteSourceTypesToDisplayName } from 'models/resolution_note/resolution_note.types'; import { WithStoreProps } from 'state/types'; +import { useStore } from 'state/useStore'; import { UserAction } from 'state/userAction'; import { withMobXProviderContext } from 'state/withStore'; import { openNotification } from 'utils'; @@ -51,8 +53,6 @@ import sanitize from 'utils/sanitize'; import { getActionButtons, getIncidentStatusTag, renderRelatedUsers } from './Incident.helpers'; import styles from './Incident.module.css'; -import SourceCode from 'components/SourceCode/SourceCode'; -import { useStore } from 'state/useStore'; const cx = cn.bind(styles); @@ -604,9 +604,9 @@ function GroupedIncident({ incident, datetimeReference }: { incident: GroupedAle {incident.render_for_web.title} {datetimeReference} - - openIncidentResponse(incident)} /> - + + openIncidentResponse(incident)} /> +
Date: Tue, 23 Aug 2022 13:49:19 +0300 Subject: [PATCH 16/77] cleanup --- grafana-plugin/src/pages/incident/Incident.module.css | 9 --------- grafana-plugin/src/pages/incident/Incident.tsx | 6 +++--- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/grafana-plugin/src/pages/incident/Incident.module.css b/grafana-plugin/src/pages/incident/Incident.module.css index 80e8bb71..3b45d8a5 100644 --- a/grafana-plugin/src/pages/incident/Incident.module.css +++ b/grafana-plugin/src/pages/incident/Incident.module.css @@ -103,13 +103,4 @@ .timeline-filter { margin-bottom: 24px; -} - -.view-response-button { - margin-left: auto; -} - -.incident-group-row > div { - display: flex; - flex-grow: 1; } \ No newline at end of file diff --git a/grafana-plugin/src/pages/incident/Incident.tsx b/grafana-plugin/src/pages/incident/Incident.tsx index c9f0daec..6425c0bd 100644 --- a/grafana-plugin/src/pages/incident/Incident.tsx +++ b/grafana-plugin/src/pages/incident/Incident.tsx @@ -523,7 +523,7 @@ class IncidentPage extends React.Component function Incident({ incident, datetimeReference }: { incident: Alert; datetimeReference: string }) { return ( -
+
{incident.inside_organization_number @@ -591,14 +591,14 @@ function GroupedIncident({ incident, datetimeReference }: { incident: GroupedAle return ( <> {isModalOpen && ( - setIsModalOpen(false)} closeOnEscape isOpen={!!isModalOpen} title="Alert Payload"> + setIsModalOpen(false)} closeOnEscape isOpen={isModalOpen} title="Alert Payload"> {JSON.stringify(incidentRawResponse.raw_request_data, null, 4)} )} -
+
{incident.render_for_web.title} From b063f79d78bd07f7869d789d25040ee93f7b298f Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Tue, 23 Aug 2022 14:04:17 +0300 Subject: [PATCH 17/77] linter --- grafana-plugin/src/pages/incident/Incident.module.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/grafana-plugin/src/pages/incident/Incident.module.css b/grafana-plugin/src/pages/incident/Incident.module.css index 3b45d8a5..f1ff8fda 100644 --- a/grafana-plugin/src/pages/incident/Incident.module.css +++ b/grafana-plugin/src/pages/incident/Incident.module.css @@ -103,4 +103,4 @@ .timeline-filter { margin-bottom: 24px; -} \ No newline at end of file +} From 27fbae7abd7eb9b524b8056261b8c0087d26b01f Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Tue, 23 Aug 2022 14:04:55 +0300 Subject: [PATCH 18/77] linter fix --- grafana-plugin/src/components/NavBar/NavBarSubtitle.module.css | 2 +- grafana-plugin/src/img/grafanaGlobalStyles.css | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/grafana-plugin/src/components/NavBar/NavBarSubtitle.module.css b/grafana-plugin/src/components/NavBar/NavBarSubtitle.module.css index 61c1346a..99f5bbb0 100644 --- a/grafana-plugin/src/components/NavBar/NavBarSubtitle.module.css +++ b/grafana-plugin/src/components/NavBar/NavBarSubtitle.module.css @@ -18,4 +18,4 @@ .navbar-link { display: flex; align-items: center; -} \ No newline at end of file +} diff --git a/grafana-plugin/src/img/grafanaGlobalStyles.css b/grafana-plugin/src/img/grafanaGlobalStyles.css index 8eab37df..2379bb84 100644 --- a/grafana-plugin/src/img/grafanaGlobalStyles.css +++ b/grafana-plugin/src/img/grafanaGlobalStyles.css @@ -36,4 +36,4 @@ .page-header__info-block { flex-grow: 1; /* Stretch the navigation subtitle panel */ -} \ No newline at end of file +} From 8158fa42a7f573d751e77487c990b66c34185d6b Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Tue, 23 Aug 2022 14:18:09 +0300 Subject: [PATCH 19/77] renamed container class to root --- grafana-plugin/src/components/NavBar/NavBarSubtitle.module.css | 2 +- grafana-plugin/src/components/NavBar/NavBarSubtitle.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/grafana-plugin/src/components/NavBar/NavBarSubtitle.module.css b/grafana-plugin/src/components/NavBar/NavBarSubtitle.module.css index 99f5bbb0..6156aa30 100644 --- a/grafana-plugin/src/components/NavBar/NavBarSubtitle.module.css +++ b/grafana-plugin/src/components/NavBar/NavBarSubtitle.module.css @@ -1,4 +1,4 @@ -.navbar-container { +.root { display: flex; align-items: center; } diff --git a/grafana-plugin/src/components/NavBar/NavBarSubtitle.tsx b/grafana-plugin/src/components/NavBar/NavBarSubtitle.tsx index e9fe292e..a0b50b2c 100644 --- a/grafana-plugin/src/components/NavBar/NavBarSubtitle.tsx +++ b/grafana-plugin/src/components/NavBar/NavBarSubtitle.tsx @@ -13,7 +13,7 @@ const cx = cn.bind(styles); function NavBarSubtitle({ backendLicense }: { backendLicense: string }) { if (backendLicense === GRAFANA_LICENSE_OSS) { return ( -
+
{APP_SUBTITLE} From 8428848844ea530efe6a44c02a6f669bde99cbe8 Mon Sep 17 00:00:00 2001 From: Vadim Stepanov Date: Thu, 25 Aug 2022 14:02:32 +0100 Subject: [PATCH 20/77] Telegram docs/instructions update (#406) * update Telegram Setup section for open-source.md * update channel connection instruction * update Telegram configuration docs --- .../chat-options/configure-telegram.md | 26 ++++++++++--------- docs/sources/open-source.md | 12 ++++----- .../TelegramIntegrationButton.tsx | 8 ++++-- 3 files changed, 25 insertions(+), 21 deletions(-) diff --git a/docs/sources/chat-options/configure-telegram.md b/docs/sources/chat-options/configure-telegram.md index e119aff4..daef6ab6 100644 --- a/docs/sources/chat-options/configure-telegram.md +++ b/docs/sources/chat-options/configure-telegram.md @@ -16,15 +16,22 @@ weight: 300 # Telegram integration for Grafana OnCall -You can use Telegram to deliver alert group notifications to a dedicated channel, and allow users to perform notification actions. +You can manage alerts either directly in your personal Telegram DMs or in a dedicated team channel. -Each alert group notification is assigned a dedicated discussion. Users can perform notification actions (acknowledge, resolve, silence), create reports, and discuss alerts in the comments section of the discussions. +## Configure Telegram user settings in Grafana OnCall -In case an integration route is not configured to use a Telegram channel, users will receive messages with alert group contents, logs and actions in their DMs. +To receive alert group contents, escalation logs and to be able to perform actions (acknowledge, resolve, silence) in Telegram DMs, please refer to the following steps: -## Connect to Telegram +1. In your profile, find the Telegram setting and click **Connect**. +1. Click **Connect automatically** for the bot to message you and to bring up your telegram account. +1. Click **Start** when the OnCall bot messages you and wait for the connection confirmation. +1. Done! Now you can receive alerts directly to your Telegram DMs. -Connect your organization's Telegram account to your Grafana OnCall instance by following the instructions provided in OnCall. You can use the following steps as a reference. +If you want to connect manually, you can click the URL provided and then **SEND MESSAGE**. In your Telegram account, click **Start**. + +## (Optional) Connect to a Telegram channel + +In case you want to manage alerts in a dedicated Telegram channel, please use the following steps as a reference. > **NOTE:** Only Grafana users with the administrator role can configure OnCall settings. @@ -42,10 +49,5 @@ Connect your organization's Telegram account to your Grafana OnCall instance by 1. In OnCall, send the provided verification code to the channel. 1. Make sure users connect to Telegram in their OnCall user profile. -## Configure Telegram user settings in OnCall - -1. In your profile, find the Telegram setting and click **Connect**. -1. Click **Connect automatically** for the bot to message you and to bring up your telegram account. -1. Click **Start** when the OnCall bot messages you. - -If you want to connect manually, you can click the URL provided and then **SEND MESSAGE**. In your Telegram account, click **Start**. +Each alert group is assigned a dedicated discussion. Users can perform actions (acknowledge, resolve, silence), and discuss alerts in the comments section of the discussions. +In case an integration route is not configured to use a Telegram channel, users will receive messages with alert group contents, logs and actions in their DMs. diff --git a/docs/sources/open-source.md b/docs/sources/open-source.md index fb02b50f..b2e6c1df 100644 --- a/docs/sources/open-source.md +++ b/docs/sources/open-source.md @@ -166,13 +166,11 @@ lt --port 8080 -s pretty-turkey-83 --print-requests The Telegram integration for Grafana OnCall is designed for collaborative team work and improved incident response. Refer to the following steps to configure the Telegram integration: -1. Ensure your OnCall environment is up and running. - -1. Request [BotFather](https://t.me/BotFather) for a key, then add your key in `TELEGRAM_TOKEN` in your Grafana OnCall **Env Variables**. - -1. Set `TELEGRAM_WEBHOOK_HOST` with your external URL for your Grafana OnCall. - -1. From the **ChatOps** tab in Grafana OnCall, click **Telegram**. +1. Ensure your Grafana OnCall environment is up and running. +2. Create a Telegram bot using [BotFather](https://t.me/BotFather) and save the token provided by BotFather. Please make sure to disable **Group Privacy** for the bot (Bot Settings -> Group Privacy -> Turn off). +3. Paste the token provided by BotFather to the `TELEGRAM_TOKEN` variable on the **Env Variables** page of your Grafana OnCall instance. +4. Set the `TELEGRAM_WEBHOOK_HOST` variable to the external address of your Grafana OnCall instance. Please note that `TELEGRAM_WEBHOOK_HOST` must start with `https://` and be publicly available (meaning that it can be reached by Telegram servers). If your host is private or local, consider using a reverse proxy (e.g. [ngrok](https://ngrok.com)). +5. Now you can connect Telegram accounts on the **Users** page and receive alert groups to Telegram direct messages. Alternatively, in case you want to connect Telegram channels to your Grafana OnCall environment, navigate to the **ChatOps** tab. ## Grafana OSS-Cloud Setup diff --git a/grafana-plugin/src/containers/TelegramIntegrationButton/TelegramIntegrationButton.tsx b/grafana-plugin/src/containers/TelegramIntegrationButton/TelegramIntegrationButton.tsx index f4b66692..256a2a16 100644 --- a/grafana-plugin/src/containers/TelegramIntegrationButton/TelegramIntegrationButton.tsx +++ b/grafana-plugin/src/containers/TelegramIntegrationButton/TelegramIntegrationButton.tsx @@ -147,12 +147,16 @@ const TelegramModal = (props: TelegramModalProps) => { {' '} - , to the channel. + , to the channel and wait for the confirmation message.
- 8. Make sure users connect to Telegram in their OnCall user profile. + 8. Make sure users connect their Telegram accounts in their OnCall user profile. +
+ +
+ 9. Done! Now you can manage alerts in your Telegram workspace.
From 356aa336ad91e3375fc1e7dbad72d8fee43a1c3f Mon Sep 17 00:00:00 2001 From: Innokentii Konstantinov Date: Fri, 26 Aug 2022 13:46:50 +0500 Subject: [PATCH 21/77] Remove auto-recreating logic for UserNotificationPolicy (#414) * Remove auto-recreating logic for UserNotificationPolicy It's removed to get rid of select_for_update on User on each notify_user_task * Fix and add tests * remove get_user_policies method --- .../incident_log_builder.py | 4 +-- engine/apps/alerts/tasks/notify_group.py | 2 +- engine/apps/alerts/tasks/notify_user.py | 4 +-- .../api/views/user_notification_policy.py | 10 ++++--- .../base/models/user_notification_policy.py | 27 ++++++------------- .../tests/test_user_notification_policy.py | 23 ++++++++++++++++ .../views/personal_notifications.py | 6 ++++- engine/apps/user_management/models/user.py | 3 +++ engine/common/exceptions/__init__.py | 7 ++++- engine/common/exceptions/exceptions.py | 4 +++ engine/conftest.py | 3 +++ 11 files changed, 62 insertions(+), 31 deletions(-) diff --git a/engine/apps/alerts/incident_log_builder/incident_log_builder.py b/engine/apps/alerts/incident_log_builder/incident_log_builder.py index c1582551..ca5ae047 100644 --- a/engine/apps/alerts/incident_log_builder/incident_log_builder.py +++ b/engine/apps/alerts/incident_log_builder/incident_log_builder.py @@ -659,9 +659,7 @@ class IncidentLogBuilder: # last passed step order + 1 notification_policy_order = last_user_log.notification_policy.order + 1 - notification_policies = UserNotificationPolicy.objects.get_or_create_for_user( - user=user_to_notify, important=important - ) + notification_policies = UserNotificationPolicy.objects.filter(user=user_to_notify, important=important) for notification_policy in notification_policies: future_notification = notification_policy.order >= notification_policy_order diff --git a/engine/apps/alerts/tasks/notify_group.py b/engine/apps/alerts/tasks/notify_group.py index d18c31b1..9803affb 100644 --- a/engine/apps/alerts/tasks/notify_group.py +++ b/engine/apps/alerts/tasks/notify_group.py @@ -58,7 +58,7 @@ def notify_group_task(alert_group_pk, escalation_policy_snapshot_order=None): if not user.is_notification_allowed: continue - notification_policies = UserNotificationPolicy.objects.get_or_create_for_user( + notification_policies = UserNotificationPolicy.objects.filter( user=user, important=escalation_policy_step == EscalationPolicy.STEP_NOTIFY_GROUP_IMPORTANT, ) diff --git a/engine/apps/alerts/tasks/notify_user.py b/engine/apps/alerts/tasks/notify_user.py index 57d902b2..a9ba1d15 100644 --- a/engine/apps/alerts/tasks/notify_user.py +++ b/engine/apps/alerts/tasks/notify_user.py @@ -73,9 +73,7 @@ def notify_user_task( user_has_notification = UserHasNotification.objects.filter(pk=user_has_notification.pk).select_for_update()[0] if previous_notification_policy_pk is None: - notification_policy = UserNotificationPolicy.objects.get_or_create_for_user( - user=user, important=important - ).first() + notification_policy = UserNotificationPolicy.objects.filter(user=user, important=important).first() # Here we collect a brief overview of notification steps configured for user to send it to thread. collected_steps_ids = [] next_notification_policy = notification_policy.next() diff --git a/engine/apps/api/views/user_notification_policy.py b/engine/apps/api/views/user_notification_policy.py index ae7e4bee..7231bcc5 100644 --- a/engine/apps/api/views/user_notification_policy.py +++ b/engine/apps/api/views/user_notification_policy.py @@ -26,6 +26,7 @@ from apps.base.models.user_notification_policy import BUILT_IN_BACKENDS, Notific from apps.user_management.models import User from common.api_helpers.exceptions import BadRequest from common.api_helpers.mixins import UpdateSerializerMixin +from common.exceptions import UserNotificationPolicyCouldNotBeDeleted from common.insight_log import EntityEvent, write_resource_insight_log @@ -55,14 +56,14 @@ class UserNotificationPolicyView(UpdateSerializerMixin, ModelViewSet): except ValueError: raise BadRequest(detail="Invalid user param") if user_id is None or user_id == self.request.user.public_primary_key: - queryset = self.model.objects.get_or_create_for_user(user=self.request.user, important=important) + queryset = self.model.objects.filter(user=self.request.user, important=important) else: try: target_user = User.objects.get(public_primary_key=user_id) except User.DoesNotExist: raise BadRequest(detail="User does not exist") - queryset = self.model.objects.get_or_create_for_user(user=target_user, important=important) + queryset = self.model.objects.filter(user=target_user, important=important) queryset = self.serializer_class.setup_eager_loading(queryset) @@ -111,7 +112,10 @@ class UserNotificationPolicyView(UpdateSerializerMixin, ModelViewSet): def perform_destroy(self, instance): user = instance.user prev_state = user.insight_logs_serialized - instance.delete() + try: + instance.delete() + except UserNotificationPolicyCouldNotBeDeleted: + raise BadRequest(detail="Can't delete last user notification policy") new_state = user.insight_logs_serialized write_resource_insight_log( instance=user, diff --git a/engine/apps/base/models/user_notification_policy.py b/engine/apps/base/models/user_notification_policy.py index e0a62275..b6444995 100644 --- a/engine/apps/base/models/user_notification_policy.py +++ b/engine/apps/base/models/user_notification_policy.py @@ -4,13 +4,14 @@ from typing import Tuple from django.conf import settings from django.core.exceptions import ValidationError from django.core.validators import MinLengthValidator -from django.db import models, transaction +from django.db import models from django.db.models import Q, QuerySet from django.utils import timezone from ordered_model.models import OrderedModel from apps.base.messaging import get_messaging_backends from apps.user_management.models import User +from common.exceptions import UserNotificationPolicyCouldNotBeDeleted from common.public_primary_keys import generate_public_primary_key, increase_public_primary_key_length @@ -69,24 +70,6 @@ def validate_channel_choice(value): class UserNotificationPolicyQuerySet(models.QuerySet): - def get_or_create_for_user(self, user: User, important: bool) -> "QuerySet[UserNotificationPolicy]": - with transaction.atomic(): - User.objects.select_for_update().get(pk=user.pk) - return self._get_or_create_for_user(user, important) - - def _get_or_create_for_user(self, user: User, important: bool) -> "QuerySet[UserNotificationPolicy]": - notification_policies = super().filter(user=user, important=important) - - if notification_policies.exists(): - return notification_policies - - if important: - policies = self.create_important_policies_for_user(user) - else: - policies = self.create_default_policies_for_user(user) - - return policies - def create_default_policies_for_user(self, user: User) -> "QuerySet[UserNotificationPolicy]": model = self.model @@ -197,6 +180,12 @@ class UserNotificationPolicy(OrderedModel): else: return "Not set" + def delete(self): + if UserNotificationPolicy.objects.filter(important=self.important, user=self.user).count() == 1: + raise UserNotificationPolicyCouldNotBeDeleted("Can't delete last user notification policy") + else: + super().delete() + class NotificationChannelOptions: """ diff --git a/engine/apps/base/tests/test_user_notification_policy.py b/engine/apps/base/tests/test_user_notification_policy.py index 5d0e1df7..41354491 100644 --- a/engine/apps/base/tests/test_user_notification_policy.py +++ b/engine/apps/base/tests/test_user_notification_policy.py @@ -9,6 +9,7 @@ from apps.base.models.user_notification_policy import ( validate_channel_choice, ) from apps.base.tests.messaging_backend import TestOnlyBackend +from common.exceptions import UserNotificationPolicyCouldNotBeDeleted @pytest.mark.parametrize( @@ -80,3 +81,25 @@ def test_extra_messaging_backends_details(): ) assert validate_channel_choice(channel_choice) is None + + +@pytest.mark.django_db +def test_unable_to_delete_last_notification_policy( + make_organization, + make_user_for_organization, + make_user_notification_policy, +): + organization = make_organization() + user = make_user_for_organization(organization) + + first_policy = make_user_notification_policy( + user, UserNotificationPolicy.Step.NOTIFY, notify_by=UserNotificationPolicy.NotificationChannel.SLACK + ) + + second_policy = make_user_notification_policy( + user, UserNotificationPolicy.Step.WAIT, wait_delay=timedelta(minutes=5) + ) + + first_policy.delete() + with pytest.raises(UserNotificationPolicyCouldNotBeDeleted): + second_policy.delete() diff --git a/engine/apps/public_api/views/personal_notifications.py b/engine/apps/public_api/views/personal_notifications.py index 44b251a3..b1288b4b 100644 --- a/engine/apps/public_api/views/personal_notifications.py +++ b/engine/apps/public_api/views/personal_notifications.py @@ -12,6 +12,7 @@ from apps.user_management.models import User from common.api_helpers.exceptions import BadRequest from common.api_helpers.mixins import RateLimitHeadersMixin, UpdateSerializerMixin from common.api_helpers.paginators import FiftyPageSizePaginator +from common.exceptions import UserNotificationPolicyCouldNotBeDeleted from common.insight_log import EntityEvent, write_resource_insight_log @@ -74,7 +75,10 @@ class PersonalNotificationView(RateLimitHeadersMixin, UpdateSerializerMixin, Mod def perform_destroy(self, instance): user = self.request.user prev_state = user.insight_logs_serialized - instance.delete() + try: + instance.delete() + except UserNotificationPolicyCouldNotBeDeleted: + raise BadRequest(detail="Can't delete last user notification policy") new_state = user.insight_logs_serialized write_resource_insight_log( instance=user, diff --git a/engine/apps/user_management/models/user.py b/engine/apps/user_management/models/user.py index c1a00669..041a0ec5 100644 --- a/engine/apps/user_management/models/user.py +++ b/engine/apps/user_management/models/user.py @@ -260,6 +260,9 @@ class User(models.Model): # TODO: check whether this signal can be moved to save method of the model @receiver(post_save, sender=User) def listen_for_user_model_save(sender, instance, created, *args, **kwargs): + if created: + instance.notification_policies.create_default_policies_for_user(instance) + instance.notification_policies.create_important_policies_for_user(instance) drop_cached_ical_for_custom_events_for_organization.apply_async( (instance.organization_id,), ) diff --git a/engine/common/exceptions/__init__.py b/engine/common/exceptions/__init__.py index d191b8f4..ec922fb4 100644 --- a/engine/common/exceptions/__init__.py +++ b/engine/common/exceptions/__init__.py @@ -1 +1,6 @@ -from .exceptions import MaintenanceCouldNotBeStartedError, TeamCanNotBeChangedError, UnableToSendDemoAlert # noqa: F401 +from .exceptions import ( # noqa: F401 + MaintenanceCouldNotBeStartedError, + TeamCanNotBeChangedError, + UnableToSendDemoAlert, + UserNotificationPolicyCouldNotBeDeleted, +) diff --git a/engine/common/exceptions/exceptions.py b/engine/common/exceptions/exceptions.py index 69318bd5..9adf0b47 100644 --- a/engine/common/exceptions/exceptions.py +++ b/engine/common/exceptions/exceptions.py @@ -17,3 +17,7 @@ class TeamCanNotBeChangedError(OperationCouldNotBePerformedError): class UnableToSendDemoAlert(OperationCouldNotBePerformedError): pass + + +class UserNotificationPolicyCouldNotBeDeleted(OperationCouldNotBePerformedError): + pass diff --git a/engine/conftest.py b/engine/conftest.py index 85fb9a3d..68ef50d5 100644 --- a/engine/conftest.py +++ b/engine/conftest.py @@ -68,6 +68,7 @@ from apps.telegram.tests.factories import ( TelegramVerificationCodeFactory, ) from apps.twilioapp.tests.factories import PhoneCallFactory, SMSFactory +from apps.user_management.models.user import User, listen_for_user_model_save from apps.user_management.tests.factories import OrganizationFactory, TeamFactory, UserFactory from common.constants.role import Role @@ -150,7 +151,9 @@ def make_organization(): @pytest.fixture def make_user_for_organization(): def _make_user_for_organization(organization, role=Role.ADMIN, **kwargs): + post_save.disconnect(listen_for_user_model_save, sender=User) user = UserFactory(organization=organization, role=role, **kwargs) + post_save.disconnect(listen_for_user_model_save, sender=User) return user return _make_user_for_organization From 76901a564b6a8d18302d3a1c85cf0a31dd35a561 Mon Sep 17 00:00:00 2001 From: Innokentii Konstantinov Date: Fri, 26 Aug 2022 14:26:01 +0500 Subject: [PATCH 22/77] Handle case when user somehow deleted all their notification policies --- engine/apps/alerts/tasks/notify_user.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/engine/apps/alerts/tasks/notify_user.py b/engine/apps/alerts/tasks/notify_user.py index a9ba1d15..425eea16 100644 --- a/engine/apps/alerts/tasks/notify_user.py +++ b/engine/apps/alerts/tasks/notify_user.py @@ -74,6 +74,11 @@ def notify_user_task( if previous_notification_policy_pk is None: notification_policy = UserNotificationPolicy.objects.filter(user=user, important=important).first() + if notification_policy is None: + task_logger.info( + f"notify_user_task: Failed to notify. No notification policies. user_id={user_pk} alert_group_id={alert_group_pk} important={important}" + ) + return # Here we collect a brief overview of notification steps configured for user to send it to thread. collected_steps_ids = [] next_notification_policy = notification_policy.next() From 25c3d19a050c7e73bf40b0cd97eb6e9509cfd398 Mon Sep 17 00:00:00 2001 From: Innokentii Konstantinov Date: Fri, 26 Aug 2022 16:06:21 +0500 Subject: [PATCH 23/77] Update CHANGELOG.md --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 46c127f9..186fc7ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Change Log +## v1.0.26 (2022-08-26) +- Insight log's format fixes +- Remove UserNotificationPolicy auto-recreating + ## v1.0.25 (2022-08-24) - Bug fixes From 912b73c3eb6425ecae793d4bbefc61198decfad3 Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Fri, 26 Aug 2022 16:43:45 +0300 Subject: [PATCH 24/77] ux changes --- .../src/models/alertgroup/alertgroup.ts | 4 +- .../src/pages/incident/Incident.module.css | 10 ++++ .../src/pages/incident/Incident.tsx | 47 ++++++++++++++----- 3 files changed, 46 insertions(+), 15 deletions(-) diff --git a/grafana-plugin/src/models/alertgroup/alertgroup.ts b/grafana-plugin/src/models/alertgroup/alertgroup.ts index a91ad33f..8c035c4b 100644 --- a/grafana-plugin/src/models/alertgroup/alertgroup.ts +++ b/grafana-plugin/src/models/alertgroup/alertgroup.ts @@ -298,10 +298,8 @@ export class AlertGroupStore extends BaseStore { }); } - @action async getPayloadForIncident(pk: Alert['pk']) { - const result = await makeRequest(`/alerts/${pk}`, {}); - return result; + return await makeRequest(`/alerts/${pk}`, {}); } @action diff --git a/grafana-plugin/src/pages/incident/Incident.module.css b/grafana-plugin/src/pages/incident/Incident.module.css index f1ff8fda..8dfd44c9 100644 --- a/grafana-plugin/src/pages/incident/Incident.module.css +++ b/grafana-plugin/src/pages/incident/Incident.module.css @@ -2,6 +2,16 @@ margin-top: 24px; } +.incident-row { + display: flex; +} +.incident-row-left { + flex-grow: 1; +} +.payload-subtitle { + margin-bottom: 16px; +} + .info-row { width: 100%; border-bottom: 1px solid rgba(204, 204, 220, 0.15); diff --git a/grafana-plugin/src/pages/incident/Incident.tsx b/grafana-plugin/src/pages/incident/Incident.tsx index 6425c0bd..c3f6bb24 100644 --- a/grafana-plugin/src/pages/incident/Incident.tsx +++ b/grafana-plugin/src/pages/incident/Incident.tsx @@ -587,27 +587,50 @@ function GroupedIncident({ incident, datetimeReference }: { incident: GroupedAle const store = useStore(); const [incidentRawResponse, setIncidentRawResponse] = useState<{ id: string; raw_request_data: any }>(undefined); const [isModalOpen, setIsModalOpen] = useState(false); + const payloadJSON = isModalOpen ? JSON.stringify(incidentRawResponse.raw_request_data, null, 4) : undefined; return ( <> {isModalOpen && ( - setIsModalOpen(false)} closeOnEscape isOpen={isModalOpen} title="Alert Payload"> + setIsModalOpen(false)} + closeOnEscape + isOpen={isModalOpen} + title="Alert Payload" + > +
+ + + {incident.render_for_web.title} - {datetimeReference} + + +
- {JSON.stringify(incidentRawResponse.raw_request_data, null, 4)} + {payloadJSON} + +
)} -
- - - {incident.render_for_web.title} - - {datetimeReference} - - openIncidentResponse(incident)} /> - - +
+
+
+ + + {incident.render_for_web.title} + + {datetimeReference} + +
+
+ + + openIncidentResponse(incident)} /> + + +
+
Date: Fri, 26 Aug 2022 17:19:39 +0300 Subject: [PATCH 25/77] styling --- grafana-plugin/src/components/SourceCode/SourceCode.module.css | 1 + 1 file changed, 1 insertion(+) diff --git a/grafana-plugin/src/components/SourceCode/SourceCode.module.css b/grafana-plugin/src/components/SourceCode/SourceCode.module.css index 7cac30a8..beabde1e 100644 --- a/grafana-plugin/src/components/SourceCode/SourceCode.module.css +++ b/grafana-plugin/src/components/SourceCode/SourceCode.module.css @@ -1,5 +1,6 @@ .root { position: relative; + width: 100%; } .scroller { From 4b850bb4d432bbb3e05a036e35e3f2b1d1d3c8c0 Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Fri, 26 Aug 2022 17:40:27 +0300 Subject: [PATCH 26/77] ux --- .../src/components/SourceCode/SourceCode.tsx | 25 +++++++++++-------- .../src/pages/incident/Incident.module.css | 2 ++ .../src/pages/incident/Incident.tsx | 19 ++++++++------ 3 files changed, 28 insertions(+), 18 deletions(-) diff --git a/grafana-plugin/src/components/SourceCode/SourceCode.tsx b/grafana-plugin/src/components/SourceCode/SourceCode.tsx index 3484b82e..1689212c 100644 --- a/grafana-plugin/src/components/SourceCode/SourceCode.tsx +++ b/grafana-plugin/src/components/SourceCode/SourceCode.tsx @@ -12,23 +12,26 @@ const cx = cn.bind(styles); interface SourceCodeProps { noMaxHeight?: boolean; + showCopyToClipboard?: boolean; } const SourceCode: FC = (props) => { - const { children, noMaxHeight = false } = props; + const { children, noMaxHeight = false, showCopyToClipboard = true } = props; return (
- { - openNotification('Copied!'); - }} - > - - + {showCopyToClipboard && ( + { + openNotification('Copied!'); + }} + > + + + )}
       {isModalOpen && (
-         setIsModalOpen(false)}
-          closeOnEscape
-          isOpen={isModalOpen}
-          title="Alert Payload"
-        >
+         setIsModalOpen(false)} closeOnEscape isOpen={isModalOpen} title="Alert Payload">
           
@@ -606,8 +601,18 @@ function GroupedIncident({ incident, datetimeReference }: { incident: GroupedAle
- {payloadJSON} + {payloadJSON} + { + openNotification('Copied!'); + }} + > + +
From fb67003f1eeda528127528297eb237d42798e3a3 Mon Sep 17 00:00:00 2001 From: Innokentii Konstantinov Date: Mon, 29 Aug 2022 17:34:27 +0500 Subject: [PATCH 27/77] Fix GRAFANA_CLOUD_ONCALL_API_URL --- engine/settings/base.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/engine/settings/base.py b/engine/settings/base.py index 53c35c66..fae94a47 100644 --- a/engine/settings/base.py +++ b/engine/settings/base.py @@ -37,7 +37,7 @@ MIRAGE_CIPHER_IV = os.environ.get("MIRAGE_CIPHER_IV") MIRAGE_CIPHER_MODE = "CBC" # SECURITY WARNING: don't run with debug turned on in production! -DEBUG = False +DEBUG = True ALLOWED_HOSTS = [item.strip() for item in os.environ.get("ALLOWED_HOSTS", "*").split(",")] @@ -75,7 +75,9 @@ SENDGRID_SECRET_KEY = os.environ.get("SENDGRID_SECRET_KEY") SENDGRID_INBOUND_EMAIL_DOMAIN = os.environ.get("SENDGRID_INBOUND_EMAIL_DOMAIN") # For Grafana Cloud integration -GRAFANA_CLOUD_ONCALL_API_URL = os.environ.get("GRAFANA_CLOUD_ONCALL_API_URL", "https://a-prod-us-central-0.grafana.net") +GRAFANA_CLOUD_ONCALL_API_URL = os.environ.get( + "GRAFANA_CLOUD_ONCALL_API_URL", "https://oncall-prod-us-central-0.grafana.net/oncall" +) GRAFANA_CLOUD_ONCALL_TOKEN = os.environ.get("GRAFANA_CLOUD_ONCALL_TOKEN", None) # Outgoing webhook settings From b703130dcbcbaca7e95f3fa50c6e94bda3087ba0 Mon Sep 17 00:00:00 2001 From: Innokentii Konstantinov Date: Mon, 29 Aug 2022 17:36:27 +0500 Subject: [PATCH 28/77] Revert "Fix GRAFANA_CLOUD_ONCALL_API_URL" This reverts commit fb67003f1eeda528127528297eb237d42798e3a3. --- engine/settings/base.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/engine/settings/base.py b/engine/settings/base.py index fae94a47..53c35c66 100644 --- a/engine/settings/base.py +++ b/engine/settings/base.py @@ -37,7 +37,7 @@ MIRAGE_CIPHER_IV = os.environ.get("MIRAGE_CIPHER_IV") MIRAGE_CIPHER_MODE = "CBC" # SECURITY WARNING: don't run with debug turned on in production! -DEBUG = True +DEBUG = False ALLOWED_HOSTS = [item.strip() for item in os.environ.get("ALLOWED_HOSTS", "*").split(",")] @@ -75,9 +75,7 @@ SENDGRID_SECRET_KEY = os.environ.get("SENDGRID_SECRET_KEY") SENDGRID_INBOUND_EMAIL_DOMAIN = os.environ.get("SENDGRID_INBOUND_EMAIL_DOMAIN") # For Grafana Cloud integration -GRAFANA_CLOUD_ONCALL_API_URL = os.environ.get( - "GRAFANA_CLOUD_ONCALL_API_URL", "https://oncall-prod-us-central-0.grafana.net/oncall" -) +GRAFANA_CLOUD_ONCALL_API_URL = os.environ.get("GRAFANA_CLOUD_ONCALL_API_URL", "https://a-prod-us-central-0.grafana.net") GRAFANA_CLOUD_ONCALL_TOKEN = os.environ.get("GRAFANA_CLOUD_ONCALL_TOKEN", None) # Outgoing webhook settings From 363e90c861c590aa6a54d2a00e411d7d41aa2d2d Mon Sep 17 00:00:00 2001 From: Matias Bordese Date: Mon, 29 Aug 2022 14:33:19 -0300 Subject: [PATCH 29/77] Fix custom on call shift test --- engine/apps/schedules/tests/test_custom_on_call_shift.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/engine/apps/schedules/tests/test_custom_on_call_shift.py b/engine/apps/schedules/tests/test_custom_on_call_shift.py index cf3d5e91..5adeea9b 100644 --- a/engine/apps/schedules/tests/test_custom_on_call_shift.py +++ b/engine/apps/schedules/tests/test_custom_on_call_shift.py @@ -585,7 +585,8 @@ def test_rolling_users_with_diff_start_and_rotation_start_weekly_by_day( schedule = make_schedule(organization, schedule_class=OnCallScheduleWeb) now = timezone.now().replace(hour=0, minute=0, second=0, microsecond=0) today_weekday = now.weekday() - next_week_monday = now + timezone.timedelta(days=(0 - today_weekday) % 7) + delta_days = (0 - today_weekday) % 7 + (7 if today_weekday == 0 else 0) + next_week_monday = now + timezone.timedelta(days=delta_days) # SAT, SUN weekdays = [5, 6] by_day = [CustomOnCallShift.ICAL_WEEKDAY_MAP[day] for day in weekdays] From a8124b6afef7429f33459fcfcda82de4bcb989e1 Mon Sep 17 00:00:00 2001 From: Matias Bordese Date: Mon, 29 Aug 2022 14:07:15 -0300 Subject: [PATCH 30/77] Add unlink slack account option to profile --- engine/apps/api/tests/test_user.py | 71 +++++++++++++++++++ engine/apps/api/views/user.py | 9 +++ .../parts/connectors/SlackConnector.tsx | 7 ++ grafana-plugin/src/models/user/user.ts | 14 ++++ 4 files changed, 101 insertions(+) diff --git a/engine/apps/api/tests/test_user.py b/engine/apps/api/tests/test_user.py index 7c064616..78da25d9 100644 --- a/engine/apps/api/tests/test_user.py +++ b/engine/apps/api/tests/test_user.py @@ -800,6 +800,30 @@ def test_admin_can_unlink_another_user_backend_account( assert response.status_code == status.HTTP_200_OK +@pytest.mark.django_db +def test_admin_can_unlink_another_user_slack_account( + make_organization_with_slack_team_identity, + make_user_for_organization, + make_user_with_slack_user_identity, + make_token_for_organization, + make_user_auth_headers, +): + organization, slack_team_identity = make_organization_with_slack_team_identity() + admin = make_user_for_organization(organization, role=Role.ADMIN) + editor, slack_user_identity_1 = make_user_with_slack_user_identity( + slack_team_identity, organization, slack_id="user_1", role=Role.EDITOR + ) + + _, token = make_token_for_organization(organization) + client = APIClient() + url = reverse("api-internal:user-unlink-slack", kwargs={"pk": editor.public_primary_key}) + + response = client.post(url, format="json", **make_user_auth_headers(admin, token)) + assert response.status_code == status.HTTP_200_OK + editor.refresh_from_db() + assert editor.slack_user_identity is None + + """Test user permissions""" @@ -1038,6 +1062,28 @@ def test_user_cant_get_another_user_backend_verification_code( assert response.status_code == status.HTTP_403_FORBIDDEN +@pytest.mark.django_db +def test_user_can_unlink_own_slack_account( + make_organization_with_slack_team_identity, + make_user_with_slack_user_identity, + make_token_for_organization, + make_user_auth_headers, +): + organization, slack_team_identity = make_organization_with_slack_team_identity() + user, slack_user_identity_1 = make_user_with_slack_user_identity( + slack_team_identity, organization, slack_id="user_1", role=Role.EDITOR + ) + + _, token = make_token_for_organization(organization) + client = APIClient() + url = reverse("api-internal:user-unlink-slack", kwargs={"pk": user.public_primary_key}) + + response = client.post(url, format="json", **make_user_auth_headers(user, token)) + assert response.status_code == status.HTTP_200_OK + user.refresh_from_db() + assert user.slack_user_identity is None + + @pytest.mark.django_db def test_user_can_unlink_backend_own_account( make_organization, make_user_for_organization, make_token_for_organization, make_user_auth_headers @@ -1086,6 +1132,31 @@ def test_user_unlink_backend_backend_account_not_found( assert response.status_code == status.HTTP_400_BAD_REQUEST +@pytest.mark.django_db +def test_user_cant_unlink_slack_another_user( + make_organization_with_slack_team_identity, + make_user_with_slack_user_identity, + make_token_for_organization, + make_user_auth_headers, +): + organization, slack_team_identity = make_organization_with_slack_team_identity() + first_user, slack_user_identity_1 = make_user_with_slack_user_identity( + slack_team_identity, organization, slack_id="user_1", role=Role.EDITOR + ) + second_user, slack_user_identity_2 = make_user_with_slack_user_identity( + slack_team_identity, organization, slack_id="user_2", role=Role.EDITOR + ) + + _, token = make_token_for_organization(organization) + client = APIClient() + url = reverse("api-internal:user-unlink-slack", kwargs={"pk": first_user.public_primary_key}) + + response = client.post(url, format="json", **make_user_auth_headers(second_user, token)) + assert response.status_code == status.HTTP_403_FORBIDDEN + first_user.refresh_from_db() + assert first_user.slack_user_identity is not None + + @pytest.mark.django_db def test_user_cant_unlink_backend__another_user( make_organization, make_user_for_organization, make_token_for_organization, make_user_auth_headers diff --git a/engine/apps/api/views/user.py b/engine/apps/api/views/user.py index 7911acd2..d436b2d0 100644 --- a/engine/apps/api/views/user.py +++ b/engine/apps/api/views/user.py @@ -127,6 +127,7 @@ class UserView( "get_verification_code", "get_backend_verification_code", "get_telegram_verification_code", + "unlink_slack", "unlink_telegram", "unlink_backend", "make_test_call", @@ -146,6 +147,7 @@ class UserView( "get_verification_code", "get_backend_verification_code", "get_telegram_verification_code", + "unlink_slack", "unlink_telegram", "unlink_backend", "make_test_call", @@ -350,6 +352,13 @@ class UserView( return Response({"telegram_code": str(new_code.uuid), "bot_link": bot_link}, status=status.HTTP_200_OK) + @action(detail=True, methods=["post"]) + def unlink_slack(self, request, pk): + user = self.get_object() + user.slack_user_identity = None + user.save(update_fields=["slack_user_identity"]) + return Response(status=status.HTTP_200_OK) + @action(detail=True, methods=["post"]) def unlink_telegram(self, request, pk): user = self.get_object() diff --git a/grafana-plugin/src/containers/UserSettings/parts/connectors/SlackConnector.tsx b/grafana-plugin/src/containers/UserSettings/parts/connectors/SlackConnector.tsx index ad84b330..61094479 100644 --- a/grafana-plugin/src/containers/UserSettings/parts/connectors/SlackConnector.tsx +++ b/grafana-plugin/src/containers/UserSettings/parts/connectors/SlackConnector.tsx @@ -32,6 +32,10 @@ const SlackConnector = (props: SlackConnectorProps) => { onTabChange(UserSettingsTab.SlackInfo); }, []); + const handleUnlinkSlackAccount = useCallback(() => { + userStore.unlinkSlack(userStore.currentUserPk); + }, []); + return (
@@ -39,6 +43,9 @@ const SlackConnector = (props: SlackConnectorProps) => { {storeUser.slack_user_identity ? (
Slack account is connected +
) : teamStore.currentTeam?.slack_team_identity ? (
diff --git a/grafana-plugin/src/models/user/user.ts b/grafana-plugin/src/models/user/user.ts index 9f98d04d..2a388913 100644 --- a/grafana-plugin/src/models/user/user.ts +++ b/grafana-plugin/src/models/user/user.ts @@ -124,6 +124,20 @@ export class UserStore extends BaseStore { return await makeRequest(`/users/${userPk}/get_backend_verification_code/?backend=${backend}`, {}); }; + @action + unlinkSlack = async (userPk: User['pk']) => { + await makeRequest(`/users/${userPk}/unlink_slack/`, { + method: 'POST', + }); + + const user = await this.getById(userPk); + + this.items = { + ...this.items, + [user.pk]: user, + }; + }; + @action unlinkTelegram = async (userPk: User['pk']) => { await makeRequest(`/users/${userPk}/unlink_telegram/`, { From 6a36fd623a97aff7caa29a8c06d73d7f1f183aa2 Mon Sep 17 00:00:00 2001 From: Vadim Stepanov Date: Tue, 30 Aug 2022 13:42:24 +0100 Subject: [PATCH 31/77] Add Drone step for pushing linux/arm64 image to DockerHub (#426) --- .drone.yml | 63 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/.drone.yml b/.drone.yml index 58274b4b..e87beed6 100644 --- a/.drone.yml +++ b/.drone.yml @@ -263,6 +263,69 @@ trigger: event: - promote +--- +kind: pipeline +type: docker +name: OSS Release (arm64) +platform: + os: linux + arch: arm64 + +steps: + - name: Check Promote + image: alpine + commands: + - if [ -z "$DRONE_DEPLOY_TO" ]; then echo "Missing DRONE_DEPLOY_TO (Target)"; exit 1; fi + - if [ -z "$DRONE_TAG" ]; then echo "Missing DRONE_TAG"; exit 1; fi + - echo Promoting $DRONE_TAG to $DRONE_DEPLOY_TO + + - name: Image Tag + image: alpine + commands: + - apk add --no-cache bash git sed + - git fetch origin --tags + - chmod +x ./tools/image-tag.sh + - echo $(./tools/image-tag.sh) + - echo $(./tools/image-tag.sh) > .tags + - if [ -z "$DRONE_TAG" ]; then echo "No tag, not modifying version"; else sed "0,/VERSION.*/ s/VERSION.*/VERSION = \"${DRONE_TAG}\"/g" engine/settings/base.py > engine/settings/base.temp && mv engine/settings/base.temp engine/settings/base.py; fi + - cat engine/settings/base.py | grep VERSION | head -1 + depends_on: + - Check Promote + when: + event: + - promote + target: + - oss + ref: + - refs/tags/v*.*.* + + - name: Build and Push Engine Docker Image Backend to Dockerhub + image: plugins/docker + settings: + repo: grafana/oncall + dockerfile: engine/Dockerfile + context: engine/ + password: + from_secret: docker_password + username: + from_secret: docker_username + depends_on: + - Image Tag + + - name: Unrecognized Promote Target + image: alpine + commands: + - echo $DRONE_DEPLOY_TO is not a recognized promote target! + - exit 1 + when: + target: + exclude: + - oss + +trigger: + event: + - promote + --- # Secret for pulling docker images. kind: secret From 6b2dad72d68e6f2e58a57447a615528063e99d5b Mon Sep 17 00:00:00 2001 From: Matias Bordese Date: Tue, 30 Aug 2022 09:55:57 -0300 Subject: [PATCH 32/77] Add insight log entry --- engine/apps/api/views/user.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/engine/apps/api/views/user.py b/engine/apps/api/views/user.py index d436b2d0..affd0ad9 100644 --- a/engine/apps/api/views/user.py +++ b/engine/apps/api/views/user.py @@ -357,6 +357,13 @@ class UserView( user = self.get_object() user.slack_user_identity = None user.save(update_fields=["slack_user_identity"]) + write_chatops_insight_log( + author=request.user, + event_name=ChatOpsEvent.USER_UNLINKED, + chatops_type=ChatOpsType.SLACK, + linked_user=user.username, + linked_user_id=user.public_primary_key, + ) return Response(status=status.HTTP_200_OK) @action(detail=True, methods=["post"]) From ddedf886327eebc3866b567c13fb7a843fbf36fa Mon Sep 17 00:00:00 2001 From: Vadim Stepanov Date: Tue, 30 Aug 2022 14:00:02 +0100 Subject: [PATCH 33/77] Update Drone signature (#430) --- .drone.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.drone.yml b/.drone.yml index e87beed6..fa283561 100644 --- a/.drone.yml +++ b/.drone.yml @@ -397,6 +397,6 @@ kind: secret name: drone_token --- kind: signature -hmac: a74dd831a3d0a87b8fc1db45699a6a834ea769da9f437c55979ae665948c3b3f +hmac: 484ec4337e39172f554faafaa93b67ceb9397d2c668ab2eb6f38176e94402aaa ... From 45a2d46405af59ceb7724bbf6fbaeba8172a6fa7 Mon Sep 17 00:00:00 2001 From: Innokentii Konstantinov Date: Tue, 30 Aug 2022 18:25:19 +0500 Subject: [PATCH 34/77] Fix deletion of public api tokens (#422) --- engine/apps/api/views/public_api_tokens.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/engine/apps/api/views/public_api_tokens.py b/engine/apps/api/views/public_api_tokens.py index 36ba4488..55833ce7 100644 --- a/engine/apps/api/views/public_api_tokens.py +++ b/engine/apps/api/views/public_api_tokens.py @@ -31,7 +31,7 @@ class PublicApiTokenView( def destroy(self, request, *args, **kwargs): instance = self.get_object() - write_resource_insight_log(instance=instance, author=instance.author, event=EntityEvent.DELETED) + write_resource_insight_log(instance=instance, author=request.user, event=EntityEvent.DELETED) self.perform_destroy(instance) return Response(status=status.HTTP_204_NO_CONTENT) From b4e7bbb35fe0723976085b2ebe0f283ccca2dd8d Mon Sep 17 00:00:00 2001 From: Vadim Stepanov Date: Tue, 30 Aug 2022 14:31:04 +0100 Subject: [PATCH 35/77] Update CHANGELOG.md --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 186fc7ba..e0132ea8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Change Log +## v1.0.27 (2022-08-30) +- Bug fixes + ## v1.0.26 (2022-08-26) - Insight log's format fixes - Remove UserNotificationPolicy auto-recreating From 0aa591647b1dccfbef7e96dfe4822b56e1470cb6 Mon Sep 17 00:00:00 2001 From: Vadim Stepanov Date: Tue, 30 Aug 2022 14:41:32 +0100 Subject: [PATCH 36/77] Update CHANGELOG.md --- grafana-plugin/CHANGELOG.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/grafana-plugin/CHANGELOG.md b/grafana-plugin/CHANGELOG.md index ded0f67a..e0132ea8 100644 --- a/grafana-plugin/CHANGELOG.md +++ b/grafana-plugin/CHANGELOG.md @@ -1,5 +1,22 @@ # Change Log +## v1.0.27 (2022-08-30) +- Bug fixes + +## v1.0.26 (2022-08-26) +- Insight log's format fixes +- Remove UserNotificationPolicy auto-recreating + +## v1.0.25 (2022-08-24) +- Bug fixes + +## v1.0.24 (2022-08-24) +- Insight logs +- Default DATA_UPLOAD_MAX_MEMORY_SIZE to 1mb + +## v1.0.23 (2022-08-23) +- Bug fixes + ## v1.0.22 (2022-08-16) - Make STATIC_URL configurable from environment variable From 36ce64b57a63329d23cd790d5f1b8958be58428c Mon Sep 17 00:00:00 2001 From: Julia Date: Tue, 30 Aug 2022 16:43:36 +0300 Subject: [PATCH 37/77] Revert changes in events start date calculation, fix bug with getting wrong events start date --- .../schedules/models/custom_on_call_shift.py | 36 ++++++++-- .../tests/test_custom_on_call_shift.py | 71 ++++++++++++++++++- 2 files changed, 101 insertions(+), 6 deletions(-) diff --git a/engine/apps/schedules/models/custom_on_call_shift.py b/engine/apps/schedules/models/custom_on_call_shift.py index 030f2cb0..5cfac667 100644 --- a/engine/apps/schedules/models/custom_on_call_shift.py +++ b/engine/apps/schedules/models/custom_on_call_shift.py @@ -381,6 +381,23 @@ class CustomOnCallShift(models.Model): days_for_next_event += next_month_days next_event_start = current_event_start + timezone.timedelta(days=days_for_next_event) + end_date = None + # get the period for calculating the current rotation end date for long events with frequency weekly and monthly + if self.frequency == CustomOnCallShift.FREQUENCY_WEEKLY: + DAYS_IN_A_WEEK = 7 + days_diff = 0 + # get the last day of the week with respect to the week_start + if next_event_start.weekday() != self.week_start: + days_diff = DAYS_IN_A_WEEK + next_event_start.weekday() - self.week_start + days_diff %= DAYS_IN_A_WEEK + end_date = next_event_start + timezone.timedelta(days=DAYS_IN_A_WEEK - days_diff - ONE_DAY) + elif self.frequency == CustomOnCallShift.FREQUENCY_MONTHLY: + # get the last day of the month + current_day_number = next_event_start.day + number_of_days = monthrange(next_event_start.year, next_event_start.month)[1] + days_diff = number_of_days - current_day_number + end_date = next_event_start + timezone.timedelta(days=days_diff) + next_event = None # repetitions generate the next event shift according with the recurrence rules repetitions = UnfoldableCalendar(current_event).RepeatedEvent( @@ -388,12 +405,21 @@ class CustomOnCallShift(models.Model): ) ical_iter = repetitions.__iter__() for event in ical_iter: - if event.start >= next_event_start: - next_event = event - break - next_event_dt = next_event.start if next_event is not None else None + if end_date: # end_date exists for long events with frequency weekly and monthly + if end_date >= event.start >= next_event_start: + if event.start >= self.rotation_start: + next_event = event + break + elif end_date < event.start: + break + else: + if event.start >= next_event_start: + next_event = event + break - if self.until and next_event_dt and next_event_dt > self.until: + next_event_dt = next_event.start if next_event is not None else next_event_start + + if self.until and next_event_dt > self.until: return return next_event_dt diff --git a/engine/apps/schedules/tests/test_custom_on_call_shift.py b/engine/apps/schedules/tests/test_custom_on_call_shift.py index 5adeea9b..f5369d48 100644 --- a/engine/apps/schedules/tests/test_custom_on_call_shift.py +++ b/engine/apps/schedules/tests/test_custom_on_call_shift.py @@ -575,7 +575,7 @@ def test_rolling_users_with_diff_start_and_rotation_start_weekly( @pytest.mark.django_db -def test_rolling_users_with_diff_start_and_rotation_start_weekly_by_day( +def test_rolling_users_with_diff_start_and_rotation_start_weekly_by_day_weekend( make_organization_and_user, make_user_for_organization, make_on_call_shift, make_schedule ): organization, user_1 = make_organization_and_user() @@ -640,6 +640,75 @@ def test_rolling_users_with_diff_start_and_rotation_start_weekly_by_day( assert len(users_on_call) == 0 +@pytest.mark.django_db +def test_rolling_users_with_diff_start_and_rotation_start_weekly_by_day( + make_organization_and_user, make_user_for_organization, make_on_call_shift, make_schedule +): + organization, user_1 = make_organization_and_user() + user_2 = make_user_for_organization(organization) + user_3 = make_user_for_organization(organization) + + schedule = make_schedule(organization, schedule_class=OnCallScheduleWeb) + now = timezone.now().replace(microsecond=0) + today_weekday = now.weekday() + weekdays = [(today_weekday + 1) % 7, (today_weekday + 3) % 7] + by_day = [CustomOnCallShift.ICAL_WEEKDAY_MAP[day] for day in weekdays] + + data = { + "priority_level": 1, + "start": now, + "week_start": today_weekday, + "rotation_start": now + timezone.timedelta(days=8, hours=1), + "duration": timezone.timedelta(seconds=1800), + "frequency": CustomOnCallShift.FREQUENCY_WEEKLY, + "schedule": schedule, + "until": now + timezone.timedelta(days=23, minutes=1), + "by_day": by_day, + } + rolling_users = [[user_1], [user_2], [user_3]] + on_call_shift = make_on_call_shift( + organization=organization, shift_type=CustomOnCallShift.TYPE_ROLLING_USERS_EVENT, **data + ) + on_call_shift.add_rolling_users(rolling_users) + + date = now + timezone.timedelta(minutes=5) + + # week 1: weekdays[0] - no (+1 day from start) ; weekdays[1] - no (+3 days from start) user_1 + # week 2: weekdays[0] - no (+8 days from start) ; weekdays[1] - yes (+10 days from start) user_2 + # week 3: weekdays[0] - yes (+15 days from start) ; weekdays[1] - yes (+17 days from start) user_3 + # week 4: weekdays[0] - yes (+22 days from start) ; weekdays[1] - no (+24 days from start) user_1 + user_1_on_call_dates = [date + timezone.timedelta(days=22)] + user_2_on_call_dates = [date + timezone.timedelta(days=10)] + user_3_on_call_dates = [date + timezone.timedelta(days=15), date + timezone.timedelta(days=17)] + nobody_on_call_dates = [ + date, # less than rotation start + date + timezone.timedelta(days=1), # less than rotation start + date + timezone.timedelta(days=3), # less than rotation start + date + timezone.timedelta(days=8), # less than rotation start + date + timezone.timedelta(days=9), # weekday value not in by_day + date + timezone.timedelta(days=24), # higher than until + ] + + for dt in user_1_on_call_dates: + users_on_call = list_users_to_notify_from_ical(schedule, dt) + assert len(users_on_call) == 1 + assert user_1 in users_on_call + + for dt in user_2_on_call_dates: + users_on_call = list_users_to_notify_from_ical(schedule, dt) + assert len(users_on_call) == 1 + assert user_2 in users_on_call + + for dt in user_3_on_call_dates: + users_on_call = list_users_to_notify_from_ical(schedule, dt) + assert len(users_on_call) == 1 + assert user_3 in users_on_call + + for dt in nobody_on_call_dates: + users_on_call = list_users_to_notify_from_ical(schedule, dt) + assert len(users_on_call) == 0 + + @pytest.mark.django_db def test_rolling_users_with_diff_start_and_rotation_start_monthly( make_organization_and_user, make_user_for_organization, make_on_call_shift, make_schedule From df5abd5cc76ebbd18054649bd452026d8c8cd24c Mon Sep 17 00:00:00 2001 From: Michael Derynck Date: Tue, 30 Aug 2022 07:55:21 -0600 Subject: [PATCH 38/77] Update DEVELOPER.md Move drone section and add to table of contents. --- DEVELOPER.md | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/DEVELOPER.md b/DEVELOPER.md index 7fd8fb04..6612a8e7 100644 --- a/DEVELOPER.md +++ b/DEVELOPER.md @@ -1,7 +1,9 @@ * [Developer quickstart](#developer-quickstart) + * [Code style](#code-style) * [Backend setup](#backend-setup) * [Frontend setup](#frontend-setup) * [Slack application setup](#slack-application-setup) + * [Update drone build](#update-drone-build) * [Troubleshooting](#troubleshooting) * [ld: library not found for -lssl](#ld-library-not-found-for--lssl) * [Could not build wheels for cryptography which use PEP 517 and cannot be installed directly](#could-not-build-wheels-for-cryptography-which-use-pep-517-and-cannot-be-installed-directly) @@ -131,6 +133,22 @@ extra_hosts: For Slack app configuration check our docs: https://grafana.com/docs/grafana-cloud/oncall/open-source/#slack-setup + +### Update drone build +The .drone.yml build file must be signed when changes are made to it. Follow these steps: + +If you have not installed drone CLI follow [these instructions](https://docs.drone.io/cli/install/) + +To sign the .drone.yml file: +```bash +export DRONE_SERVER=https://drone.grafana.net + +# Get your drone token from https://drone.grafana.net/account +export DRONE_TOKEN= + +drone sign --save grafana/oncall .drone.yml +``` + ## Troubleshooting ### ld: library not found for -lssl @@ -241,18 +259,3 @@ pytest -n4 5. Create a new Django Server run configuration to Run/Debug the engine - Use a plugin such as EnvFile to load the .env file - Change port from 8000 to 8080 - -## Update drone build -The .drone.yml build file must be signed when changes are made to it. Follow these steps: - -If you have not installed drone CLI follow [these instructions](https://docs.drone.io/cli/install/) - -To sign the .drone.yml file: -```bash -export DRONE_SERVER=https://drone.grafana.net - -# Get your drone token from https://drone.grafana.net/account -export DRONE_TOKEN= - -drone sign --save grafana/oncall .drone.yml -``` From b5862e51d871084a9477143451f31a6114e3a666 Mon Sep 17 00:00:00 2001 From: Matias Bordese Date: Tue, 30 Aug 2022 13:00:18 -0300 Subject: [PATCH 39/77] Add a confirmation request when unlinking slack account --- .../UserSettings/parts/connectors/SlackConnector.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/grafana-plugin/src/containers/UserSettings/parts/connectors/SlackConnector.tsx b/grafana-plugin/src/containers/UserSettings/parts/connectors/SlackConnector.tsx index 61094479..2a8f141c 100644 --- a/grafana-plugin/src/containers/UserSettings/parts/connectors/SlackConnector.tsx +++ b/grafana-plugin/src/containers/UserSettings/parts/connectors/SlackConnector.tsx @@ -5,6 +5,7 @@ import cn from 'classnames/bind'; import PluginLink from 'components/PluginLink/PluginLink'; import Text from 'components/Text/Text'; +import WithConfirm from 'components/WithConfirm/WithConfirm'; import { UserSettingsTab } from 'containers/UserSettings/UserSettings.types'; import { User } from 'models/user/user.types'; import { useStore } from 'state/useStore'; @@ -43,9 +44,11 @@ const SlackConnector = (props: SlackConnectorProps) => { {storeUser.slack_user_identity ? (
Slack account is connected - + + +
) : teamStore.currentTeam?.slack_team_identity ? (
From ef6e3cdcaac691553675cd2af71267b6b0bc1b38 Mon Sep 17 00:00:00 2001 From: Vadim Stepanov Date: Tue, 30 Aug 2022 18:36:13 +0100 Subject: [PATCH 40/77] Revert #426 (#435) This reverts commit 6a36fd623a97aff7caa29a8c06d73d7f1f183aa2. --- .drone.yml | 63 ------------------------------------------------------ 1 file changed, 63 deletions(-) diff --git a/.drone.yml b/.drone.yml index fa283561..530bc53c 100644 --- a/.drone.yml +++ b/.drone.yml @@ -263,69 +263,6 @@ trigger: event: - promote ---- -kind: pipeline -type: docker -name: OSS Release (arm64) -platform: - os: linux - arch: arm64 - -steps: - - name: Check Promote - image: alpine - commands: - - if [ -z "$DRONE_DEPLOY_TO" ]; then echo "Missing DRONE_DEPLOY_TO (Target)"; exit 1; fi - - if [ -z "$DRONE_TAG" ]; then echo "Missing DRONE_TAG"; exit 1; fi - - echo Promoting $DRONE_TAG to $DRONE_DEPLOY_TO - - - name: Image Tag - image: alpine - commands: - - apk add --no-cache bash git sed - - git fetch origin --tags - - chmod +x ./tools/image-tag.sh - - echo $(./tools/image-tag.sh) - - echo $(./tools/image-tag.sh) > .tags - - if [ -z "$DRONE_TAG" ]; then echo "No tag, not modifying version"; else sed "0,/VERSION.*/ s/VERSION.*/VERSION = \"${DRONE_TAG}\"/g" engine/settings/base.py > engine/settings/base.temp && mv engine/settings/base.temp engine/settings/base.py; fi - - cat engine/settings/base.py | grep VERSION | head -1 - depends_on: - - Check Promote - when: - event: - - promote - target: - - oss - ref: - - refs/tags/v*.*.* - - - name: Build and Push Engine Docker Image Backend to Dockerhub - image: plugins/docker - settings: - repo: grafana/oncall - dockerfile: engine/Dockerfile - context: engine/ - password: - from_secret: docker_password - username: - from_secret: docker_username - depends_on: - - Image Tag - - - name: Unrecognized Promote Target - image: alpine - commands: - - echo $DRONE_DEPLOY_TO is not a recognized promote target! - - exit 1 - when: - target: - exclude: - - oss - -trigger: - event: - - promote - --- # Secret for pulling docker images. kind: secret From 2650a09d02b80c24322752e0df548a888d97bacc Mon Sep 17 00:00:00 2001 From: Vadim Stepanov Date: Tue, 30 Aug 2022 18:38:34 +0100 Subject: [PATCH 41/77] drone sign --- .drone.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.drone.yml b/.drone.yml index 530bc53c..58274b4b 100644 --- a/.drone.yml +++ b/.drone.yml @@ -334,6 +334,6 @@ kind: secret name: drone_token --- kind: signature -hmac: 484ec4337e39172f554faafaa93b67ceb9397d2c668ab2eb6f38176e94402aaa +hmac: a74dd831a3d0a87b8fc1db45699a6a834ea769da9f437c55979ae665948c3b3f ... From cb2f03eeb8df2444aa6ab15045118ac05f3162a4 Mon Sep 17 00:00:00 2001 From: Vadim Stepanov Date: Tue, 30 Aug 2022 18:45:41 +0100 Subject: [PATCH 42/77] Add drone step for creating & pushing docker manifest list + cleanup (#434) * add drone step for creating & pushing docker manifest + cleanup * drone sign --- .drone.yml | 148 +++++++++++++++++++++++++++++++++++------------------ 1 file changed, 98 insertions(+), 50 deletions(-) diff --git a/.drone.yml b/.drone.yml index 58274b4b..8bf3201a 100644 --- a/.drone.yml +++ b/.drone.yml @@ -158,17 +158,10 @@ trigger: --- kind: pipeline type: docker -name: OSS Release +name: OSS plugin release steps: - - name: Check Promote - image: alpine - commands: - - if [ -z "$DRONE_DEPLOY_TO" ]; then echo "Missing DRONE_DEPLOY_TO (Target)"; exit 1; fi - - if [ -z "$DRONE_TAG" ]; then echo "Missing DRONE_TAG"; exit 1; fi - - echo Promoting $DRONE_TAG to $DRONE_DEPLOY_TO - - - name: Build Plugin + - name: build plugin image: node:14.6.0-stretch commands: - apt-get update @@ -178,23 +171,14 @@ steps: - yarn --network-timeout 500000 - yarn build - ls ./ - depends_on: - - Check Promote - when: - event: - - promote - target: - - oss - ref: - - refs/tags/v*.*.* - - name: Sign and Package Plugin + - name: sign and package plugin image: node:14.6.0-stretch environment: GRAFANA_API_KEY: from_secret: gcom_plugin_publisher_api_key depends_on: - - Build Plugin + - build plugin commands: - apt-get update - apt-get install zip @@ -206,7 +190,7 @@ steps: - zip -r grafana-oncall-app.zip ./grafana-oncall-app - if [ -z "$DRONE_TAG" ]; then echo "No tag, skipping archive"; else cp grafana-oncall-app.zip grafana-oncall-app-${DRONE_TAG}.zip; fi - - name: Publish Plugin to grafana.com (release) + - name: publish plugin to grafana.com (release) image: curlimages/curl:7.73.0 environment: GRAFANA_API_KEY: @@ -214,32 +198,36 @@ steps: commands: - "curl -f -s -H \"Authorization: Bearer $${GRAFANA_API_KEY}\" -d \"download[any][url]=https://storage.googleapis.com/grafana-oncall-app/releases/grafana-oncall-app-${DRONE_TAG}.zip\" -d \"download[any][md5]=$$(curl -sL https://storage.googleapis.com/grafana-oncall-app/releases/grafana-oncall-app-${DRONE_TAG}.zip | md5sum | cut -d' ' -f1)\" -d url=https://github.com/grafana/oncall/grafana-plugin https://grafana.com/api/plugins" depends_on: - - Sign and Package Plugin + - sign and package plugin - - name: Image Tag +trigger: + event: + - promote + target: + - oss + ref: + - refs/tags/v*.*.* + +--- +kind: pipeline +type: docker +name: OSS engine release (amd64) +platform: + os: linux + arch: amd64 +steps: + - name: set engine version image: alpine commands: - - apk add --no-cache bash git sed - - git fetch origin --tags - - chmod +x ./tools/image-tag.sh - - echo $(./tools/image-tag.sh) - - echo $(./tools/image-tag.sh) > .tags + - apk add --no-cache bash sed - if [ -z "$DRONE_TAG" ]; then echo "No tag, not modifying version"; else sed "0,/VERSION.*/ s/VERSION.*/VERSION = \"${DRONE_TAG}\"/g" engine/settings/base.py > engine/settings/base.temp && mv engine/settings/base.temp engine/settings/base.py; fi - cat engine/settings/base.py | grep VERSION | head -1 - depends_on: - - Check Promote - when: - event: - - promote - target: - - oss - ref: - - refs/tags/v*.*.* - - name: Build and Push Engine Docker Image Backend to Dockerhub + - name: build and push docker image image: plugins/docker settings: repo: grafana/oncall + tags: ${DRONE_TAG}-amd64-linux dockerfile: engine/Dockerfile context: engine/ password: @@ -247,21 +235,81 @@ steps: username: from_secret: docker_username depends_on: - - Image Tag - - - name: Unrecognized Promote Target - image: alpine - commands: - - echo $DRONE_DEPLOY_TO is not a recognized promote target! - - exit 1 - when: - target: - exclude: - - oss + - set engine version trigger: event: - promote + target: + - oss + ref: + - refs/tags/v*.*.* + +--- +kind: pipeline +type: docker +name: OSS engine release (arm64) +platform: + os: linux + arch: arm64 +steps: + - name: set engine version + image: alpine + commands: + - apk add --no-cache bash sed + - if [ -z "$DRONE_TAG" ]; then echo "No tag, not modifying version"; else sed "0,/VERSION.*/ s/VERSION.*/VERSION = \"${DRONE_TAG}\"/g" engine/settings/base.py > engine/settings/base.temp && mv engine/settings/base.temp engine/settings/base.py; fi + - cat engine/settings/base.py | grep VERSION | head -1 + + - name: build and push docker image + image: plugins/docker + settings: + repo: grafana/oncall + tags: ${DRONE_TAG}-arm64-linux + dockerfile: engine/Dockerfile + context: engine/ + password: + from_secret: docker_password + username: + from_secret: docker_username + depends_on: + - set engine version + +trigger: + event: + - promote + target: + - oss + ref: + - refs/tags/v*.*.* + +--- +kind: pipeline +type: docker +name: manifest +steps: + - name: manifest + image: plugins/manifest + settings: + username: + from_secret: docker_username + password: + from_secret: docker_password + target: grafana/oncall:${DRONE_TAG} + template: grafana/oncall:${DRONE_TAG}-ARCH-OS + platforms: + - linux/amd64 + - linux/arm64 +depends_on: + - OSS engine release (amd64) + - OSS engine release (arm64) + +trigger: + event: + - promote + target: + - oss + ref: + - refs/tags/v*.*.* --- # Secret for pulling docker images. @@ -334,6 +382,6 @@ kind: secret name: drone_token --- kind: signature -hmac: a74dd831a3d0a87b8fc1db45699a6a834ea769da9f437c55979ae665948c3b3f +hmac: a52e4cea86ef50bbf7b19a05bec619368281e58927206709ef1e9dfd1df44e0e ... From 88504e6e3c8b995467192eca7a85a8f11b1c6b96 Mon Sep 17 00:00:00 2001 From: Vadim Stepanov Date: Tue, 30 Aug 2022 18:56:35 +0100 Subject: [PATCH 43/77] drone.yml move depends_on up for manifest step --- .drone.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.drone.yml b/.drone.yml index 8bf3201a..94ed445b 100644 --- a/.drone.yml +++ b/.drone.yml @@ -283,6 +283,9 @@ trigger: - refs/tags/v*.*.* --- +depends_on: + - OSS engine release (amd64) + - OSS engine release (arm64) kind: pipeline type: docker name: manifest @@ -299,9 +302,6 @@ steps: platforms: - linux/amd64 - linux/arm64 -depends_on: - - OSS engine release (amd64) - - OSS engine release (arm64) trigger: event: From ae280474b17ba46d18548a6dd879c5c86d3895eb Mon Sep 17 00:00:00 2001 From: Vadim Stepanov Date: Tue, 30 Aug 2022 18:57:41 +0100 Subject: [PATCH 44/77] drone sign --- .drone.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.drone.yml b/.drone.yml index 94ed445b..36627c64 100644 --- a/.drone.yml +++ b/.drone.yml @@ -382,6 +382,6 @@ kind: secret name: drone_token --- kind: signature -hmac: a52e4cea86ef50bbf7b19a05bec619368281e58927206709ef1e9dfd1df44e0e +hmac: 79a494b96019fe2dd27e14dd74314a00f451a79a44a54bb6b7e1bfb978374264 ... From 86b174bf619bfc173abe791568f402f90da42189 Mon Sep 17 00:00:00 2001 From: Vadim Stepanov Date: Tue, 30 Aug 2022 19:00:10 +0100 Subject: [PATCH 45/77] drone debug --- .drone.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.drone.yml b/.drone.yml index 36627c64..828a6524 100644 --- a/.drone.yml +++ b/.drone.yml @@ -297,8 +297,8 @@ steps: from_secret: docker_username password: from_secret: docker_password - target: grafana/oncall:${DRONE_TAG} template: grafana/oncall:${DRONE_TAG}-ARCH-OS + target: grafana/oncall:${DRONE_TAG} platforms: - linux/amd64 - linux/arm64 @@ -382,6 +382,6 @@ kind: secret name: drone_token --- kind: signature -hmac: 79a494b96019fe2dd27e14dd74314a00f451a79a44a54bb6b7e1bfb978374264 +hmac: 9b99dec3391988531b1e76b31ddaed46cd4f76dde311ab22150f99fdcfe39d79 ... From beb3006b3b91d0b0dfddd02ae6839e881d0c81dd Mon Sep 17 00:00:00 2001 From: Vadim Stepanov Date: Tue, 30 Aug 2022 19:02:20 +0100 Subject: [PATCH 46/77] drone debug --- .drone.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.drone.yml b/.drone.yml index 828a6524..083a3be8 100644 --- a/.drone.yml +++ b/.drone.yml @@ -297,8 +297,8 @@ steps: from_secret: docker_username password: from_secret: docker_password - template: grafana/oncall:${DRONE_TAG}-ARCH-OS - target: grafana/oncall:${DRONE_TAG} + template: grafana/oncall:$${DRONE_TAG}-ARCH-OS + target: grafana/oncall:$${DRONE_TAG} platforms: - linux/amd64 - linux/arm64 @@ -382,6 +382,6 @@ kind: secret name: drone_token --- kind: signature -hmac: 9b99dec3391988531b1e76b31ddaed46cd4f76dde311ab22150f99fdcfe39d79 +hmac: eae39c0217bdba11b2bfe75861a67b591f871bc064d9b217e3eacc29fab2ce7b ... From 4936d24dc77c4d6566d73beddfb3ae303fa016f1 Mon Sep 17 00:00:00 2001 From: Vadim Stepanov Date: Tue, 30 Aug 2022 19:08:40 +0100 Subject: [PATCH 47/77] drone debug --- .drone.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.drone.yml b/.drone.yml index 083a3be8..a8a4042f 100644 --- a/.drone.yml +++ b/.drone.yml @@ -7,6 +7,7 @@ steps: - name: Build Plugin image: node:14.6.0-stretch commands: + - echo $${DRONE_TAG} - apt-get update - apt-get --assume-yes install jq - cd grafana-plugin/ @@ -382,6 +383,6 @@ kind: secret name: drone_token --- kind: signature -hmac: eae39c0217bdba11b2bfe75861a67b591f871bc064d9b217e3eacc29fab2ce7b +hmac: a1ce6492128c5fd68feb6fe26bd8d46b3baa35f12a998617e007695088d227c0 ... From c71debc2e5027af062b711b13d71946bdf562a7d Mon Sep 17 00:00:00 2001 From: Vadim Stepanov Date: Tue, 30 Aug 2022 19:10:16 +0100 Subject: [PATCH 48/77] drone debug --- .drone.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.drone.yml b/.drone.yml index a8a4042f..e53ec46d 100644 --- a/.drone.yml +++ b/.drone.yml @@ -298,8 +298,8 @@ steps: from_secret: docker_username password: from_secret: docker_password - template: grafana/oncall:$${DRONE_TAG}-ARCH-OS target: grafana/oncall:$${DRONE_TAG} + template: grafana/oncall:${DRONE_TAG}-ARCH-OS platforms: - linux/amd64 - linux/arm64 @@ -383,6 +383,6 @@ kind: secret name: drone_token --- kind: signature -hmac: a1ce6492128c5fd68feb6fe26bd8d46b3baa35f12a998617e007695088d227c0 +hmac: 9b84b0bba4ca719f7b6eab93b7972f5a4ae348184374e112228607536727d27b ... From 571bfd24bd62bebf380ef7c88d1afcc96199f233 Mon Sep 17 00:00:00 2001 From: Vadim Stepanov Date: Tue, 30 Aug 2022 19:11:40 +0100 Subject: [PATCH 49/77] drone debug --- .drone.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.drone.yml b/.drone.yml index e53ec46d..346178a9 100644 --- a/.drone.yml +++ b/.drone.yml @@ -7,7 +7,6 @@ steps: - name: Build Plugin image: node:14.6.0-stretch commands: - - echo $${DRONE_TAG} - apt-get update - apt-get --assume-yes install jq - cd grafana-plugin/ @@ -298,7 +297,7 @@ steps: from_secret: docker_username password: from_secret: docker_password - target: grafana/oncall:$${DRONE_TAG} + target: grafana/oncall:${DRONE_TAG} template: grafana/oncall:${DRONE_TAG}-ARCH-OS platforms: - linux/amd64 @@ -383,6 +382,6 @@ kind: secret name: drone_token --- kind: signature -hmac: 9b84b0bba4ca719f7b6eab93b7972f5a4ae348184374e112228607536727d27b +hmac: 664d7a6f2620b1fbc110f968da3668db91fab35baeb0214e178579ac7a422fc3 ... From 40f329a0029333fc2eef37714b5c4f1b4f5b9c8a Mon Sep 17 00:00:00 2001 From: Vadim Stepanov Date: Tue, 30 Aug 2022 19:12:19 +0100 Subject: [PATCH 50/77] drone debug --- .drone.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.drone.yml b/.drone.yml index 346178a9..36627c64 100644 --- a/.drone.yml +++ b/.drone.yml @@ -382,6 +382,6 @@ kind: secret name: drone_token --- kind: signature -hmac: 664d7a6f2620b1fbc110f968da3668db91fab35baeb0214e178579ac7a422fc3 +hmac: 79a494b96019fe2dd27e14dd74314a00f451a79a44a54bb6b7e1bfb978374264 ... From 15c2070898a0ca118fb36a07b6e7ff106cee2a4a Mon Sep 17 00:00:00 2001 From: Vadim Stepanov Date: Tue, 30 Aug 2022 19:15:16 +0100 Subject: [PATCH 51/77] Revert "Add drone step for creating & pushing docker manifest list + cleanup (#434)" This reverts commit cb2f03ee --- .drone.yml | 142 ++++++++++++++++++----------------------------------- 1 file changed, 47 insertions(+), 95 deletions(-) diff --git a/.drone.yml b/.drone.yml index 36627c64..58274b4b 100644 --- a/.drone.yml +++ b/.drone.yml @@ -158,10 +158,17 @@ trigger: --- kind: pipeline type: docker -name: OSS plugin release +name: OSS Release steps: - - name: build plugin + - name: Check Promote + image: alpine + commands: + - if [ -z "$DRONE_DEPLOY_TO" ]; then echo "Missing DRONE_DEPLOY_TO (Target)"; exit 1; fi + - if [ -z "$DRONE_TAG" ]; then echo "Missing DRONE_TAG"; exit 1; fi + - echo Promoting $DRONE_TAG to $DRONE_DEPLOY_TO + + - name: Build Plugin image: node:14.6.0-stretch commands: - apt-get update @@ -171,14 +178,23 @@ steps: - yarn --network-timeout 500000 - yarn build - ls ./ + depends_on: + - Check Promote + when: + event: + - promote + target: + - oss + ref: + - refs/tags/v*.*.* - - name: sign and package plugin + - name: Sign and Package Plugin image: node:14.6.0-stretch environment: GRAFANA_API_KEY: from_secret: gcom_plugin_publisher_api_key depends_on: - - build plugin + - Build Plugin commands: - apt-get update - apt-get install zip @@ -190,7 +206,7 @@ steps: - zip -r grafana-oncall-app.zip ./grafana-oncall-app - if [ -z "$DRONE_TAG" ]; then echo "No tag, skipping archive"; else cp grafana-oncall-app.zip grafana-oncall-app-${DRONE_TAG}.zip; fi - - name: publish plugin to grafana.com (release) + - name: Publish Plugin to grafana.com (release) image: curlimages/curl:7.73.0 environment: GRAFANA_API_KEY: @@ -198,36 +214,32 @@ steps: commands: - "curl -f -s -H \"Authorization: Bearer $${GRAFANA_API_KEY}\" -d \"download[any][url]=https://storage.googleapis.com/grafana-oncall-app/releases/grafana-oncall-app-${DRONE_TAG}.zip\" -d \"download[any][md5]=$$(curl -sL https://storage.googleapis.com/grafana-oncall-app/releases/grafana-oncall-app-${DRONE_TAG}.zip | md5sum | cut -d' ' -f1)\" -d url=https://github.com/grafana/oncall/grafana-plugin https://grafana.com/api/plugins" depends_on: - - sign and package plugin + - Sign and Package Plugin -trigger: - event: - - promote - target: - - oss - ref: - - refs/tags/v*.*.* - ---- -kind: pipeline -type: docker -name: OSS engine release (amd64) -platform: - os: linux - arch: amd64 -steps: - - name: set engine version + - name: Image Tag image: alpine commands: - - apk add --no-cache bash sed + - apk add --no-cache bash git sed + - git fetch origin --tags + - chmod +x ./tools/image-tag.sh + - echo $(./tools/image-tag.sh) + - echo $(./tools/image-tag.sh) > .tags - if [ -z "$DRONE_TAG" ]; then echo "No tag, not modifying version"; else sed "0,/VERSION.*/ s/VERSION.*/VERSION = \"${DRONE_TAG}\"/g" engine/settings/base.py > engine/settings/base.temp && mv engine/settings/base.temp engine/settings/base.py; fi - cat engine/settings/base.py | grep VERSION | head -1 + depends_on: + - Check Promote + when: + event: + - promote + target: + - oss + ref: + - refs/tags/v*.*.* - - name: build and push docker image + - name: Build and Push Engine Docker Image Backend to Dockerhub image: plugins/docker settings: repo: grafana/oncall - tags: ${DRONE_TAG}-amd64-linux dockerfile: engine/Dockerfile context: engine/ password: @@ -235,81 +247,21 @@ steps: username: from_secret: docker_username depends_on: - - set engine version + - Image Tag -trigger: - event: - - promote - target: - - oss - ref: - - refs/tags/v*.*.* - ---- -kind: pipeline -type: docker -name: OSS engine release (arm64) -platform: - os: linux - arch: arm64 -steps: - - name: set engine version + - name: Unrecognized Promote Target image: alpine commands: - - apk add --no-cache bash sed - - if [ -z "$DRONE_TAG" ]; then echo "No tag, not modifying version"; else sed "0,/VERSION.*/ s/VERSION.*/VERSION = \"${DRONE_TAG}\"/g" engine/settings/base.py > engine/settings/base.temp && mv engine/settings/base.temp engine/settings/base.py; fi - - cat engine/settings/base.py | grep VERSION | head -1 - - - name: build and push docker image - image: plugins/docker - settings: - repo: grafana/oncall - tags: ${DRONE_TAG}-arm64-linux - dockerfile: engine/Dockerfile - context: engine/ - password: - from_secret: docker_password - username: - from_secret: docker_username - depends_on: - - set engine version + - echo $DRONE_DEPLOY_TO is not a recognized promote target! + - exit 1 + when: + target: + exclude: + - oss trigger: event: - promote - target: - - oss - ref: - - refs/tags/v*.*.* - ---- -depends_on: - - OSS engine release (amd64) - - OSS engine release (arm64) -kind: pipeline -type: docker -name: manifest -steps: - - name: manifest - image: plugins/manifest - settings: - username: - from_secret: docker_username - password: - from_secret: docker_password - target: grafana/oncall:${DRONE_TAG} - template: grafana/oncall:${DRONE_TAG}-ARCH-OS - platforms: - - linux/amd64 - - linux/arm64 - -trigger: - event: - - promote - target: - - oss - ref: - - refs/tags/v*.*.* --- # Secret for pulling docker images. @@ -382,6 +334,6 @@ kind: secret name: drone_token --- kind: signature -hmac: 79a494b96019fe2dd27e14dd74314a00f451a79a44a54bb6b7e1bfb978374264 +hmac: a74dd831a3d0a87b8fc1db45699a6a834ea769da9f437c55979ae665948c3b3f ... From c26158ba9d70549f2413486a53bb0ea9c5b8f2e5 Mon Sep 17 00:00:00 2001 From: Michael Derynck Date: Tue, 30 Aug 2022 15:26:04 -0600 Subject: [PATCH 52/77] Check if notification policies is empty --- engine/apps/alerts/tasks/notify_group.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/engine/apps/alerts/tasks/notify_group.py b/engine/apps/alerts/tasks/notify_group.py index 9803affb..0a748c33 100644 --- a/engine/apps/alerts/tasks/notify_group.py +++ b/engine/apps/alerts/tasks/notify_group.py @@ -62,12 +62,14 @@ def notify_group_task(alert_group_pk, escalation_policy_snapshot_order=None): user=user, important=escalation_policy_step == EscalationPolicy.STEP_NOTIFY_GROUP_IMPORTANT, ) - usergroup_notification_plan += "\n_{} (".format( - step.get_user_notification_message_for_thread_for_usergroup(user, notification_policies.first()) - ) + notification_channels = [] if notification_policies.filter(step=UserNotificationPolicy.Step.NOTIFY).count() == 0: usergroup_notification_plan += "Empty notifications" + else: + usergroup_notification_plan += "\n_{} (".format( + step.get_user_notification_message_for_thread_for_usergroup(user, notification_policies.first()) + ) for notification_policy in notification_policies: if notification_policy.step == UserNotificationPolicy.Step.NOTIFY: notification_channels.append( From 53b9bb638c42c94e4db3fa359a163bd2e17581a1 Mon Sep 17 00:00:00 2001 From: Michael Derynck Date: Tue, 30 Aug 2022 15:29:10 -0600 Subject: [PATCH 53/77] Check if notification policies is empty --- engine/apps/alerts/tasks/notify_group.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/engine/apps/alerts/tasks/notify_group.py b/engine/apps/alerts/tasks/notify_group.py index 0a748c33..a3de0b9f 100644 --- a/engine/apps/alerts/tasks/notify_group.py +++ b/engine/apps/alerts/tasks/notify_group.py @@ -63,13 +63,15 @@ def notify_group_task(alert_group_pk, escalation_policy_snapshot_order=None): important=escalation_policy_step == EscalationPolicy.STEP_NOTIFY_GROUP_IMPORTANT, ) - notification_channels = [] - if notification_policies.filter(step=UserNotificationPolicy.Step.NOTIFY).count() == 0: - usergroup_notification_plan += "Empty notifications" - else: + if notification_policies: usergroup_notification_plan += "\n_{} (".format( step.get_user_notification_message_for_thread_for_usergroup(user, notification_policies.first()) ) + + notification_channels = [] + if notification_policies.filter(step=UserNotificationPolicy.Step.NOTIFY).count() == 0: + usergroup_notification_plan += "Empty notifications" + for notification_policy in notification_policies: if notification_policy.step == UserNotificationPolicy.Step.NOTIFY: notification_channels.append( From ba1dfd853f19191e41cac44e63c237e5b7937e74 Mon Sep 17 00:00:00 2001 From: Innokentii Konstantinov Date: Wed, 31 Aug 2022 14:27:12 +0500 Subject: [PATCH 54/77] Fix GRAFANA_CLOUD_ONCALL_API_URL --- engine/settings/base.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/engine/settings/base.py b/engine/settings/base.py index 53c35c66..6b1ea143 100644 --- a/engine/settings/base.py +++ b/engine/settings/base.py @@ -75,7 +75,9 @@ SENDGRID_SECRET_KEY = os.environ.get("SENDGRID_SECRET_KEY") SENDGRID_INBOUND_EMAIL_DOMAIN = os.environ.get("SENDGRID_INBOUND_EMAIL_DOMAIN") # For Grafana Cloud integration -GRAFANA_CLOUD_ONCALL_API_URL = os.environ.get("GRAFANA_CLOUD_ONCALL_API_URL", "https://a-prod-us-central-0.grafana.net") +GRAFANA_CLOUD_ONCALL_API_URL = os.environ.get( + "GRAFANA_CLOUD_ONCALL_API_URL", "https://oncall-prod-us-central-0.grafana.net/oncall" +) GRAFANA_CLOUD_ONCALL_TOKEN = os.environ.get("GRAFANA_CLOUD_ONCALL_TOKEN", None) # Outgoing webhook settings From fbdf8bf12f371331c821b88797fb6614c3a34801 Mon Sep 17 00:00:00 2001 From: Vadim Stepanov Date: Wed, 31 Aug 2022 10:43:52 +0100 Subject: [PATCH 55/77] Drone arm64 builds (#440) * drone arm64 build * drone sign --- .drone.yml | 148 +++++++++++++++++++++++++++++++++++------------------ 1 file changed, 98 insertions(+), 50 deletions(-) diff --git a/.drone.yml b/.drone.yml index 58274b4b..d90ee47e 100644 --- a/.drone.yml +++ b/.drone.yml @@ -158,17 +158,10 @@ trigger: --- kind: pipeline type: docker -name: OSS Release +name: OSS plugin release steps: - - name: Check Promote - image: alpine - commands: - - if [ -z "$DRONE_DEPLOY_TO" ]; then echo "Missing DRONE_DEPLOY_TO (Target)"; exit 1; fi - - if [ -z "$DRONE_TAG" ]; then echo "Missing DRONE_TAG"; exit 1; fi - - echo Promoting $DRONE_TAG to $DRONE_DEPLOY_TO - - - name: Build Plugin + - name: build plugin image: node:14.6.0-stretch commands: - apt-get update @@ -178,23 +171,14 @@ steps: - yarn --network-timeout 500000 - yarn build - ls ./ - depends_on: - - Check Promote - when: - event: - - promote - target: - - oss - ref: - - refs/tags/v*.*.* - - name: Sign and Package Plugin + - name: sign and package plugin image: node:14.6.0-stretch environment: GRAFANA_API_KEY: from_secret: gcom_plugin_publisher_api_key depends_on: - - Build Plugin + - build plugin commands: - apt-get update - apt-get install zip @@ -206,7 +190,7 @@ steps: - zip -r grafana-oncall-app.zip ./grafana-oncall-app - if [ -z "$DRONE_TAG" ]; then echo "No tag, skipping archive"; else cp grafana-oncall-app.zip grafana-oncall-app-${DRONE_TAG}.zip; fi - - name: Publish Plugin to grafana.com (release) + - name: publish plugin to grafana.com (release) image: curlimages/curl:7.73.0 environment: GRAFANA_API_KEY: @@ -214,32 +198,36 @@ steps: commands: - "curl -f -s -H \"Authorization: Bearer $${GRAFANA_API_KEY}\" -d \"download[any][url]=https://storage.googleapis.com/grafana-oncall-app/releases/grafana-oncall-app-${DRONE_TAG}.zip\" -d \"download[any][md5]=$$(curl -sL https://storage.googleapis.com/grafana-oncall-app/releases/grafana-oncall-app-${DRONE_TAG}.zip | md5sum | cut -d' ' -f1)\" -d url=https://github.com/grafana/oncall/grafana-plugin https://grafana.com/api/plugins" depends_on: - - Sign and Package Plugin + - sign and package plugin - - name: Image Tag +trigger: + event: + - promote + target: + - oss + ref: + - refs/tags/v*.*.* + +--- +kind: pipeline +type: docker +name: OSS engine release (amd64) +platform: + os: linux + arch: amd64 +steps: + - name: set engine version image: alpine commands: - - apk add --no-cache bash git sed - - git fetch origin --tags - - chmod +x ./tools/image-tag.sh - - echo $(./tools/image-tag.sh) - - echo $(./tools/image-tag.sh) > .tags + - apk add --no-cache bash sed - if [ -z "$DRONE_TAG" ]; then echo "No tag, not modifying version"; else sed "0,/VERSION.*/ s/VERSION.*/VERSION = \"${DRONE_TAG}\"/g" engine/settings/base.py > engine/settings/base.temp && mv engine/settings/base.temp engine/settings/base.py; fi - cat engine/settings/base.py | grep VERSION | head -1 - depends_on: - - Check Promote - when: - event: - - promote - target: - - oss - ref: - - refs/tags/v*.*.* - - name: Build and Push Engine Docker Image Backend to Dockerhub + - name: build and push docker image image: plugins/docker settings: repo: grafana/oncall + tags: ${DRONE_TAG}-amd64-linux dockerfile: engine/Dockerfile context: engine/ password: @@ -247,21 +235,81 @@ steps: username: from_secret: docker_username depends_on: - - Image Tag - - - name: Unrecognized Promote Target - image: alpine - commands: - - echo $DRONE_DEPLOY_TO is not a recognized promote target! - - exit 1 - when: - target: - exclude: - - oss + - set engine version trigger: event: - promote + target: + - oss + ref: + - refs/tags/v*.*.* + +--- +kind: pipeline +type: docker +name: OSS engine release (arm64) +platform: + os: linux + arch: arm64 +steps: + - name: set engine version + image: alpine + commands: + - apk add --no-cache bash sed + - if [ -z "$DRONE_TAG" ]; then echo "No tag, not modifying version"; else sed "0,/VERSION.*/ s/VERSION.*/VERSION = \"${DRONE_TAG}\"/g" engine/settings/base.py > engine/settings/base.temp && mv engine/settings/base.temp engine/settings/base.py; fi + - cat engine/settings/base.py | grep VERSION | head -1 + + - name: build and push docker image + image: plugins/docker + settings: + repo: grafana/oncall + tags: ${DRONE_TAG}-arm64-linux + dockerfile: engine/Dockerfile + context: engine/ + password: + from_secret: docker_password + username: + from_secret: docker_username + depends_on: + - set engine version + +trigger: + event: + - promote + target: + - oss + ref: + - refs/tags/v*.*.* + +--- +depends_on: + - OSS engine release (amd64) + - OSS engine release (arm64) +kind: pipeline +type: docker +name: manifest +steps: + - name: manifest + image: plugins/manifest + settings: + username: + from_secret: docker_username + password: + from_secret: docker_password + target: "grafana/oncall:${DRONE_TAG}" + template: "grafana/oncall:${DRONE_TAG}-ARCH-OS" + platforms: + - linux/amd64 + - linux/arm64 + +trigger: + event: + - promote + target: + - oss + ref: + - refs/tags/v*.*.* --- # Secret for pulling docker images. @@ -334,6 +382,6 @@ kind: secret name: drone_token --- kind: signature -hmac: a74dd831a3d0a87b8fc1db45699a6a834ea769da9f437c55979ae665948c3b3f +hmac: 09ddc8e158a950d621d5fc6f6d6b5b33f49ec243e2d122798c0407f11bd9fce0 ... From 21bd096a80fe1a8c7fb27f693d00eb2d1656fd40 Mon Sep 17 00:00:00 2001 From: Vadim Stepanov Date: Wed, 31 Aug 2022 11:49:47 +0100 Subject: [PATCH 56/77] drone docker manifest latest tag --- .drone.yml | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/.drone.yml b/.drone.yml index d90ee47e..54ed71b4 100644 --- a/.drone.yml +++ b/.drone.yml @@ -290,7 +290,7 @@ kind: pipeline type: docker name: manifest steps: - - name: manifest + - name: manifest tag image: plugins/manifest settings: username: @@ -303,6 +303,19 @@ steps: - linux/amd64 - linux/arm64 + - name: manifest latest + image: plugins/manifest + settings: + username: + from_secret: docker_username + password: + from_secret: docker_password + target: "grafana/oncall:latest" + template: "grafana/oncall:${DRONE_TAG}-ARCH-OS" + platforms: + - linux/amd64 + - linux/arm64 + trigger: event: - promote From 910c5e5855ce759278f68d40704f3f2a4b9c0ade Mon Sep 17 00:00:00 2001 From: Vadim Stepanov Date: Wed, 31 Aug 2022 11:54:36 +0100 Subject: [PATCH 57/77] drone sign --- .drone.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.drone.yml b/.drone.yml index 54ed71b4..4eaf7cc4 100644 --- a/.drone.yml +++ b/.drone.yml @@ -395,6 +395,6 @@ kind: secret name: drone_token --- kind: signature -hmac: 09ddc8e158a950d621d5fc6f6d6b5b33f49ec243e2d122798c0407f11bd9fce0 +hmac: 3b2548550485a99d3c4123808f2ae83e51bcc1261a7a5ab8f480e8975acdc870 ... From 86c0e816c586650ac1b553898d7a278225962919 Mon Sep 17 00:00:00 2001 From: Vadim Stepanov Date: Wed, 31 Aug 2022 11:56:03 +0100 Subject: [PATCH 58/77] Update CHANGELOG.md (#443) * Update CHANGELOG.md * update CHANGELOG.md --- CHANGELOG.md | 6 ++++++ grafana-plugin/CHANGELOG.md | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e0132ea8..6545f14f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Change Log +## v1.0.29 (2022-08-31) +- Add arm64 docker image + +## v1.0.28 (2022-08-31) +- Bug fixes + ## v1.0.27 (2022-08-30) - Bug fixes diff --git a/grafana-plugin/CHANGELOG.md b/grafana-plugin/CHANGELOG.md index e0132ea8..6545f14f 100644 --- a/grafana-plugin/CHANGELOG.md +++ b/grafana-plugin/CHANGELOG.md @@ -1,5 +1,11 @@ # Change Log +## v1.0.29 (2022-08-31) +- Add arm64 docker image + +## v1.0.28 (2022-08-31) +- Bug fixes + ## v1.0.27 (2022-08-30) - Bug fixes From b71b92e8dd1a27534c94549e9ff14f3a2d8a42b7 Mon Sep 17 00:00:00 2001 From: Michael Derynck Date: Wed, 31 Aug 2022 12:29:56 -0600 Subject: [PATCH 59/77] Update changelog for v1.0.30 --- CHANGELOG.md | 5 ++++- grafana-plugin/CHANGELOG.md | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6545f14f..b028104c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Change Log +## v1.0.30 (2022-08-31) +- Bug fix: check user notification policy before access + ## v1.0.29 (2022-08-31) - Add arm64 docker image @@ -82,7 +85,7 @@ ## 1.0.2 (2022-06-17) - Fix Grafana Alerting integration to handle API changes in Grafana 9 -- Improve public api endpoint for for outgoing webhooks (/actions) by adding ability to create, update and delete outgoing webhook instance +- Improve public api endpoint for outgoing webhooks (/actions) by adding ability to create, update and delete outgoing webhook instance ## 1.0.0 (2022-06-14) diff --git a/grafana-plugin/CHANGELOG.md b/grafana-plugin/CHANGELOG.md index 6545f14f..b028104c 100644 --- a/grafana-plugin/CHANGELOG.md +++ b/grafana-plugin/CHANGELOG.md @@ -1,5 +1,8 @@ # Change Log +## v1.0.30 (2022-08-31) +- Bug fix: check user notification policy before access + ## v1.0.29 (2022-08-31) - Add arm64 docker image @@ -82,7 +85,7 @@ ## 1.0.2 (2022-06-17) - Fix Grafana Alerting integration to handle API changes in Grafana 9 -- Improve public api endpoint for for outgoing webhooks (/actions) by adding ability to create, update and delete outgoing webhook instance +- Improve public api endpoint for outgoing webhooks (/actions) by adding ability to create, update and delete outgoing webhook instance ## 1.0.0 (2022-06-14) From 6c7193f11a06ee47dcf1dfcbf8c7dc681c780e34 Mon Sep 17 00:00:00 2001 From: Ildar Iskhakov Date: Thu, 1 Sep 2022 12:56:06 +0300 Subject: [PATCH 60/77] Bump celery version, telegram version --- engine/requirements.txt | 5 ++--- engine/settings/base.py | 1 - 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/engine/requirements.txt b/engine/requirements.txt index a3612d8e..8406faf5 100644 --- a/engine/requirements.txt +++ b/engine/requirements.txt @@ -5,16 +5,15 @@ whitenoise==5.3.0 twilio~=6.37.0 phonenumbers==8.10.0 django-ordered-model==3.1.1 -celery==4.3.0 +celery==5.2.7 redis==3.2.0 -django-celery-results==1.0.4 humanize==0.5.1 uwsgi==2.0.20 django-cors-headers==3.7.0 django-debug-toolbar==3.2.1 django-sns-view==0.1.2 kombu==4.5.0 -python-telegram-bot==11.1.0 +python-telegram-bot==13.13 django-silk==4.1.0 django-redis-cache==3.0.0 hiredis==1.0.0 diff --git a/engine/settings/base.py b/engine/settings/base.py index 53c35c66..d02a9652 100644 --- a/engine/settings/base.py +++ b/engine/settings/base.py @@ -130,7 +130,6 @@ INSTALLED_APPS = [ "apps.grafana_plugin", "apps.grafana_plugin_management", "apps.migration_tool", - "django_celery_results", "corsheaders", "debug_toolbar", "social_django", From 7fa26b1664b4a2b04d01e3146e7a57ad06dd3d5d Mon Sep 17 00:00:00 2001 From: Ildar Iskhakov Date: Thu, 1 Sep 2022 13:03:05 +0300 Subject: [PATCH 61/77] Bump kombu version --- engine/requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/engine/requirements.txt b/engine/requirements.txt index 8406faf5..1bf66e51 100644 --- a/engine/requirements.txt +++ b/engine/requirements.txt @@ -12,7 +12,6 @@ uwsgi==2.0.20 django-cors-headers==3.7.0 django-debug-toolbar==3.2.1 django-sns-view==0.1.2 -kombu==4.5.0 python-telegram-bot==13.13 django-silk==4.1.0 django-redis-cache==3.0.0 From 4f6968fc4b50c5f88fe6457161ad321d11c144ba Mon Sep 17 00:00:00 2001 From: Ildar Iskhakov Date: Thu, 1 Sep 2022 13:37:52 +0300 Subject: [PATCH 62/77] Bump celery version, telegram version (#454) * Bump celery version, telegram version * Bump kombu version --- engine/requirements.txt | 6 ++---- engine/settings/base.py | 1 - 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/engine/requirements.txt b/engine/requirements.txt index a3612d8e..1bf66e51 100644 --- a/engine/requirements.txt +++ b/engine/requirements.txt @@ -5,16 +5,14 @@ whitenoise==5.3.0 twilio~=6.37.0 phonenumbers==8.10.0 django-ordered-model==3.1.1 -celery==4.3.0 +celery==5.2.7 redis==3.2.0 -django-celery-results==1.0.4 humanize==0.5.1 uwsgi==2.0.20 django-cors-headers==3.7.0 django-debug-toolbar==3.2.1 django-sns-view==0.1.2 -kombu==4.5.0 -python-telegram-bot==11.1.0 +python-telegram-bot==13.13 django-silk==4.1.0 django-redis-cache==3.0.0 hiredis==1.0.0 diff --git a/engine/settings/base.py b/engine/settings/base.py index 6b1ea143..1d3876b6 100644 --- a/engine/settings/base.py +++ b/engine/settings/base.py @@ -132,7 +132,6 @@ INSTALLED_APPS = [ "apps.grafana_plugin", "apps.grafana_plugin_management", "apps.migration_tool", - "django_celery_results", "corsheaders", "debug_toolbar", "social_django", From 27afa18e6d2f863a369516ac27d1596cc243a8cf Mon Sep 17 00:00:00 2001 From: Ildar Iskhakov Date: Thu, 1 Sep 2022 13:54:19 +0300 Subject: [PATCH 63/77] Change flags position for celery 5 --- engine/celery_with_exporter.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/engine/celery_with_exporter.sh b/engine/celery_with_exporter.sh index 360cc864..d1e020e9 100755 --- a/engine/celery_with_exporter.sh +++ b/engine/celery_with_exporter.sh @@ -25,10 +25,10 @@ if [ -z "$CELERY_WORKER_MAX_TASKS_PER_CHILD" ]; then fi CELERY_ARGS=( + "--quiet" # --quite parameter removes pointless banner when celery starts "-A" "engine" "worker" "-l" "info" - "--quiet" # --quite parameter removes pointless banner when celery starts "--concurrency=$CELERY_WORKER_CONCURRENCY" "--max-tasks-per-child=$CELERY_WORKER_MAX_TASKS_PER_CHILD" "-Q" "$CELERY_WORKER_QUEUE" From f6f29b061e060287988fbf09b2de5220de1829ae Mon Sep 17 00:00:00 2001 From: Innokentii Konstantinov Date: Thu, 1 Sep 2022 16:02:53 +0500 Subject: [PATCH 64/77] Add logging --- engine/apps/schedules/tasks/refresh_ical_files.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/engine/apps/schedules/tasks/refresh_ical_files.py b/engine/apps/schedules/tasks/refresh_ical_files.py index 5e446b8c..5797c668 100644 --- a/engine/apps/schedules/tasks/refresh_ical_files.py +++ b/engine/apps/schedules/tasks/refresh_ical_files.py @@ -44,17 +44,22 @@ def refresh_ical_file(schedule_pk): if schedule.cached_ical_file_primary is not None: if schedule.prev_ical_file_primary is None: run_task_primary = True + task_logger.info(f"run_task_primary {schedule_pk} {run_task_primary} prev_ical_file_primary is None") else: run_task_primary = not is_icals_equal(schedule.cached_ical_file_primary, schedule.prev_ical_file_primary) + task_logger.info(f"run_task_primary {schedule_pk} {run_task_primary} icals not equal") run_task_overrides = False if schedule.cached_ical_file_overrides is not None: if schedule.prev_ical_file_overrides is None: run_task_overrides = True + task_logger.info(f"run_task_overrides {schedule_pk} {run_task_primary} prev_ical_file_overrides is None") else: run_task_overrides = not is_icals_equal( schedule.cached_ical_file_overrides, schedule.prev_ical_file_overrides ) + task_logger.info(f"run_task_overrides {schedule_pk} {run_task_primary} icals not equal") run_task = run_task_primary or run_task_overrides + if run_task: notify_about_empty_shifts_in_schedule.apply_async((schedule_pk,)) notify_about_gaps_in_schedule.apply_async((schedule_pk,)) From 0d24c1850d2d4979ab803163a88c7b95fec8343e Mon Sep 17 00:00:00 2001 From: Innokentii Konstantinov Date: Thu, 1 Sep 2022 16:22:58 +0500 Subject: [PATCH 65/77] Update CHANGELOG.md --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b028104c..21c8fcbd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Change Log +## v1.0.31 (2022-09-01) +- Bump celery version +- Fix oss to cloud connection + ## v1.0.30 (2022-08-31) - Bug fix: check user notification policy before access From 9c5ce34880f9ab62d98a7d1fa6b8b68f8ae3d075 Mon Sep 17 00:00:00 2001 From: Ildar Iskhakov Date: Thu, 1 Sep 2022 15:09:51 +0300 Subject: [PATCH 66/77] Bump libs in requirements.txt --- engine/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/engine/requirements.txt b/engine/requirements.txt index a3612d8e..36ddba8a 100644 --- a/engine/requirements.txt +++ b/engine/requirements.txt @@ -26,7 +26,7 @@ slack-export-viewer==1.0.0 beautifulsoup4==4.8.1 social-auth-app-django==3.1.0 sendgrid==6.1.2 -cryptography==3.2 +cryptography==37.0.4 pytest==5.4.3 pytest-django==3.9.0 pytest_factoryboy==2.0.3 From 5ad172f7045d75c48faf7452fc096afc11cf9063 Mon Sep 17 00:00:00 2001 From: Michael Derynck Date: Thu, 1 Sep 2022 07:29:57 -0600 Subject: [PATCH 67/77] Remove unused footer/duplicate block_id from post message to channel --- .../slack/scenarios/notification_delivery.py | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/engine/apps/slack/scenarios/notification_delivery.py b/engine/apps/slack/scenarios/notification_delivery.py index 056e3ddd..4b9805da 100644 --- a/engine/apps/slack/scenarios/notification_delivery.py +++ b/engine/apps/slack/scenarios/notification_delivery.py @@ -24,7 +24,6 @@ class NotificationDeliveryStep(scenario_step.ScenarioStep): self.post_message_to_channel( f"Attempt to send an SMS to {user_verbal_with_mention} has been failed due to a plan limit", alert_group.slack_message.channel_id, - color="red", ) elif ( log_record.notification_error_code @@ -33,7 +32,6 @@ class NotificationDeliveryStep(scenario_step.ScenarioStep): self.post_message_to_channel( f"Attempt to call to {user_verbal_with_mention} has been failed due to a plan limit", alert_group.slack_message.channel_id, - color="red", ) elif ( log_record.notification_error_code @@ -42,7 +40,6 @@ class NotificationDeliveryStep(scenario_step.ScenarioStep): self.post_message_to_channel( f"Failed to send email to {user_verbal_with_mention}. Exceeded limit for mails", alert_group.slack_message.channel_id, - color="red", ) elif ( log_record.notification_error_code @@ -52,18 +49,14 @@ class NotificationDeliveryStep(scenario_step.ScenarioStep): self.post_message_to_channel( f"Failed to send an SMS to {user_verbal_with_mention}. Phone number is not verified", alert_group.slack_message.channel_id, - color="red", ) elif log_record.notification_channel == UserNotificationPolicy.NotificationChannel.PHONE_CALL: self.post_message_to_channel( f"Failed to call to {user_verbal_with_mention}. Phone number is not verified", alert_group.slack_message.channel_id, - color="red", ) - def post_message_to_channel(self, text, channel, color=None, footer=None): - # TODO: No color in blocks, use prefix emoji? - # color_id = self.get_color_id(color) + def post_message_to_channel(self, text, channel): blocks = [ { "type": "section", @@ -73,15 +66,6 @@ class NotificationDeliveryStep(scenario_step.ScenarioStep): "text": text, }, }, - {"type": "divider"}, - { - "type": "section", - "block_id": "alert", - "text": { - "type": "mrkdwn", - "text": footer, - }, - }, ] try: # TODO: slack-onprem, check exceptions From 5f69350679d2dc940e10b25865d1194c2d632a88 Mon Sep 17 00:00:00 2001 From: Vadim Stepanov Date: Thu, 1 Sep 2022 14:36:34 +0100 Subject: [PATCH 68/77] Handle JSONDecodeError for outgoing webhooks (#460) --- engine/apps/alerts/tasks/custom_button_result.py | 8 +++----- engine/apps/slack/scenarios/distribute_alerts.py | 2 +- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/engine/apps/alerts/tasks/custom_button_result.py b/engine/apps/alerts/tasks/custom_button_result.py index e7e826a5..e1dcff3a 100644 --- a/engine/apps/alerts/tasks/custom_button_result.py +++ b/engine/apps/alerts/tasks/custom_button_result.py @@ -48,11 +48,9 @@ def custom_button_result(custom_button_pk, alert_group_pk, user_pk=None, escalat except TemplateError: is_request_successful = False result_message = "Template error" - except json.JSONDecodeError as e: - task_logger.error( - f"Failed to send build_post_kwargs for alert_group {alert_group_pk}, " f"custom_button {custom_button_pk}" - ) - raise e + except json.JSONDecodeError: + is_request_successful = False + result_message = "JSON decoding error" else: is_request_successful, result_message = request_outgoing_webhook( custom_button.webhook, "POST", post_kwargs=post_kwargs diff --git a/engine/apps/slack/scenarios/distribute_alerts.py b/engine/apps/slack/scenarios/distribute_alerts.py index 2ec3db29..109577b1 100644 --- a/engine/apps/slack/scenarios/distribute_alerts.py +++ b/engine/apps/slack/scenarios/distribute_alerts.py @@ -611,7 +611,7 @@ class CustomButtonProcessStep( custom_button = log_record.custom_button debug_message = "" if not log_record.step_specific_info["is_request_successful"]: - with suppress(TemplateError): + with suppress(TemplateError, json.JSONDecodeError): post_kwargs = custom_button.build_post_kwargs(log_record.alert_group.alerts.first()) curl_request = render_curl_command(log_record.custom_button.webhook, "POST", post_kwargs) debug_message = f"```{curl_request}```" From b6e667928f73736729c8d014020686c918429caa Mon Sep 17 00:00:00 2001 From: Vadim Stepanov Date: Thu, 1 Sep 2022 14:48:14 +0100 Subject: [PATCH 69/77] update changelog --- CHANGELOG.md | 3 +++ grafana-plugin/CHANGELOG.md | 7 +++++++ 2 files changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 21c8fcbd..74ec49b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Change Log +## v1.0.32 (2022-09-01) +- Bug fixes + ## v1.0.31 (2022-09-01) - Bump celery version - Fix oss to cloud connection diff --git a/grafana-plugin/CHANGELOG.md b/grafana-plugin/CHANGELOG.md index b028104c..74ec49b2 100644 --- a/grafana-plugin/CHANGELOG.md +++ b/grafana-plugin/CHANGELOG.md @@ -1,5 +1,12 @@ # Change Log +## v1.0.32 (2022-09-01) +- Bug fixes + +## v1.0.31 (2022-09-01) +- Bump celery version +- Fix oss to cloud connection + ## v1.0.30 (2022-08-31) - Bug fix: check user notification policy before access From c047463a14757d3c1c69648a2fd57fd6ba30aa39 Mon Sep 17 00:00:00 2001 From: Vadim Stepanov Date: Thu, 1 Sep 2022 15:37:59 +0100 Subject: [PATCH 70/77] Update Chart.yaml --- helm/oncall/Chart.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/helm/oncall/Chart.yaml b/helm/oncall/Chart.yaml index 1b853761..2f98dd4c 100644 --- a/helm/oncall/Chart.yaml +++ b/helm/oncall/Chart.yaml @@ -8,13 +8,13 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 1.0.3 +version: 1.0.4 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. # It is recommended to use it with quotes. -appVersion: "v1.0.13" +appVersion: "v1.0.32" dependencies: - name: cert-manager version: v1.8.0 From cc67d49047f092f433831ee42aaaab7d1f62cc67 Mon Sep 17 00:00:00 2001 From: Vadim Stepanov Date: Thu, 1 Sep 2022 17:50:17 +0100 Subject: [PATCH 71/77] Remove outdated tasks & DynamicSettingsManager (#465) --- engine/apps/alerts/tasks/__init__.py | 3 -- .../alerts/tasks/cache_alert_group_for_web.py | 19 ------------ .../invalidate_web_cache_for_alert_group.py | 11 ------- .../tasks/resolve_alert_group_if_needed.py | 31 ------------------- engine/apps/base/models/dynamic_setting.py | 26 +--------------- engine/settings/prod_without_db.py | 3 -- 6 files changed, 1 insertion(+), 92 deletions(-) delete mode 100644 engine/apps/alerts/tasks/cache_alert_group_for_web.py delete mode 100644 engine/apps/alerts/tasks/invalidate_web_cache_for_alert_group.py delete mode 100644 engine/apps/alerts/tasks/resolve_alert_group_if_needed.py diff --git a/engine/apps/alerts/tasks/__init__.py b/engine/apps/alerts/tasks/__init__.py index 79b8b0ed..bf1ad097 100644 --- a/engine/apps/alerts/tasks/__init__.py +++ b/engine/apps/alerts/tasks/__init__.py @@ -1,5 +1,4 @@ from .acknowledge_reminder import acknowledge_reminder_task # noqa: F401 -from .cache_alert_group_for_web import cache_alert_group_for_web, schedule_cache_for_alert_group # noqa: F401 from .calculcate_escalation_finish_time import calculate_escalation_finish_time # noqa from .call_ack_url import call_ack_url # noqa: F401 from .check_escalation_finished import check_escalation_finished_task # noqa: F401 @@ -9,7 +8,6 @@ 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, 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 @@ -17,7 +15,6 @@ from .notify_group import notify_group_task # noqa: F401 from .notify_ical_schedule_shift import notify_ical_schedule_shift # noqa: F401 from .notify_user import notify_user_task # noqa: F401 from .resolve_alert_group_by_source_if_needed import resolve_alert_group_by_source_if_needed # noqa: F401 -from .resolve_alert_group_if_needed import resolve_alert_group_if_needed # noqa: F401 from .resolve_by_last_step import resolve_by_last_step_task # noqa: F401 from .send_alert_group_signal import send_alert_group_signal # noqa: F401 from .send_update_log_report_signal import send_update_log_report_signal # noqa: F401 diff --git a/engine/apps/alerts/tasks/cache_alert_group_for_web.py b/engine/apps/alerts/tasks/cache_alert_group_for_web.py deleted file mode 100644 index 5f0c52d5..00000000 --- a/engine/apps/alerts/tasks/cache_alert_group_for_web.py +++ /dev/null @@ -1,19 +0,0 @@ -from django.conf import settings - -from common.custom_celery_tasks import shared_dedicated_queue_retry_task - - -@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): - # 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): - # todo: remove - pass diff --git a/engine/apps/alerts/tasks/invalidate_web_cache_for_alert_group.py b/engine/apps/alerts/tasks/invalidate_web_cache_for_alert_group.py deleted file mode 100644 index 9c8786d9..00000000 --- a/engine/apps/alerts/tasks/invalidate_web_cache_for_alert_group.py +++ /dev/null @@ -1,11 +0,0 @@ -from django.conf import settings - -from common.custom_celery_tasks import shared_dedicated_queue_retry_task - - -@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): - # todo: remove - pass diff --git a/engine/apps/alerts/tasks/resolve_alert_group_if_needed.py b/engine/apps/alerts/tasks/resolve_alert_group_if_needed.py deleted file mode 100644 index 1fb38712..00000000 --- a/engine/apps/alerts/tasks/resolve_alert_group_if_needed.py +++ /dev/null @@ -1,31 +0,0 @@ -# TODO: remove this file when all the resolve_alert_group_if_needed are processed -# New version - apps.alerts.tasks.resolve_alert_group_by_source_if_needed.resolve_alert_group_by_source_if_needed - -from django.apps import apps -from django.conf import settings - -from common.custom_celery_tasks import shared_dedicated_queue_retry_task - - -@shared_dedicated_queue_retry_task( - autoretry_for=(Exception,), retry_backoff=True, max_retries=1 if settings.DEBUG else None -) -def resolve_alert_group_if_needed(alert_id): - """ - The purpose of this task is to avoid computation-heavy check after each alert. - Should be delayed and invoked only for the last one. - """ - AlertGroupForAlertManager = apps.get_model("alerts", "AlertGroupForAlertManager") - AlertForAlertManager = apps.get_model("alerts", "AlertForAlertManager") - - alert = AlertForAlertManager.objects.get(pk=alert_id) - if not resolve_alert_group_if_needed.request.id == alert.group.active_resolve_calculation_id: - return "Resolve calculation celery ID mismatch. Duplication or non-active. Active: {}".format( - alert.group.active_resolve_calculation_id - ) - else: - # Retrieving group again to have an access to child class methods - alert_group = AlertGroupForAlertManager.all_objects.get(pk=alert.group_id) - if alert_group.is_alert_a_resolve_signal(alert): - alert_group.resolve_by_source() - return f"resolved alert_group {alert_group.pk}" diff --git a/engine/apps/base/models/dynamic_setting.py b/engine/apps/base/models/dynamic_setting.py index de6ce129..70fd4971 100644 --- a/engine/apps/base/models/dynamic_setting.py +++ b/engine/apps/base/models/dynamic_setting.py @@ -1,32 +1,8 @@ -from django.db import IntegrityError, models +from django.db import models from django.db.models import JSONField -class DynamicSettingsManager(models.Manager): - def get_or_create(self, defaults=None, **kwargs): - """ - Using get_or_create inside celery task sometimes triggers making two identical DynamicSettings. - E.g. https://gitlab.amixr.io/amixr/amixr/issues/843 - More info: https://stackoverflow.com/questions/17960593/multipleobjectsreturned-with-get-or-create - Solution is to create UniqueConstraint on DynamicSetting.Name and catch IntegrityError. - Django 3 has built-in check https://github.com/django/django/blob/master/django/db/models/query.py#L571 - As for now we are using Django 2.2 which has not. - # TODO: remove this method when we will move to Django 3 - So it is overridden get_or_create to catch IntegrityError and just return object in this case. - """ - try: - return super(DynamicSettingsManager, self).get_or_create(defaults=defaults, **kwargs) - except IntegrityError: - try: - return self.get(**kwargs), False - except self.model.DoesNotExist: - pass - raise - - class DynamicSetting(models.Model): - objects = DynamicSettingsManager() - name = models.CharField(max_length=100) boolean_value = models.BooleanField(null=True, default=None) numeric_value = models.IntegerField(null=True, default=None) diff --git a/engine/settings/prod_without_db.py b/engine/settings/prod_without_db.py index ed73daed..88261cbb 100644 --- a/engine/settings/prod_without_db.py +++ b/engine/settings/prod_without_db.py @@ -83,9 +83,6 @@ 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" - }, # todo: remove "apps.alerts.tasks.send_alert_group_signal.send_alert_group_signal": {"queue": "default"}, "apps.alerts.tasks.wipe.wipe": {"queue": "default"}, "apps.heartbeat.tasks.heartbeat_checkup": {"queue": "default"}, From 2fdce0504b33d01cf5cbd962aa25f29300834403 Mon Sep 17 00:00:00 2001 From: Vadim Stepanov Date: Fri, 2 Sep 2022 15:16:00 +0100 Subject: [PATCH 72/77] Improve version mismatch warning (#469) --- engine/apps/api/serializers/organization.py | 21 +------------------ .../DefaultPageLayout/DefaultPageLayout.tsx | 20 +++++++----------- 2 files changed, 9 insertions(+), 32 deletions(-) diff --git a/engine/apps/api/serializers/organization.py b/engine/apps/api/serializers/organization.py index 49179049..58013052 100644 --- a/engine/apps/api/serializers/organization.py +++ b/engine/apps/api/serializers/organization.py @@ -3,7 +3,6 @@ from datetime import timedelta import humanize import pytz from django.apps import apps -from django.conf import settings from django.utils import timezone from rest_framework import fields, serializers @@ -110,25 +109,7 @@ class CurrentOrganizationSerializer(OrganizationSerializer): def get_limits(self, obj): user = self.context["request"].user - if not settings.OSS_INSTALLATION: - return obj.notifications_limit_web_report(user) - - # show a version warning on OSS installations in case backend and frontend are different versions - frontend_version = self.context["request"].headers.get("X-OnCall-Plugin-Version") - backend_version = settings.VERSION - version_warning = {} - if backend_version and frontend_version and backend_version != frontend_version: - text = ( - "Version mismatch! Please make sure you have the same versions of the Grafana OnCall plugin " - "and Grafana OnCall engine, " - "otherwise there could be issues with your Grafana OnCall installation! " - f"Current plugin version: {frontend_version}, current engine version: {backend_version}. " - "Please see the update instructions: " - "https://grafana.com/docs/oncall/latest/open-source/#update-grafana-oncall-oss" - ) - version_warning = {"period_title": "Version mismatch", "show_limits_warning": True, "warning_text": text} - - return version_warning or obj.notifications_limit_web_report(user) + return obj.notifications_limit_web_report(user) def get_env_status(self, obj): LiveSetting.populate_settings_if_needed() diff --git a/grafana-plugin/src/containers/DefaultPageLayout/DefaultPageLayout.tsx b/grafana-plugin/src/containers/DefaultPageLayout/DefaultPageLayout.tsx index 5cefb3b1..64e9b877 100644 --- a/grafana-plugin/src/containers/DefaultPageLayout/DefaultPageLayout.tsx +++ b/grafana-plugin/src/containers/DefaultPageLayout/DefaultPageLayout.tsx @@ -93,8 +93,14 @@ const DefaultPageLayout: FC = observer((props) => { {store.backendLicense === GRAFANA_LICENSE_OSS && store.backendVersion && plugin?.version && - store.backendVersion !== plugin?.version && ( - + store.backendVersion !== plugin?.version && + !getItem(`version_mismatch_${store.backendVersion}_${plugin?.version}`) && ( + Please make sure you have the same versions of the Grafana OnCall plugin and the Grafana OnCall engine, otherwise there could be issues with your Grafana OnCall installation!
@@ -107,16 +113,6 @@ const DefaultPageLayout: FC = observer((props) => { .
)} - {currentTeam?.limits.show_limits_warning && - currentTeam?.limits.period_title !== 'Version mismatch' && // don't show version mismatch warning twice - !getItem(currentTeam.limits.warning_text) && ( - - )} {Boolean( currentTeam && currentUser && From 1207ea0fb4df8866b91bff25ffc12c56b5d29377 Mon Sep 17 00:00:00 2001 From: Vadim Stepanov Date: Fri, 2 Sep 2022 15:44:19 +0100 Subject: [PATCH 73/77] Revert "Bump libs in requirements.txt" (#470) --- engine/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/engine/requirements.txt b/engine/requirements.txt index 93fb99df..1bf66e51 100644 --- a/engine/requirements.txt +++ b/engine/requirements.txt @@ -24,7 +24,7 @@ slack-export-viewer==1.0.0 beautifulsoup4==4.8.1 social-auth-app-django==3.1.0 sendgrid==6.1.2 -cryptography==37.0.4 +cryptography==3.2 pytest==5.4.3 pytest-django==3.9.0 pytest_factoryboy==2.0.3 From f1f4303825b7a67ed81e54931057d766c7b8324c Mon Sep 17 00:00:00 2001 From: Vadim Stepanov Date: Mon, 5 Sep 2022 10:05:19 +0100 Subject: [PATCH 74/77] Alert group search by title (#466) * use web title template to render alert group verbose name * remove group_verbose_name from tests * clean up group_verbose_name * remove verbose_name from API & plugin * verbose_name migration * update verbose name on web title template change * use long queue for updating verbose name * use first alert for updating verbose name * improve batch_ids --- .../apps/alerts/integration_options_mixin.py | 1 - .../migrations/0007_populate_verbose_name.py | 23 ++++++ engine/apps/alerts/models/alert.py | 14 ++-- engine/apps/alerts/models/alert_group.py | 2 +- engine/apps/alerts/tasks/__init__.py | 1 + .../alerts/tasks/alert_group_verbose_name.py | 72 +++++++++++++++++++ .../alerts/tests/test_default_templates.py | 1 - engine/apps/api/serializers/alert_group.py | 1 - engine/apps/api/views/alert_group.py | 2 +- .../views/alert_receive_channel_template.py | 10 +++ engine/apps/public_api/views/integrations.py | 17 ++++- engine/config_integrations/alertmanager.py | 2 - engine/config_integrations/elastalert.py | 2 - .../config_integrations/formatted_webhook.py | 2 - engine/config_integrations/grafana.py | 5 -- .../config_integrations/grafana_alerting.py | 2 - engine/config_integrations/heartbeat.py | 2 - engine/config_integrations/inbound_email.py | 2 - engine/config_integrations/kapacitor.py | 2 - engine/config_integrations/maintenance.py | 2 - engine/config_integrations/manual.py | 2 - engine/config_integrations/slack_channel.py | 2 - engine/config_integrations/webhook.py | 2 - engine/settings/prod_without_db.py | 2 + .../src/models/alertgroup/alertgroup.types.ts | 1 - 25 files changed, 131 insertions(+), 43 deletions(-) create mode 100644 engine/apps/alerts/migrations/0007_populate_verbose_name.py create mode 100644 engine/apps/alerts/tasks/alert_group_verbose_name.py diff --git a/engine/apps/alerts/integration_options_mixin.py b/engine/apps/alerts/integration_options_mixin.py index b5b00a41..a0a81bab 100644 --- a/engine/apps/alerts/integration_options_mixin.py +++ b/engine/apps/alerts/integration_options_mixin.py @@ -69,7 +69,6 @@ class IntegrationOptionsMixin: "grouping_id", "resolve_condition", "acknowledge_condition", - "group_verbose_name", "source_link", ] diff --git a/engine/apps/alerts/migrations/0007_populate_verbose_name.py b/engine/apps/alerts/migrations/0007_populate_verbose_name.py new file mode 100644 index 00000000..89d6fc44 --- /dev/null +++ b/engine/apps/alerts/migrations/0007_populate_verbose_name.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.15 on 2022-09-01 16:54 + +from django.db import migrations + +from apps.alerts.models import AlertReceiveChannel +from apps.alerts.tasks import update_verbose_name_for_alert_receive_channel + + +def populate_verbose_name(apps, _): + pks = AlertReceiveChannel.objects_with_deleted.values_list("pk", flat=True) + for pk in pks: + update_verbose_name_for_alert_receive_channel.delay(pk) + + +class Migration(migrations.Migration): + + dependencies = [ + ('alerts', '0006_alertgroup_alerts_aler_channel_ee84a7_idx'), + ] + + operations = [ + migrations.RunPython(populate_verbose_name, migrations.RunPython.noop), + ] diff --git a/engine/apps/alerts/models/alert.py b/engine/apps/alerts/models/alert.py index 8f5b272a..a41d3f12 100644 --- a/engine/apps/alerts/models/alert.py +++ b/engine/apps/alerts/models/alert.py @@ -179,19 +179,19 @@ class Alert(models.Model): is_resolve_signal = False is_acknowledge_signal = False group_distinction = None - group_verbose_name = "Incident" acknowledge_condition_template = template_manager.get_attr_template( "acknowledge_condition", alert_receive_channel ) resolve_condition_template = template_manager.get_attr_template("resolve_condition", alert_receive_channel) grouping_id_template = template_manager.get_attr_template("grouping_id", alert_receive_channel) - # use get_default_attr_template because there is no ability to customize group_verbose_name, only default value - group_verbose_name_template = template_manager.get_default_attr_template( - "group_verbose_name", alert_receive_channel - ) - if group_verbose_name_template is not None: - group_verbose_name, _ = apply_jinja_template(group_verbose_name_template, raw_request_data) + + # set verbose_name to web title to allow alert group searching based on verbose_name + web_title_template = template_manager.get_attr_template("title", alert_receive_channel, render_for="web") + if web_title_template: + group_verbose_name = apply_jinja_template(web_title_template, raw_request_data)[0] or None + else: + group_verbose_name = None if grouping_id_template is not None: group_distinction, _ = apply_jinja_template(grouping_id_template, raw_request_data) diff --git a/engine/apps/alerts/models/alert_group.py b/engine/apps/alerts/models/alert_group.py index 84a0a9aa..1f60ce91 100644 --- a/engine/apps/alerts/models/alert_group.py +++ b/engine/apps/alerts/models/alert_group.py @@ -899,7 +899,7 @@ class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models. self.resolve(resolved_by=AlertGroup.WIPED) self.stop_escalation() self.distinction = "" - self.verbose_name = "Wiped incident" + self.verbose_name = None self.wiped_at = timezone.now() self.wiped_by = user for alert in self.alerts.all(): diff --git a/engine/apps/alerts/tasks/__init__.py b/engine/apps/alerts/tasks/__init__.py index bf1ad097..48a30b7a 100644 --- a/engine/apps/alerts/tasks/__init__.py +++ b/engine/apps/alerts/tasks/__init__.py @@ -1,4 +1,5 @@ from .acknowledge_reminder import acknowledge_reminder_task # noqa: F401 +from .alert_group_verbose_name import update_verbose_name, update_verbose_name_for_alert_receive_channel # noqa:F401 from .calculcate_escalation_finish_time import calculate_escalation_finish_time # noqa from .call_ack_url import call_ack_url # noqa: F401 from .check_escalation_finished import check_escalation_finished_task # noqa: F401 diff --git a/engine/apps/alerts/tasks/alert_group_verbose_name.py b/engine/apps/alerts/tasks/alert_group_verbose_name.py new file mode 100644 index 00000000..202df5bc --- /dev/null +++ b/engine/apps/alerts/tasks/alert_group_verbose_name.py @@ -0,0 +1,72 @@ +from django.db.models import Min + +from apps.alerts.incident_appearance.templaters import TemplateLoader +from apps.alerts.tasks.task_logger import task_logger +from common.custom_celery_tasks import shared_dedicated_queue_retry_task +from common.jinja_templater import apply_jinja_template + +# BATCH_SIZE is how many alert groups will be processed per second (for every individual alert receive channel) +BATCH_SIZE = 1000 + + +def batch_ids(queryset, cursor): + return list(queryset.filter(id__gt=cursor).order_by("id").values_list("id", flat=True)[:BATCH_SIZE]) + + +@shared_dedicated_queue_retry_task +def update_verbose_name_for_alert_receive_channel(alert_receive_channel_pk): + from apps.alerts.models import AlertGroup + + countdown = 0 + cursor = 0 + queryset = AlertGroup.all_objects.filter(channel_id=alert_receive_channel_pk) + ids = batch_ids(queryset, cursor) + + while ids: + update_verbose_name.apply_async((alert_receive_channel_pk, ids[0], ids[-1]), countdown=countdown) + + cursor = ids[-1] + ids = batch_ids(queryset, cursor) + countdown += 1 + + +@shared_dedicated_queue_retry_task +def update_verbose_name(alert_receive_channel_pk, alert_group_pk_start, alert_group_pk_end): + from apps.alerts.models import Alert, AlertGroup, AlertReceiveChannel + + try: + alert_receive_channel = AlertReceiveChannel.objects_with_deleted.get(pk=alert_receive_channel_pk) + except AlertReceiveChannel.DoesNotExist: + task_logger.warning(f"AlertReceiveChannel {alert_receive_channel_pk} doesn't exist") + return + + alert_groups = AlertGroup.all_objects.filter(pk__gte=alert_group_pk_start, pk__lte=alert_group_pk_end).only("pk") + + # get first alerts in 2 SQL queries + alerts_info = ( + Alert.objects.values("group_id") + .filter(group_id__gte=alert_group_pk_start, group_id__lte=alert_group_pk_end) + .annotate(first_alert_id=Min("id")) + ) + alerts_info_map = {info["group_id"]: info for info in alerts_info} + + first_alert_ids = [info["first_alert_id"] for info in alerts_info_map.values()] + first_alerts = Alert.objects.filter(pk__in=first_alert_ids).values("group_id", "raw_request_data") + first_alert_map = {alert["group_id"]: alert for alert in first_alerts} + + template_manager = TemplateLoader() + web_title_template = template_manager.get_attr_template("title", alert_receive_channel, render_for="web") + + for alert_group in alert_groups: + if web_title_template: + if alert_group.pk in first_alert_map: + raw_request_data = first_alert_map[alert_group.pk]["raw_request_data"] + verbose_name = apply_jinja_template(web_title_template, raw_request_data)[0] or None + else: + verbose_name = None + else: + verbose_name = None + + alert_group.verbose_name = verbose_name + + AlertGroup.all_objects.bulk_update(alert_groups, ["verbose_name"]) diff --git a/engine/apps/alerts/tests/test_default_templates.py b/engine/apps/alerts/tests/test_default_templates.py index 63cfd0b8..259aa051 100644 --- a/engine/apps/alerts/tests/test_default_templates.py +++ b/engine/apps/alerts/tests/test_default_templates.py @@ -92,7 +92,6 @@ def test_render_group_data_templates( assert group_data.group_distinction == template_module.tests.get("group_distinction") assert group_data.is_resolve_signal == template_module.tests.get("is_resolve_signal") assert group_data.is_acknowledge_signal == template_module.tests.get("is_acknowledge_signal") - assert group_data.group_verbose_name == template_module.tests.get("group_verbose_name") def test_default_templates_are_valid(): diff --git a/engine/apps/api/serializers/alert_group.py b/engine/apps/api/serializers/alert_group.py index df5583c4..f9ecf443 100644 --- a/engine/apps/api/serializers/alert_group.py +++ b/engine/apps/api/serializers/alert_group.py @@ -61,7 +61,6 @@ class AlertGroupListSerializer(EagerLoadingMixin, serializers.ModelSerializer): "pk", "alerts_count", "inside_organization_number", - "verbose_name", "alert_receive_channel", "resolved", "resolved_by", diff --git a/engine/apps/api/views/alert_group.py b/engine/apps/api/views/alert_group.py index 9fd72296..0d44e150 100644 --- a/engine/apps/api/views/alert_group.py +++ b/engine/apps/api/views/alert_group.py @@ -192,7 +192,7 @@ class AlertGroupView( filter_backends = [SearchFilter, filters.DjangoFilterBackend] # todo: add ability to search by templated title - search_fields = ["public_primary_key", "inside_organization_number"] + search_fields = ["public_primary_key", "inside_organization_number", "verbose_name"] filterset_class = AlertGroupFilter diff --git a/engine/apps/api/views/alert_receive_channel_template.py b/engine/apps/api/views/alert_receive_channel_template.py index ff8cd923..b1fac13b 100644 --- a/engine/apps/api/views/alert_receive_channel_template.py +++ b/engine/apps/api/views/alert_receive_channel_template.py @@ -2,6 +2,7 @@ from rest_framework import mixins, viewsets from rest_framework.permissions import IsAuthenticated from apps.alerts.models import AlertReceiveChannel +from apps.alerts.tasks import update_verbose_name_for_alert_receive_channel from apps.api.permissions import MODIFY_ACTIONS, READ_ACTIONS, ActionPermission, AnyRole, IsAdmin from apps.api.serializers.alert_receive_channel import AlertReceiveChannelTemplatesSerializer from apps.auth_token.auth import PluginAuthentication @@ -36,9 +37,14 @@ class AlertReceiveChannelTemplateView( def update(self, request, *args, **kwargs): instance = self.get_object() prev_state = instance.insight_logs_serialized + prev_web_title_template = instance.web_title_template + result = super().update(request, *args, **kwargs) + instance = self.get_object() new_state = instance.insight_logs_serialized + new_web_title_template = instance.web_title_template + write_resource_insight_log( instance=instance, author=self.request.user, @@ -46,4 +52,8 @@ class AlertReceiveChannelTemplateView( prev_state=prev_state, new_state=new_state, ) + + if new_web_title_template != prev_web_title_template: + update_verbose_name_for_alert_receive_channel.delay(instance.pk) + return result diff --git a/engine/apps/public_api/views/integrations.py b/engine/apps/public_api/views/integrations.py index 36ef6ea3..5c5df6f3 100644 --- a/engine/apps/public_api/views/integrations.py +++ b/engine/apps/public_api/views/integrations.py @@ -5,6 +5,7 @@ from rest_framework.permissions import IsAuthenticated from rest_framework.viewsets import ModelViewSet from apps.alerts.models import AlertReceiveChannel +from apps.alerts.tasks import update_verbose_name_for_alert_receive_channel from apps.auth_token.auth import ApiTokenAuthentication from apps.public_api.serializers import IntegrationSerializer, IntegrationUpdateSerializer from apps.public_api.throttlers.user_throttle import UserThrottle @@ -58,17 +59,27 @@ class IntegrationView( raise NotFound def perform_update(self, serializer): - prev_state = serializer.instance.insight_logs_serialized + instance = serializer.instance + + prev_state = instance.insight_logs_serialized + prev_web_title_template = instance.web_title_template + serializer.save() - new_state = serializer.instance.insight_logs_serialized + + new_state = instance.insight_logs_serialized + new_web_title_template = instance.web_title_template + write_resource_insight_log( - instance=serializer.instance, + instance=instance, author=self.request.user, event=EntityEvent.UPDATED, prev_state=prev_state, new_state=new_state, ) + if new_web_title_template != prev_web_title_template: + update_verbose_name_for_alert_receive_channel.delay(instance.pk) + def perform_destroy(self, instance): write_resource_insight_log(instance=instance, author=self.request.user, event=EntityEvent.DELETED) instance.delete() diff --git a/engine/config_integrations/alertmanager.py b/engine/config_integrations/alertmanager.py index cb2fa9b6..bfdcff2e 100644 --- a/engine/config_integrations/alertmanager.py +++ b/engine/config_integrations/alertmanager.py @@ -116,8 +116,6 @@ resolve_condition = """\ acknowledge_condition = None -group_verbose_name = "Incident" - tests = { "payload": { "endsAt": "0001-01-01T00:00:00Z", diff --git a/engine/config_integrations/elastalert.py b/engine/config_integrations/elastalert.py index 90e9bfcc..73320d53 100644 --- a/engine/config_integrations/elastalert.py +++ b/engine/config_integrations/elastalert.py @@ -61,6 +61,4 @@ resolve_condition = """\ acknowledge_condition = None -group_verbose_name = "Incident" - example_payload = {"message": "This alert was sent by user for the demonstration purposes"} diff --git a/engine/config_integrations/formatted_webhook.py b/engine/config_integrations/formatted_webhook.py index 6f712a23..6847639f 100644 --- a/engine/config_integrations/formatted_webhook.py +++ b/engine/config_integrations/formatted_webhook.py @@ -50,8 +50,6 @@ resolve_condition = '{{ payload.get("state", "").upper() == "OK" }}' acknowledge_condition = None -group_verbose_name = web_title - example_payload = { "alert_uid": "08d6891a-835c-e661-39fa-96b6a9e26552", "title": "TestAlert: The whole system is down", diff --git a/engine/config_integrations/grafana.py b/engine/config_integrations/grafana.py index 383390c4..4feefd61 100644 --- a/engine/config_integrations/grafana.py +++ b/engine/config_integrations/grafana.py @@ -143,10 +143,6 @@ resolve_condition = """\ acknowledge_condition = None -group_verbose_name = """\ -{{ payload.get("ruleName", "Incident") }} -""" - tests = { "payload": { "endsAt": "0001-01-01T00:00:00Z", @@ -257,7 +253,6 @@ tests = { "group_distinction": "c6bf5494a2d3052459b4dac837e41455", "is_resolve_signal": False, "is_acknowledge_signal": False, - "group_verbose_name": "Incident", } # Miscellaneous diff --git a/engine/config_integrations/grafana_alerting.py b/engine/config_integrations/grafana_alerting.py index ae07e12e..e8942b1e 100644 --- a/engine/config_integrations/grafana_alerting.py +++ b/engine/config_integrations/grafana_alerting.py @@ -120,8 +120,6 @@ resolve_condition = """\ acknowledge_condition = None -group_verbose_name = "Incident" - tests = { "payload": { "endsAt": "0001-01-01T00:00:00Z", diff --git a/engine/config_integrations/heartbeat.py b/engine/config_integrations/heartbeat.py index f051a44c..e339b56f 100644 --- a/engine/config_integrations/heartbeat.py +++ b/engine/config_integrations/heartbeat.py @@ -26,6 +26,4 @@ resolve_condition = '{{ payload.get("is_resolve", False) == True }}' acknowledge_condition = None -group_verbose_name = '{{ payload.get("title", "Title") }}' - example_payload = {"foo": "bar"} diff --git a/engine/config_integrations/inbound_email.py b/engine/config_integrations/inbound_email.py index b934e35a..4ecac8e4 100644 --- a/engine/config_integrations/inbound_email.py +++ b/engine/config_integrations/inbound_email.py @@ -49,5 +49,3 @@ grouping_id = '{{ payload.get("title", "")}}' resolve_condition = '{{ payload.get("state", "").upper() == "OK" }}' acknowledge_condition = None - -group_verbose_name = web_title diff --git a/engine/config_integrations/kapacitor.py b/engine/config_integrations/kapacitor.py index d5f013fe..3d761766 100644 --- a/engine/config_integrations/kapacitor.py +++ b/engine/config_integrations/kapacitor.py @@ -56,8 +56,6 @@ resolve_condition = '{{ payload.get("level", "").startswith("OK") }}' acknowledge_condition = None -group_verbose_name = '{{ payload.get("id", "") }}' - example_payload = { "id": "TestAlert", "message": "This alert was sent by user for the demonstration purposes", diff --git a/engine/config_integrations/maintenance.py b/engine/config_integrations/maintenance.py index 957e53e9..d27405ef 100644 --- a/engine/config_integrations/maintenance.py +++ b/engine/config_integrations/maintenance.py @@ -49,5 +49,3 @@ grouping_id = None resolve_condition = None acknowledge_condition = None - -group_verbose_name = "Incident" diff --git a/engine/config_integrations/manual.py b/engine/config_integrations/manual.py index 43f4852b..fdcaadaa 100644 --- a/engine/config_integrations/manual.py +++ b/engine/config_integrations/manual.py @@ -58,5 +58,3 @@ grouping_id = """{{ payload }}""" resolve_condition = None acknowledge_condition = None - -group_verbose_name = web_title diff --git a/engine/config_integrations/slack_channel.py b/engine/config_integrations/slack_channel.py index d01c186b..cd8ef14f 100644 --- a/engine/config_integrations/slack_channel.py +++ b/engine/config_integrations/slack_channel.py @@ -39,6 +39,4 @@ resolve_condition = None acknowledge_condition = None -group_verbose_name = '<#{{ payload.get("channel", "") }}>' - source_link = '{{ payload.get("amixr_mixin", {}).get("permalink", "")}}' diff --git a/engine/config_integrations/webhook.py b/engine/config_integrations/webhook.py index 113efc56..4a3b0b73 100644 --- a/engine/config_integrations/webhook.py +++ b/engine/config_integrations/webhook.py @@ -60,6 +60,4 @@ resolve_condition = """\ {%- endif %}""" acknowledge_condition = None -group_verbose_name = web_title - example_payload = {"message": "This alert was sent by user for the demonstration purposes"} diff --git a/engine/settings/prod_without_db.py b/engine/settings/prod_without_db.py index 88261cbb..fe99bed1 100644 --- a/engine/settings/prod_without_db.py +++ b/engine/settings/prod_without_db.py @@ -139,6 +139,8 @@ CELERY_TASK_ROUTES = { "apps.schedules.tasks.drop_cached_ical.drop_cached_ical_for_custom_events_for_organization": {"queue": "critical"}, "apps.schedules.tasks.drop_cached_ical.drop_cached_ical_task": {"queue": "critical"}, # LONG + "apps.alerts.tasks.alert_group_verbose_name.update_verbose_name_for_alert_receive_channel": {"queue": "long"}, + "apps.alerts.tasks.alert_group_verbose_name.update_verbose_name": {"queue": "long"}, "apps.alerts.tasks.check_escalation_finished.check_escalation_finished_task": {"queue": "long"}, "apps.grafana_plugin.tasks.sync.start_sync_organizations": {"queue": "long"}, "apps.grafana_plugin.tasks.sync.sync_organization_async": {"queue": "long"}, diff --git a/grafana-plugin/src/models/alertgroup/alertgroup.types.ts b/grafana-plugin/src/models/alertgroup/alertgroup.types.ts index 8f5e231b..d704c34e 100644 --- a/grafana-plugin/src/models/alertgroup/alertgroup.types.ts +++ b/grafana-plugin/src/models/alertgroup/alertgroup.types.ts @@ -72,7 +72,6 @@ export interface Alert { silenced_until: string; started_at: string; last_alert_at: string; - verbose_name: string; dependent_alert_groups: Alert[]; status: IncidentStatus; short?: boolean; From 920a8632627d70255fe88384ab8522c1698b861f Mon Sep 17 00:00:00 2001 From: Vadim Stepanov Date: Mon, 5 Sep 2022 11:44:22 +0100 Subject: [PATCH 75/77] Revert "Alert group search by title (#466)" (#479) This reverts commit f1f4303825b7a67ed81e54931057d766c7b8324c. --- .../apps/alerts/integration_options_mixin.py | 1 + .../migrations/0007_populate_verbose_name.py | 23 ------ engine/apps/alerts/models/alert.py | 14 ++-- engine/apps/alerts/models/alert_group.py | 2 +- engine/apps/alerts/tasks/__init__.py | 1 - .../alerts/tasks/alert_group_verbose_name.py | 72 ------------------- .../alerts/tests/test_default_templates.py | 1 + engine/apps/api/serializers/alert_group.py | 1 + engine/apps/api/views/alert_group.py | 2 +- .../views/alert_receive_channel_template.py | 10 --- engine/apps/public_api/views/integrations.py | 17 +---- engine/config_integrations/alertmanager.py | 2 + engine/config_integrations/elastalert.py | 2 + .../config_integrations/formatted_webhook.py | 2 + engine/config_integrations/grafana.py | 5 ++ .../config_integrations/grafana_alerting.py | 2 + engine/config_integrations/heartbeat.py | 2 + engine/config_integrations/inbound_email.py | 2 + engine/config_integrations/kapacitor.py | 2 + engine/config_integrations/maintenance.py | 2 + engine/config_integrations/manual.py | 2 + engine/config_integrations/slack_channel.py | 2 + engine/config_integrations/webhook.py | 2 + engine/settings/prod_without_db.py | 2 - .../src/models/alertgroup/alertgroup.types.ts | 1 + 25 files changed, 43 insertions(+), 131 deletions(-) delete mode 100644 engine/apps/alerts/migrations/0007_populate_verbose_name.py delete mode 100644 engine/apps/alerts/tasks/alert_group_verbose_name.py diff --git a/engine/apps/alerts/integration_options_mixin.py b/engine/apps/alerts/integration_options_mixin.py index a0a81bab..b5b00a41 100644 --- a/engine/apps/alerts/integration_options_mixin.py +++ b/engine/apps/alerts/integration_options_mixin.py @@ -69,6 +69,7 @@ class IntegrationOptionsMixin: "grouping_id", "resolve_condition", "acknowledge_condition", + "group_verbose_name", "source_link", ] diff --git a/engine/apps/alerts/migrations/0007_populate_verbose_name.py b/engine/apps/alerts/migrations/0007_populate_verbose_name.py deleted file mode 100644 index 89d6fc44..00000000 --- a/engine/apps/alerts/migrations/0007_populate_verbose_name.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 3.2.15 on 2022-09-01 16:54 - -from django.db import migrations - -from apps.alerts.models import AlertReceiveChannel -from apps.alerts.tasks import update_verbose_name_for_alert_receive_channel - - -def populate_verbose_name(apps, _): - pks = AlertReceiveChannel.objects_with_deleted.values_list("pk", flat=True) - for pk in pks: - update_verbose_name_for_alert_receive_channel.delay(pk) - - -class Migration(migrations.Migration): - - dependencies = [ - ('alerts', '0006_alertgroup_alerts_aler_channel_ee84a7_idx'), - ] - - operations = [ - migrations.RunPython(populate_verbose_name, migrations.RunPython.noop), - ] diff --git a/engine/apps/alerts/models/alert.py b/engine/apps/alerts/models/alert.py index a41d3f12..8f5b272a 100644 --- a/engine/apps/alerts/models/alert.py +++ b/engine/apps/alerts/models/alert.py @@ -179,19 +179,19 @@ class Alert(models.Model): is_resolve_signal = False is_acknowledge_signal = False group_distinction = None + group_verbose_name = "Incident" acknowledge_condition_template = template_manager.get_attr_template( "acknowledge_condition", alert_receive_channel ) resolve_condition_template = template_manager.get_attr_template("resolve_condition", alert_receive_channel) grouping_id_template = template_manager.get_attr_template("grouping_id", alert_receive_channel) - - # set verbose_name to web title to allow alert group searching based on verbose_name - web_title_template = template_manager.get_attr_template("title", alert_receive_channel, render_for="web") - if web_title_template: - group_verbose_name = apply_jinja_template(web_title_template, raw_request_data)[0] or None - else: - group_verbose_name = None + # use get_default_attr_template because there is no ability to customize group_verbose_name, only default value + group_verbose_name_template = template_manager.get_default_attr_template( + "group_verbose_name", alert_receive_channel + ) + if group_verbose_name_template is not None: + group_verbose_name, _ = apply_jinja_template(group_verbose_name_template, raw_request_data) if grouping_id_template is not None: group_distinction, _ = apply_jinja_template(grouping_id_template, raw_request_data) diff --git a/engine/apps/alerts/models/alert_group.py b/engine/apps/alerts/models/alert_group.py index 1f60ce91..84a0a9aa 100644 --- a/engine/apps/alerts/models/alert_group.py +++ b/engine/apps/alerts/models/alert_group.py @@ -899,7 +899,7 @@ class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models. self.resolve(resolved_by=AlertGroup.WIPED) self.stop_escalation() self.distinction = "" - self.verbose_name = None + self.verbose_name = "Wiped incident" self.wiped_at = timezone.now() self.wiped_by = user for alert in self.alerts.all(): diff --git a/engine/apps/alerts/tasks/__init__.py b/engine/apps/alerts/tasks/__init__.py index 48a30b7a..bf1ad097 100644 --- a/engine/apps/alerts/tasks/__init__.py +++ b/engine/apps/alerts/tasks/__init__.py @@ -1,5 +1,4 @@ from .acknowledge_reminder import acknowledge_reminder_task # noqa: F401 -from .alert_group_verbose_name import update_verbose_name, update_verbose_name_for_alert_receive_channel # noqa:F401 from .calculcate_escalation_finish_time import calculate_escalation_finish_time # noqa from .call_ack_url import call_ack_url # noqa: F401 from .check_escalation_finished import check_escalation_finished_task # noqa: F401 diff --git a/engine/apps/alerts/tasks/alert_group_verbose_name.py b/engine/apps/alerts/tasks/alert_group_verbose_name.py deleted file mode 100644 index 202df5bc..00000000 --- a/engine/apps/alerts/tasks/alert_group_verbose_name.py +++ /dev/null @@ -1,72 +0,0 @@ -from django.db.models import Min - -from apps.alerts.incident_appearance.templaters import TemplateLoader -from apps.alerts.tasks.task_logger import task_logger -from common.custom_celery_tasks import shared_dedicated_queue_retry_task -from common.jinja_templater import apply_jinja_template - -# BATCH_SIZE is how many alert groups will be processed per second (for every individual alert receive channel) -BATCH_SIZE = 1000 - - -def batch_ids(queryset, cursor): - return list(queryset.filter(id__gt=cursor).order_by("id").values_list("id", flat=True)[:BATCH_SIZE]) - - -@shared_dedicated_queue_retry_task -def update_verbose_name_for_alert_receive_channel(alert_receive_channel_pk): - from apps.alerts.models import AlertGroup - - countdown = 0 - cursor = 0 - queryset = AlertGroup.all_objects.filter(channel_id=alert_receive_channel_pk) - ids = batch_ids(queryset, cursor) - - while ids: - update_verbose_name.apply_async((alert_receive_channel_pk, ids[0], ids[-1]), countdown=countdown) - - cursor = ids[-1] - ids = batch_ids(queryset, cursor) - countdown += 1 - - -@shared_dedicated_queue_retry_task -def update_verbose_name(alert_receive_channel_pk, alert_group_pk_start, alert_group_pk_end): - from apps.alerts.models import Alert, AlertGroup, AlertReceiveChannel - - try: - alert_receive_channel = AlertReceiveChannel.objects_with_deleted.get(pk=alert_receive_channel_pk) - except AlertReceiveChannel.DoesNotExist: - task_logger.warning(f"AlertReceiveChannel {alert_receive_channel_pk} doesn't exist") - return - - alert_groups = AlertGroup.all_objects.filter(pk__gte=alert_group_pk_start, pk__lte=alert_group_pk_end).only("pk") - - # get first alerts in 2 SQL queries - alerts_info = ( - Alert.objects.values("group_id") - .filter(group_id__gte=alert_group_pk_start, group_id__lte=alert_group_pk_end) - .annotate(first_alert_id=Min("id")) - ) - alerts_info_map = {info["group_id"]: info for info in alerts_info} - - first_alert_ids = [info["first_alert_id"] for info in alerts_info_map.values()] - first_alerts = Alert.objects.filter(pk__in=first_alert_ids).values("group_id", "raw_request_data") - first_alert_map = {alert["group_id"]: alert for alert in first_alerts} - - template_manager = TemplateLoader() - web_title_template = template_manager.get_attr_template("title", alert_receive_channel, render_for="web") - - for alert_group in alert_groups: - if web_title_template: - if alert_group.pk in first_alert_map: - raw_request_data = first_alert_map[alert_group.pk]["raw_request_data"] - verbose_name = apply_jinja_template(web_title_template, raw_request_data)[0] or None - else: - verbose_name = None - else: - verbose_name = None - - alert_group.verbose_name = verbose_name - - AlertGroup.all_objects.bulk_update(alert_groups, ["verbose_name"]) diff --git a/engine/apps/alerts/tests/test_default_templates.py b/engine/apps/alerts/tests/test_default_templates.py index 259aa051..63cfd0b8 100644 --- a/engine/apps/alerts/tests/test_default_templates.py +++ b/engine/apps/alerts/tests/test_default_templates.py @@ -92,6 +92,7 @@ def test_render_group_data_templates( assert group_data.group_distinction == template_module.tests.get("group_distinction") assert group_data.is_resolve_signal == template_module.tests.get("is_resolve_signal") assert group_data.is_acknowledge_signal == template_module.tests.get("is_acknowledge_signal") + assert group_data.group_verbose_name == template_module.tests.get("group_verbose_name") def test_default_templates_are_valid(): diff --git a/engine/apps/api/serializers/alert_group.py b/engine/apps/api/serializers/alert_group.py index f9ecf443..df5583c4 100644 --- a/engine/apps/api/serializers/alert_group.py +++ b/engine/apps/api/serializers/alert_group.py @@ -61,6 +61,7 @@ class AlertGroupListSerializer(EagerLoadingMixin, serializers.ModelSerializer): "pk", "alerts_count", "inside_organization_number", + "verbose_name", "alert_receive_channel", "resolved", "resolved_by", diff --git a/engine/apps/api/views/alert_group.py b/engine/apps/api/views/alert_group.py index 0d44e150..9fd72296 100644 --- a/engine/apps/api/views/alert_group.py +++ b/engine/apps/api/views/alert_group.py @@ -192,7 +192,7 @@ class AlertGroupView( filter_backends = [SearchFilter, filters.DjangoFilterBackend] # todo: add ability to search by templated title - search_fields = ["public_primary_key", "inside_organization_number", "verbose_name"] + search_fields = ["public_primary_key", "inside_organization_number"] filterset_class = AlertGroupFilter diff --git a/engine/apps/api/views/alert_receive_channel_template.py b/engine/apps/api/views/alert_receive_channel_template.py index b1fac13b..ff8cd923 100644 --- a/engine/apps/api/views/alert_receive_channel_template.py +++ b/engine/apps/api/views/alert_receive_channel_template.py @@ -2,7 +2,6 @@ from rest_framework import mixins, viewsets from rest_framework.permissions import IsAuthenticated from apps.alerts.models import AlertReceiveChannel -from apps.alerts.tasks import update_verbose_name_for_alert_receive_channel from apps.api.permissions import MODIFY_ACTIONS, READ_ACTIONS, ActionPermission, AnyRole, IsAdmin from apps.api.serializers.alert_receive_channel import AlertReceiveChannelTemplatesSerializer from apps.auth_token.auth import PluginAuthentication @@ -37,14 +36,9 @@ class AlertReceiveChannelTemplateView( def update(self, request, *args, **kwargs): instance = self.get_object() prev_state = instance.insight_logs_serialized - prev_web_title_template = instance.web_title_template - result = super().update(request, *args, **kwargs) - instance = self.get_object() new_state = instance.insight_logs_serialized - new_web_title_template = instance.web_title_template - write_resource_insight_log( instance=instance, author=self.request.user, @@ -52,8 +46,4 @@ class AlertReceiveChannelTemplateView( prev_state=prev_state, new_state=new_state, ) - - if new_web_title_template != prev_web_title_template: - update_verbose_name_for_alert_receive_channel.delay(instance.pk) - return result diff --git a/engine/apps/public_api/views/integrations.py b/engine/apps/public_api/views/integrations.py index 5c5df6f3..36ef6ea3 100644 --- a/engine/apps/public_api/views/integrations.py +++ b/engine/apps/public_api/views/integrations.py @@ -5,7 +5,6 @@ from rest_framework.permissions import IsAuthenticated from rest_framework.viewsets import ModelViewSet from apps.alerts.models import AlertReceiveChannel -from apps.alerts.tasks import update_verbose_name_for_alert_receive_channel from apps.auth_token.auth import ApiTokenAuthentication from apps.public_api.serializers import IntegrationSerializer, IntegrationUpdateSerializer from apps.public_api.throttlers.user_throttle import UserThrottle @@ -59,27 +58,17 @@ class IntegrationView( raise NotFound def perform_update(self, serializer): - instance = serializer.instance - - prev_state = instance.insight_logs_serialized - prev_web_title_template = instance.web_title_template - + prev_state = serializer.instance.insight_logs_serialized serializer.save() - - new_state = instance.insight_logs_serialized - new_web_title_template = instance.web_title_template - + new_state = serializer.instance.insight_logs_serialized write_resource_insight_log( - instance=instance, + instance=serializer.instance, author=self.request.user, event=EntityEvent.UPDATED, prev_state=prev_state, new_state=new_state, ) - if new_web_title_template != prev_web_title_template: - update_verbose_name_for_alert_receive_channel.delay(instance.pk) - def perform_destroy(self, instance): write_resource_insight_log(instance=instance, author=self.request.user, event=EntityEvent.DELETED) instance.delete() diff --git a/engine/config_integrations/alertmanager.py b/engine/config_integrations/alertmanager.py index bfdcff2e..cb2fa9b6 100644 --- a/engine/config_integrations/alertmanager.py +++ b/engine/config_integrations/alertmanager.py @@ -116,6 +116,8 @@ resolve_condition = """\ acknowledge_condition = None +group_verbose_name = "Incident" + tests = { "payload": { "endsAt": "0001-01-01T00:00:00Z", diff --git a/engine/config_integrations/elastalert.py b/engine/config_integrations/elastalert.py index 73320d53..90e9bfcc 100644 --- a/engine/config_integrations/elastalert.py +++ b/engine/config_integrations/elastalert.py @@ -61,4 +61,6 @@ resolve_condition = """\ acknowledge_condition = None +group_verbose_name = "Incident" + example_payload = {"message": "This alert was sent by user for the demonstration purposes"} diff --git a/engine/config_integrations/formatted_webhook.py b/engine/config_integrations/formatted_webhook.py index 6847639f..6f712a23 100644 --- a/engine/config_integrations/formatted_webhook.py +++ b/engine/config_integrations/formatted_webhook.py @@ -50,6 +50,8 @@ resolve_condition = '{{ payload.get("state", "").upper() == "OK" }}' acknowledge_condition = None +group_verbose_name = web_title + example_payload = { "alert_uid": "08d6891a-835c-e661-39fa-96b6a9e26552", "title": "TestAlert: The whole system is down", diff --git a/engine/config_integrations/grafana.py b/engine/config_integrations/grafana.py index 4feefd61..383390c4 100644 --- a/engine/config_integrations/grafana.py +++ b/engine/config_integrations/grafana.py @@ -143,6 +143,10 @@ resolve_condition = """\ acknowledge_condition = None +group_verbose_name = """\ +{{ payload.get("ruleName", "Incident") }} +""" + tests = { "payload": { "endsAt": "0001-01-01T00:00:00Z", @@ -253,6 +257,7 @@ tests = { "group_distinction": "c6bf5494a2d3052459b4dac837e41455", "is_resolve_signal": False, "is_acknowledge_signal": False, + "group_verbose_name": "Incident", } # Miscellaneous diff --git a/engine/config_integrations/grafana_alerting.py b/engine/config_integrations/grafana_alerting.py index e8942b1e..ae07e12e 100644 --- a/engine/config_integrations/grafana_alerting.py +++ b/engine/config_integrations/grafana_alerting.py @@ -120,6 +120,8 @@ resolve_condition = """\ acknowledge_condition = None +group_verbose_name = "Incident" + tests = { "payload": { "endsAt": "0001-01-01T00:00:00Z", diff --git a/engine/config_integrations/heartbeat.py b/engine/config_integrations/heartbeat.py index e339b56f..f051a44c 100644 --- a/engine/config_integrations/heartbeat.py +++ b/engine/config_integrations/heartbeat.py @@ -26,4 +26,6 @@ resolve_condition = '{{ payload.get("is_resolve", False) == True }}' acknowledge_condition = None +group_verbose_name = '{{ payload.get("title", "Title") }}' + example_payload = {"foo": "bar"} diff --git a/engine/config_integrations/inbound_email.py b/engine/config_integrations/inbound_email.py index 4ecac8e4..b934e35a 100644 --- a/engine/config_integrations/inbound_email.py +++ b/engine/config_integrations/inbound_email.py @@ -49,3 +49,5 @@ grouping_id = '{{ payload.get("title", "")}}' resolve_condition = '{{ payload.get("state", "").upper() == "OK" }}' acknowledge_condition = None + +group_verbose_name = web_title diff --git a/engine/config_integrations/kapacitor.py b/engine/config_integrations/kapacitor.py index 3d761766..d5f013fe 100644 --- a/engine/config_integrations/kapacitor.py +++ b/engine/config_integrations/kapacitor.py @@ -56,6 +56,8 @@ resolve_condition = '{{ payload.get("level", "").startswith("OK") }}' acknowledge_condition = None +group_verbose_name = '{{ payload.get("id", "") }}' + example_payload = { "id": "TestAlert", "message": "This alert was sent by user for the demonstration purposes", diff --git a/engine/config_integrations/maintenance.py b/engine/config_integrations/maintenance.py index d27405ef..957e53e9 100644 --- a/engine/config_integrations/maintenance.py +++ b/engine/config_integrations/maintenance.py @@ -49,3 +49,5 @@ grouping_id = None resolve_condition = None acknowledge_condition = None + +group_verbose_name = "Incident" diff --git a/engine/config_integrations/manual.py b/engine/config_integrations/manual.py index fdcaadaa..43f4852b 100644 --- a/engine/config_integrations/manual.py +++ b/engine/config_integrations/manual.py @@ -58,3 +58,5 @@ grouping_id = """{{ payload }}""" resolve_condition = None acknowledge_condition = None + +group_verbose_name = web_title diff --git a/engine/config_integrations/slack_channel.py b/engine/config_integrations/slack_channel.py index cd8ef14f..d01c186b 100644 --- a/engine/config_integrations/slack_channel.py +++ b/engine/config_integrations/slack_channel.py @@ -39,4 +39,6 @@ resolve_condition = None acknowledge_condition = None +group_verbose_name = '<#{{ payload.get("channel", "") }}>' + source_link = '{{ payload.get("amixr_mixin", {}).get("permalink", "")}}' diff --git a/engine/config_integrations/webhook.py b/engine/config_integrations/webhook.py index 4a3b0b73..113efc56 100644 --- a/engine/config_integrations/webhook.py +++ b/engine/config_integrations/webhook.py @@ -60,4 +60,6 @@ resolve_condition = """\ {%- endif %}""" acknowledge_condition = None +group_verbose_name = web_title + example_payload = {"message": "This alert was sent by user for the demonstration purposes"} diff --git a/engine/settings/prod_without_db.py b/engine/settings/prod_without_db.py index fe99bed1..88261cbb 100644 --- a/engine/settings/prod_without_db.py +++ b/engine/settings/prod_without_db.py @@ -139,8 +139,6 @@ CELERY_TASK_ROUTES = { "apps.schedules.tasks.drop_cached_ical.drop_cached_ical_for_custom_events_for_organization": {"queue": "critical"}, "apps.schedules.tasks.drop_cached_ical.drop_cached_ical_task": {"queue": "critical"}, # LONG - "apps.alerts.tasks.alert_group_verbose_name.update_verbose_name_for_alert_receive_channel": {"queue": "long"}, - "apps.alerts.tasks.alert_group_verbose_name.update_verbose_name": {"queue": "long"}, "apps.alerts.tasks.check_escalation_finished.check_escalation_finished_task": {"queue": "long"}, "apps.grafana_plugin.tasks.sync.start_sync_organizations": {"queue": "long"}, "apps.grafana_plugin.tasks.sync.sync_organization_async": {"queue": "long"}, diff --git a/grafana-plugin/src/models/alertgroup/alertgroup.types.ts b/grafana-plugin/src/models/alertgroup/alertgroup.types.ts index d704c34e..8f5e231b 100644 --- a/grafana-plugin/src/models/alertgroup/alertgroup.types.ts +++ b/grafana-plugin/src/models/alertgroup/alertgroup.types.ts @@ -72,6 +72,7 @@ export interface Alert { silenced_until: string; started_at: string; last_alert_at: string; + verbose_name: string; dependent_alert_groups: Alert[]; status: IncidentStatus; short?: boolean; From 8f43cf238aed0e2912201014364b4a65b6faeb08 Mon Sep 17 00:00:00 2001 From: Vadim Stepanov Date: Mon, 5 Sep 2022 12:29:55 +0100 Subject: [PATCH 76/77] Bump node version to 14.17.0 on CI pipelines (#481) --- .drone.yml | 8 ++++---- .github/workflows/ci.yml | 2 +- .github/workflows/synk.yml | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.drone.yml b/.drone.yml index 4eaf7cc4..5e0ef037 100644 --- a/.drone.yml +++ b/.drone.yml @@ -5,7 +5,7 @@ name: Build and Release steps: - name: Build Plugin - image: node:14.6.0-stretch + image: node:14.17.0-stretch commands: - apt-get update - apt-get --assume-yes install jq @@ -16,7 +16,7 @@ steps: - ls ./ - name: Sign and Package Plugin - image: node:14.6.0-stretch + image: node:14.17.0-stretch environment: GRAFANA_API_KEY: from_secret: gcom_plugin_publisher_api_key @@ -162,7 +162,7 @@ name: OSS plugin release steps: - name: build plugin - image: node:14.6.0-stretch + image: node:14.17.0-stretch commands: - apt-get update - apt-get --assume-yes install jq @@ -173,7 +173,7 @@ steps: - ls ./ - name: sign and package plugin - image: node:14.6.0-stretch + image: node:14.17.0-stretch environment: GRAFANA_API_KEY: from_secret: gcom_plugin_publisher_api_key diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c27055c3..9d122096 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,7 +15,7 @@ jobs: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: - node-version: 14 + node-version: 14.17.0 - name: Build run: | pip install $(grep "pre-commit" engine/requirements.txt) diff --git a/.github/workflows/synk.yml b/.github/workflows/synk.yml index e4af1c72..54a7f9d7 100644 --- a/.github/workflows/synk.yml +++ b/.github/workflows/synk.yml @@ -16,7 +16,7 @@ jobs: python-version: '3.9' - uses: actions/setup-node@v3 with: - node-version: 14 + node-version: 14.17.0 - uses: snyk/actions/setup@master - name: Install Dependencies run: | From 290c425b3d9d5578a4612566ed03e46a9adbfe3e Mon Sep 17 00:00:00 2001 From: Vadim Stepanov Date: Mon, 5 Sep 2022 12:50:58 +0100 Subject: [PATCH 77/77] drone sign --- .drone.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.drone.yml b/.drone.yml index 5e0ef037..f7e1288f 100644 --- a/.drone.yml +++ b/.drone.yml @@ -395,6 +395,6 @@ kind: secret name: drone_token --- kind: signature -hmac: 3b2548550485a99d3c4123808f2ae83e51bcc1261a7a5ab8f480e8975acdc870 +hmac: 8a060649c132677ba1b5693b5ac6c846c02f9a5bb645fe990b26a7ea42a0fb66 ...