Merge pull request #2542 from grafana/dev

Merge dev to main
This commit is contained in:
Michael Derynck 2023-07-14 17:02:46 -06:00 committed by GitHub
commit c60074c07b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
57 changed files with 346 additions and 1463 deletions

View file

@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased
## v1.3.12 (2023-07-14)
### Added
- Add `page_size`, `current_page_number`, and `total_pages` attributes to paginated API responses by @joeyorlando ([#2471](https://github.com/grafana/oncall/pull/2471))
### Fixed
- New webhooks incorrectly masking authorization header by @mderynck ([#2541](https://github.com/grafana/oncall/pull/2541))
## v1.3.11 (2023-07-13)
### Added

View file

@ -36,7 +36,10 @@ The above command returns JSON structured in the following way:
"telegram": "https://t.me/c/5354/1234?thread=1234"
}
}
]
],
"current_page_number": 1,
"page_size": 50,
"total_pages": 1
}
```

View file

@ -96,7 +96,10 @@ The above command returns JSON structured in the following way:
]
}
}
]
],
"current_page_number": 1,
"page_size": 50,
"total_pages": 1
}
```

View file

@ -80,7 +80,10 @@ The above command returns JSON structured in the following way:
"name": "default",
"team_id": null
}
]
],
"current_page_number": 1,
"page_size": 50,
"total_pages": 1
}
```

View file

@ -105,7 +105,10 @@ The above command returns JSON structured in the following way:
"type": "notify_person_next_each_time",
"persons_to_notify_next_each_time": ["U4DNY931HHJS5"]
}
]
],
"current_page_number": 1,
"page_size": 50,
"total_pages": 1
}
```

View file

@ -33,7 +33,7 @@ The above command returns JSON structured in the following way:
"channel_id": "CH23212D"
}
},
"templates": {
"templates": {
"grouping_key": null,
"resolve_signal": null,
"acknowledge_signal": null,
@ -219,7 +219,10 @@ The above command returns JSON structured in the following way:
}
}
}
]
],
"current_page_number": 1,
"page_size": 50,
"total_pages": 1
}
```

View file

@ -141,7 +141,10 @@ The above command returns JSON structured in the following way:
"by_monthday": null,
"users": ["U4DNY931HHJS5"]
}
]
],
"current_page_number": 1,
"page_size": 50,
"total_pages": 1
}
```

View file

@ -29,7 +29,10 @@ The above command returns JSON structured in the following way:
"id": "KGEFG74LU1D8L",
"name": "Publish alert group notification to JIRA"
}
]
],
"current_page_number": 1,
"page_size": 50,
"total_pages": 1
}
```

View file

@ -113,7 +113,10 @@ The above command returns JSON structured in the following ways:
"important": true,
"type": "notify_by_phone_call"
}
]
],
"current_page_number": 1,
"page_size": 50,
"total_pages": 1
}
```

View file

@ -30,10 +30,10 @@ The above command returns JSON structured in the following way:
}
```
| Parameter | Required | Description |
| --------------- | :------: | :--------------------- |
| `alert_group_id`| Yes | Alert group ID | |
| `text` | Yes | Resolution note text |
| Parameter | Required | Description |
| ---------------- | :------: | :------------------- | --- |
| `alert_group_id` | Yes | Alert group ID | |
| `text` | Yes | Resolution note text |
**HTTP request**
@ -90,7 +90,10 @@ The above command returns JSON structured in the following way:
"created_at": "2020-06-19T12:40:01.429805Z",
"text": "Demo resolution note"
}
]
],
"current_page_number": 1,
"page_size": 50,
"total_pages": 1
}
```

View file

@ -125,7 +125,10 @@ The above command returns JSON structured in the following way:
"channel_id": "CH23212D"
}
}
]
],
"current_page_number": 1,
"page_size": 25,
"total_pages": 1
}
```

View file

@ -129,7 +129,10 @@ The above command returns JSON structured in the following way:
"user_group_id": "MEOW_SLACK_ID"
}
}
]
],
"current_page_number": 1,
"page_size": 50,
"total_pages": 1
}
```
@ -294,7 +297,10 @@ The above command returns JSON structured in the following way:
"shift_start": "2023-01-27T09:00:00Z",
"shift_end": "2023-01-27T17:00:00Z"
}
]
],
"current_page_number": 1,
"page_size": 50,
"total_pages": 1
}
```

View file

@ -25,7 +25,10 @@ The above command returns JSON structured in the following way:
"name": "meow_channel",
"slack_id": "MEOW_SLACK_ID"
}
]
],
"current_page_number": 1,
"page_size": 50,
"total_pages": 1
}
```

View file

@ -32,7 +32,10 @@ The above command returns JSON structured in the following way:
"handle": "meow_group"
}
}
]
],
"current_page_number": 1,
"page_size": 50,
"total_pages": 1
}
```

View file

@ -76,7 +76,10 @@ The above command returns JSON structured in the following way:
"username": "alex",
"role": "admin"
}
]
],
"current_page_number": 1,
"page_size": 100,
"total_pages": 1
}
```

View file

@ -527,8 +527,8 @@ class AlertReceiveChannel(IntegrationOptionsMixin, MaintainableObject):
return getattr(heartbeat, self.INTEGRATIONS_TO_REVERSE_URL_MAP[self.integration], None)
# Demo alerts
def send_demo_alert(self, force_route_id=None, payload=None):
logger.info(f"send_demo_alert integration={self.pk} force_route_id={force_route_id}")
def send_demo_alert(self, payload=None):
logger.info(f"send_demo_alert integration={self.pk}")
if not self.is_demo_alert_enabled:
raise UnableToSendDemoAlert("Unable to send demo alert for this integration.")
@ -543,9 +543,7 @@ class AlertReceiveChannel(IntegrationOptionsMixin, MaintainableObject):
"Unable to send demo alert as payload has no 'alerts' key, it is not array, or it is empty."
)
for alert in alerts:
create_alertmanager_alerts.delay(
alert_receive_channel_pk=self.pk, alert=alert, is_demo=True, force_route_id=force_route_id
)
create_alertmanager_alerts.delay(alert_receive_channel_pk=self.pk, alert=alert, is_demo=True)
else:
create_alert.delay(
title="Demo alert",
@ -556,7 +554,6 @@ class AlertReceiveChannel(IntegrationOptionsMixin, MaintainableObject):
integration_unique_data=None,
raw_request_data=payload,
is_demo=True,
force_route_id=force_route_id,
)
@property

View file

@ -165,11 +165,6 @@ class ChannelFilter(OrderedModel):
return str(self.filtering_term).replace("`", "")
raise Exception("Unknown filtering term")
def send_demo_alert(self):
"""Deprecated. May be used in the older versions of the plugin"""
integration = self.alert_receive_channel
integration.send_demo_alert(force_route_id=self.pk)
# Insight logs
@property
def insight_logs_type_verbal(self):

View file

@ -1,2 +0,0 @@
from .terraform_file_renderer import TerraformFileRenderer # noqa: F401
from .terraform_state_renderer import TerraformStateRenderer # noqa: F401

View file

@ -1,781 +0,0 @@
from django.apps import apps
from django.db.models import OuterRef, Subquery
from django.utils.text import slugify
from apps.alerts.models import EscalationPolicy
from apps.schedules.models import OnCallScheduleCalendar, OnCallScheduleICal
class TerraformFileRenderer:
AMIXR_USER_DATA_TEMPLATE = '\ndata "amixr_user" "{}" {{\n username = "{}"\n}}\n'
TEAM_DATA_TEMPLATE = '\ndata "amixr_team" "{}" {{\n name = "{}"\n}}\n'
CUSTOM_ACTION_DATA_TEMPLATE = (
'\ndata "amixr_action" "{}" {{\n name = "{}"\n integration_id = amixr_integration.{}.id\n}}\n'
)
SCHEDULE_DATA_TEMPLATE = '\ndata "amixr_schedule" "{}" {{\n name = "{}"\n}}\n'
USER_GROUP_DATA_TEMPLATE = '\ndata "amixr_user_group" "{}" {{\n slack_handle = "{}"\n}}\n'
SLACK_CHANNEL_DATA_TEMPLATE = '\ndata "amixr_slack_channel" "{}" {{\n name = "{}"\n}}\n'
INTEGRATION_RESOURCE_TEMPLATE = (
'\nresource "amixr_integration" "{}" {{\n name = "{}"\n type = "{}"\n team_id = {}\n}}\n'
)
INTEGRATION_RESOURCE_TEMPLATE_WITH_TEMPLATES = (
'\nresource "amixr_integration" "{}" {{\n'
' name = "{}"\n'
' type = "{}"\n'
" team_id = {}\n"
" templates {}\n"
"}}\n"
)
ROUTE_RESOURCES_TEMPLATE = (
'\nresource "amixr_route" "{}" {{\n'
" integration_id = amixr_integration.{}.id\n"
" escalation_chain_id = {}.id\n"
' routing_regex = "{}"\n'
" position = {}\n"
"}}\n"
)
ROUTE_RESOURCES_TEMPLATE_WITH_SLACK = (
'\nresource "amixr_route" "{}" {{\n'
" integration_id = amixr_integration.{}.id\n"
" escalation_chain_id = {}.id\n"
' routing_regex = "{}"\n'
" position = {}\n"
" slack {{\n"
" channel_id = {}\n"
" }}\n"
"}}\n"
)
ESCALATION_CHAIN_DATA_TEMPLATE = '\ndata "amixr_escalation_chain" "{}" {{\n name = "{}"\n}}\n'
ESCALATION_CHAIN_RESOURCE_TEMPLATE = (
'\nresource "amixr_escalation_chain" "{}" {{\n name = "{}"\n team_id = {}\n}}\n'
)
ESCALATION_POLICY_TEMPLATES = {
EscalationPolicy.PUBLIC_STEP_CHOICES_MAP[EscalationPolicy.STEP_WAIT]: '\nresource "amixr_escalation" "{}" {{\n'
" escalation_chain_id = {}\n"
' type = "{}"\n'
" duration = {}\n"
" position = {}\n"
"}}\n",
EscalationPolicy.PUBLIC_STEP_CHOICES_MAP[
EscalationPolicy.STEP_NOTIFY_MULTIPLE_USERS
]: '\nresource "amixr_escalation" "{}" {{\n'
" escalation_chain_id = {}\n"
' type = "{}"\n'
" important = {}\n"
" persons_to_notify = {}\n"
" position = {}\n"
"}}\n",
EscalationPolicy.PUBLIC_STEP_CHOICES_MAP[
EscalationPolicy.STEP_NOTIFY_USERS_QUEUE
]: '\nresource "amixr_escalation" "{}" {{\n'
" escalation_chain_id = {}\n"
' type = "{}"\n'
" persons_to_notify_next_each_time = {}\n"
" position = {}\n"
"}}\n",
EscalationPolicy.PUBLIC_STEP_CHOICES_MAP[
EscalationPolicy.STEP_NOTIFY_SCHEDULE
]: '\nresource "amixr_escalation" "{}" {{\n'
" escalation_chain_id = {}\n"
' type = "{}"\n'
" important = {}\n"
" notify_on_call_from_schedule = {}\n"
" position = {}\n"
"}}\n",
EscalationPolicy.PUBLIC_STEP_CHOICES_MAP[
EscalationPolicy.STEP_NOTIFY_GROUP
]: '\nresource "amixr_escalation" "{}" {{\n'
" escalation_chain_id = {}\n"
' type = "{}"\n'
" important = {}\n"
" group_to_notify = {}\n"
" position = {}\n"
"}}\n",
EscalationPolicy.PUBLIC_STEP_CHOICES_MAP[
EscalationPolicy.STEP_TRIGGER_CUSTOM_BUTTON
]: '\nresource "amixr_escalation" "{}" {{\n'
" escalation_chain_id = {}\n"
' type = "{}"\n'
" action_to_trigger = {}\n"
" position = {}\n"
"}}\n",
EscalationPolicy.PUBLIC_STEP_CHOICES_MAP[
EscalationPolicy.STEP_NOTIFY_IF_TIME
]: '\nresource "amixr_escalation" "{}" {{\n'
" escalation_chain_id = {}\n"
' type = "{}"\n'
" notify_if_time_from = {}\n"
" notify_if_time_to = {}\n"
" position = {}\n"
"}}\n",
EscalationPolicy.PUBLIC_STEP_CHOICES_MAP[
EscalationPolicy.STEP_NOTIFY_IF_NUM_ALERTS_IN_TIME_WINDOW
]: '\nresource "amixr_escalation" "{}" {{\n'
" escalation_chain_id = {}\n"
' type = "{}"\n'
" num_alerts_in_window = {}\n"
" num_minutes_in_window = {}\n"
" position = {}\n"
"}}\n",
"step_is_none": '\nresource "amixr_escalation" "{}" {{\n'
" escalation_chain_id = {}\n"
" type = null\n"
" position = {}\n"
"}}\n",
"other_steps": '\nresource "amixr_escalation" "{}" {{\n'
" escalation_chain_id = {}\n"
' type = "{}"\n'
" position = {}\n"
"}}\n",
}
AMIXR_USERS_LIST_TEMPLATE = "[\n {}\n ]"
AMIXR_USERS_LIST_TEMPLATE_EMPTY = "[]"
ROLLING_USERS_TEMPLATE = " [{}],\n"
ROLLING_USERS_LIST_TEMPLATE = "[\n{} ]"
SCHEDULE_RESOURCE_TEMPLATE_ICAL = (
'\nresource "amixr_schedule" "{}" {{\n'
' name = "{}"\n'
' type = "ical"\n'
" team_id = {}\n"
" ical_url_primary = {}\n"
" ical_url_overrides = {}\n"
"{}"
"}}\n"
)
SCHEDULE_RESOURCE_TEMPLATE_CALENDAR = (
'\nresource "amixr_schedule" "{}" {{\n'
' name = "{}"\n'
' type = "calendar"\n'
" team_id = {}\n"
' time_zone = "{}"\n'
"{}"
"}}\n"
)
SCHEDULE_RESOURCE_SLACK_TEMPLATE = " slack {{\n channel_id = {}\n }}\n"
ON_CALL_SHIFT_RESOURCE_TEMPLATE_RECURRENT_EVENT = (
'\nresource "amixr_on_call_shift" "{}" {{\n'
' name = "{}"\n'
' type = "{}"\n'
" team_id = {}\n"
' start = "{}"\n'
" duration = {}\n"
" level = {}\n"
' frequency = "{}"\n'
" interval = {}\n"
' week_start = "{}"\n'
" by_day = {}\n"
" by_month = {}\n"
" by_monthday = {}\n"
" users = {}\n"
"}}\n"
)
ON_CALL_SHIFT_RESOURCE_TEMPLATE_ROLLING_USERS = (
'\nresource "amixr_on_call_shift" "{}" {{\n'
' name = "{}"\n'
' type = "{}"\n'
" team_id = {}\n"
' start = "{}"\n'
" duration = {}\n"
" level = {}\n"
' frequency = "{}"\n'
" interval = {}\n"
' week_start = "{}"\n'
" by_day = {}\n"
" by_month = {}\n"
" by_monthday = {}\n"
" rolling_users = {}\n"
"}}\n"
)
ON_CALL_SHIFT_RESOURCE_TEMPLATE_SINGLE_EVENT = (
'\nresource "amixr_on_call_shift" "{}" {{\n'
' name = "{}"\n'
' type = "{}"\n'
" team_id = {}\n"
' start = "{}"\n'
" duration = {}\n"
" level = {}\n"
" users = {}\n"
"}}\n"
)
def __init__(self, organization):
self.organization = organization
self.data = {}
self.used_names = {}
def render_terraform_file(self):
result = self.render_resource_text()
data_result = self.render_data_text()
result = data_result + result
if len(result) == 0:
result += "There is nothing here yet. Check Settings to add integration and come back!"
return result
def render_resource_text(self):
result = ""
result += self.render_escalation_chains_related_resources_text()
integrations_related_resources_text = self.render_integrations_related_resources_text()
result += integrations_related_resources_text
shifts_related_resources_text = self.render_on_call_shift_resource_text()
result += shifts_related_resources_text
schedules_related_resources_text = self.render_schedules_related_resources_text()
result += schedules_related_resources_text
return result
def render_escalation_chains_related_resources_text(self):
result = ""
escalation_chains = self.organization.escalation_chains.all()
for escalation_chain in escalation_chains:
resource_name = self.escape_string_for_terraform(escalation_chain.name)
team_name = self.render_team_name(escalation_chain.team)
team_name_text = f"data.amixr_team.{team_name}.id" if team_name else "null"
result += self.ESCALATION_CHAIN_RESOURCE_TEMPLATE.format(
resource_name, escalation_chain.name, team_name_text
)
result += self.render_escalation_policy_resource_text(escalation_chain, resource_name)
return result
def render_escalation_policy_resource_text(self, escalation_chain, escalation_chain_resource_name):
result = ""
escalation_policies = escalation_chain.escalation_policies.all()
escalation_chain_id = f"amixr_escalation_chain.{escalation_chain_resource_name}.id"
for num, escalation_policy in enumerate(escalation_policies, start=1):
escalation_name = f"escalation-{num}-{escalation_chain_resource_name}"
step_type = None
if escalation_policy.step is None:
result += TerraformFileRenderer.ESCALATION_POLICY_TEMPLATES["step_is_none"].format(
escalation_name,
escalation_chain_id,
escalation_policy.order,
)
else:
step_type = EscalationPolicy.PUBLIC_STEP_CHOICES_MAP[escalation_policy.step]
if escalation_policy.step == EscalationPolicy.STEP_WAIT:
wait_delay = escalation_policy.wait_delay
delay = int(wait_delay.total_seconds()) if wait_delay is not None else "null"
result += TerraformFileRenderer.ESCALATION_POLICY_TEMPLATES[step_type].format(
escalation_name,
escalation_chain_id,
step_type,
delay,
escalation_policy.order,
)
elif escalation_policy.step in [
EscalationPolicy.STEP_NOTIFY_MULTIPLE_USERS,
EscalationPolicy.STEP_NOTIFY_MULTIPLE_USERS_IMPORTANT,
EscalationPolicy.STEP_NOTIFY_USERS_QUEUE,
]:
persons_to_notify = escalation_policy.sorted_users_queue
important = escalation_policy.step == EscalationPolicy.STEP_NOTIFY_MULTIPLE_USERS_IMPORTANT
important = "true" if important else "false"
rendered_persons_to_notify = self.render_amixr_users_list_text(persons_to_notify)
if escalation_policy.step == EscalationPolicy.STEP_NOTIFY_USERS_QUEUE:
result += TerraformFileRenderer.ESCALATION_POLICY_TEMPLATES[step_type].format(
escalation_name,
escalation_chain_id,
step_type,
rendered_persons_to_notify,
escalation_policy.order,
)
else:
result += TerraformFileRenderer.ESCALATION_POLICY_TEMPLATES[step_type].format(
escalation_name,
escalation_chain_id,
step_type,
important,
rendered_persons_to_notify,
escalation_policy.order,
)
elif escalation_policy.step in [
EscalationPolicy.STEP_NOTIFY_SCHEDULE,
EscalationPolicy.STEP_NOTIFY_SCHEDULE_IMPORTANT,
]:
schedule = escalation_policy.notify_schedule
schedule_name = self.render_name(schedule, "schedules", "name")
schedule_text = "null"
if schedule is not None:
schedule_text = f"amixr_schedule.{schedule_name}.id"
important = escalation_policy.step == EscalationPolicy.STEP_NOTIFY_SCHEDULE_IMPORTANT
important = "true" if important else "false"
result += TerraformFileRenderer.ESCALATION_POLICY_TEMPLATES[step_type].format(
escalation_name,
escalation_chain_id,
step_type,
important,
schedule_text,
escalation_policy.order,
)
elif escalation_policy.step in [
EscalationPolicy.STEP_NOTIFY_GROUP,
EscalationPolicy.STEP_NOTIFY_GROUP_IMPORTANT,
]:
user_group = escalation_policy.notify_to_group
user_group_name = self.render_user_group_name(user_group)
important = escalation_policy.step == EscalationPolicy.STEP_NOTIFY_GROUP_IMPORTANT
important = "true" if important else "false"
user_group_text = f"data.amixr_user_group.{user_group_name}.id" if user_group else "null"
result += TerraformFileRenderer.ESCALATION_POLICY_TEMPLATES[step_type].format(
escalation_name,
escalation_chain_id,
step_type,
important,
user_group_text,
escalation_policy.order,
)
# TODO: uncomment after custom actions refactoring
# elif escalation_policy.step == EscalationPolicy.STEP_TRIGGER_CUSTOM_BUTTON:
# custom_action = escalation_policy.custom_button_trigger
# custom_action_name = self.render_name(custom_action, "custom_actions", "name")
# self.render_custom_action_data_source(custom_action, custom_action_name, integration_resource_name)
# custom_action_text = f"data.amixr_action.{custom_action_name}.id" if custom_action else "null"
# result += TerraformFileRenderer.ESCALATION_POLICY_TEMPLATES[step_type].format(
# escalation_name, escalation_chain_id, step_type, custom_action_text, escalation_policy.order,
# )
elif escalation_policy.step == EscalationPolicy.STEP_NOTIFY_IF_TIME:
from_time = self.render_time_string(escalation_policy.from_time)
to_time = self.render_time_string(escalation_policy.to_time)
result += TerraformFileRenderer.ESCALATION_POLICY_TEMPLATES[step_type].format(
escalation_name,
escalation_chain_id,
step_type,
from_time,
to_time,
escalation_policy.order,
)
elif escalation_policy.step == EscalationPolicy.STEP_NOTIFY_IF_NUM_ALERTS_IN_TIME_WINDOW:
result += TerraformFileRenderer.ESCALATION_POLICY_TEMPLATES[step_type].format(
escalation_name,
escalation_chain_id,
step_type,
escalation_policy.num_alerts_in_window,
escalation_policy.num_minutes_in_window,
escalation_policy.order,
)
elif escalation_policy.step is not None:
result += TerraformFileRenderer.ESCALATION_POLICY_TEMPLATES["other_steps"].format(
escalation_name,
escalation_chain_id,
step_type,
escalation_policy.order,
)
return result
def render_integrations_related_resources_text(self):
AlertReceiveChannel = apps.get_model("alerts", "AlertReceiveChannel")
result = ""
integrations = self.organization.alert_receive_channels.all().order_by("created_at")
for integration in integrations:
integration_resource_name = self.render_name(integration, "integrations", "verbal_name")
team_name = self.render_team_name(integration.team)
team_name_text = f"data.amixr_team.{team_name}.id" if team_name else "null"
formatted_integration_name = self.escape_string_for_terraform(integration.verbal_name)
templates = self.render_integration_template(integration)
if templates is not None:
result += TerraformFileRenderer.INTEGRATION_RESOURCE_TEMPLATE_WITH_TEMPLATES.format(
integration_resource_name,
formatted_integration_name,
AlertReceiveChannel.INTEGRATIONS_TO_REVERSE_URL_MAP[integration.integration],
team_name_text,
templates,
)
else:
result += TerraformFileRenderer.INTEGRATION_RESOURCE_TEMPLATE.format(
integration_resource_name,
formatted_integration_name,
AlertReceiveChannel.INTEGRATIONS_TO_REVERSE_URL_MAP[integration.integration],
team_name_text,
)
route_text = self.render_route_resource_text(integration, integration_resource_name)
# render data sources for custom actions just after integration resource
actions_data_text = self.render_action_data_text()
result += actions_data_text
result += route_text
return result
def render_route_resource_text(self, integration, integration_resource_name):
SlackChannel = apps.get_model("slack", "SlackChannel")
slack_team_identity = self.organization.slack_team_identity
slack_channels_q = SlackChannel.objects.filter(
slack_id=OuterRef("slack_channel_id"),
slack_team_identity=slack_team_identity,
)
routes = integration.channel_filters.all().annotate(
slack_channel_name=Subquery(slack_channels_q.values("name")[:1])
)
result = ""
for num, route in enumerate(routes, start=1):
if route.is_default:
continue
route_name = f"route-{num}-{integration_resource_name}"
escalation_chain_resource_name = "amixr_escalation_chain." + self.escape_string_for_terraform(
route.escalation_chain.name
)
routing_regex = self.escape_string_for_terraform(route.filtering_term)
if route.slack_channel_id is not None:
if route.slack_channel_name is not None:
slack_channel_data_name = f"slack-channel-{route.slack_channel_name}"
slack_channel_id = f"data.amixr_slack_channel.{slack_channel_data_name}.slack_id"
if route.slack_channel_name not in self.data.setdefault("slack_channels", {}):
data_result = TerraformFileRenderer.SLACK_CHANNEL_DATA_TEMPLATE.format(
slack_channel_data_name,
route.slack_channel_name,
)
self.data["slack_channels"][route.slack_channel_name] = data_result
else:
slack_channel_id = f'"{route.slack_channel_id}"'
result += TerraformFileRenderer.ROUTE_RESOURCES_TEMPLATE_WITH_SLACK.format(
route_name,
integration_resource_name,
escalation_chain_resource_name,
routing_regex,
route.order,
slack_channel_id,
)
else:
result += TerraformFileRenderer.ROUTE_RESOURCES_TEMPLATE.format(
route_name,
integration_resource_name,
escalation_chain_resource_name,
routing_regex,
route.order,
)
return result
def render_schedules_related_resources_text(self):
SlackChannel = apps.get_model("slack", "SlackChannel")
slack_team_identity = self.organization.slack_team_identity
slack_channels_q = SlackChannel.objects.filter(
slack_id=OuterRef("channel"),
slack_team_identity=slack_team_identity,
)
schedules = self.organization.oncall_schedules.annotate(
slack_channel_name=Subquery(slack_channels_q.values("name")[:1])
).order_by("pk")
result = ""
for schedule in schedules:
schedule_name = self.render_name(schedule, "schedules", "name")
formatted_schedule_name = self.escape_string_for_terraform(schedule.name)
team_name = self.render_team_name(schedule.team)
team_name_text = f"data.amixr_team.{team_name}.id" if team_name else "null"
slack_channel_text = ""
if schedule.channel is not None:
if schedule.slack_channel_name is not None:
slack_channel_data_name = f"slack-channel-{schedule.slack_channel_name}"
slack_channel_id = f"data.amixr_slack_channel.{slack_channel_data_name}.slack_id"
if schedule.slack_channel_name not in self.data.setdefault("slack_channels", {}):
data_result = TerraformFileRenderer.SLACK_CHANNEL_DATA_TEMPLATE.format(
slack_channel_data_name,
schedule.slack_channel_name,
)
self.data["slack_channels"][schedule.slack_channel_name] = data_result
else:
slack_channel_id = f'"{schedule.channel}"'
slack_channel_text = TerraformFileRenderer.SCHEDULE_RESOURCE_SLACK_TEMPLATE.format(slack_channel_id)
if isinstance(schedule, OnCallScheduleICal):
ical_url_primary = f'"{schedule.ical_url_primary}"' if schedule.ical_url_primary else "null"
ical_url_overrides = f'"{schedule.ical_url_overrides}"' if schedule.ical_url_overrides else "null"
result += TerraformFileRenderer.SCHEDULE_RESOURCE_TEMPLATE_ICAL.format(
schedule_name,
formatted_schedule_name,
team_name_text,
ical_url_primary,
ical_url_overrides,
slack_channel_text,
)
elif isinstance(schedule, OnCallScheduleCalendar):
result += TerraformFileRenderer.SCHEDULE_RESOURCE_TEMPLATE_CALENDAR.format(
schedule_name, formatted_schedule_name, team_name_text, schedule.time_zone, slack_channel_text
)
return result
def render_on_call_shift_resource_text(self):
CustomOnCallShift = apps.get_model("schedules", "CustomOnCallShift")
result = ""
on_call_shifts = self.organization.custom_on_call_shifts.all().order_by("pk")
for shift in on_call_shifts:
shift_name = self.render_name(shift, "on_call_shifts", "name")
team_name = self.render_team_name(shift.team)
team_name_text = f"data.amixr_team.{team_name}.id" if team_name else "null"
formatted_integration_name = self.escape_string_for_terraform(shift.name)
shift_type = CustomOnCallShift.PUBLIC_TYPE_CHOICES_MAP[shift.type]
start = shift.start.strftime("%Y-%m-%dT%H:%M:%S")
duration = int(shift.duration.total_seconds())
level = shift.priority_level
frequency = CustomOnCallShift.PUBLIC_FREQUENCY_CHOICES_MAP.get(shift.frequency)
interval = shift.interval or "null"
week_start = CustomOnCallShift.ICAL_WEEKDAY_MAP[shift.week_start]
by_day = self.replace_quotes(f"{shift.by_day}") if shift.by_day else "null"
by_month = self.replace_quotes(f"{shift.by_day}") if shift.by_month else "null"
by_monthday = self.replace_quotes(f"{shift.by_day}") if shift.by_monthday else "null"
if shift.type == CustomOnCallShift.TYPE_ROLLING_USERS_EVENT:
rolling_amixr_users = shift.get_rolling_users()
rendered_amixr_users = self.render_rolling_users_list_text(rolling_amixr_users)
result += TerraformFileRenderer.ON_CALL_SHIFT_RESOURCE_TEMPLATE_ROLLING_USERS.format(
shift_name,
formatted_integration_name,
shift_type,
team_name_text,
start,
duration,
level,
frequency,
interval,
week_start,
by_day,
by_month,
by_monthday,
rendered_amixr_users,
)
else:
amixr_users = shift.users.all()
rendered_amixr_users = self.render_amixr_users_list_text(amixr_users)
if shift.type == CustomOnCallShift.TYPE_SINGLE_EVENT:
result += TerraformFileRenderer.ON_CALL_SHIFT_RESOURCE_TEMPLATE_SINGLE_EVENT.format(
shift_name,
formatted_integration_name,
shift_type,
team_name_text,
start,
duration,
level,
rendered_amixr_users,
)
elif shift.type == CustomOnCallShift.TYPE_RECURRENT_EVENT:
result += TerraformFileRenderer.ON_CALL_SHIFT_RESOURCE_TEMPLATE_RECURRENT_EVENT.format(
shift_name,
formatted_integration_name,
shift_type,
team_name_text,
start,
duration,
level,
frequency,
interval,
week_start,
by_day,
by_month,
by_monthday,
rendered_amixr_users,
)
return result
def render_data_text(self):
result = ""
for data_type, data_source in sorted(self.data.items(), key=lambda x: x[0], reverse=True):
for data_result in data_source:
result += data_source[data_result]
return result
def render_action_data_text(self):
result = ""
actions = self.data.pop("custom_actions", {})
for action, action_text in actions.items():
result += action_text
return result
def render_names_and_data_for_amixr_users(self, amixr_users):
amixr_users_names = []
for user in amixr_users:
user_data_name = self.render_name(user, "users", "username")
amixr_users_names.append(user_data_name)
if user.public_primary_key not in self.data.setdefault("users", {}):
data_result = TerraformFileRenderer.AMIXR_USER_DATA_TEMPLATE.format(user_data_name, user.username)
self.data["users"][user.public_primary_key] = data_result
return amixr_users_names
def render_rolling_users_list_text(self, rolling_amixr_users):
if rolling_amixr_users:
rolling_users_text = ""
for amixr_users in rolling_amixr_users:
if amixr_users:
amixr_users_names = self.render_names_and_data_for_amixr_users(amixr_users)
rendered_amixr_users = ", ".join(
[f"data.amixr_user.{user_verbal}.id" for user_verbal in amixr_users_names]
)
else:
rendered_amixr_users = TerraformFileRenderer.AMIXR_USERS_LIST_TEMPLATE_EMPTY
rolling_users_text += TerraformFileRenderer.ROLLING_USERS_TEMPLATE.format(rendered_amixr_users)
rendered_rolling_users = TerraformFileRenderer.ROLLING_USERS_LIST_TEMPLATE.format(rolling_users_text)
else:
rendered_rolling_users = TerraformFileRenderer.AMIXR_USERS_LIST_TEMPLATE_EMPTY
return rendered_rolling_users
def render_amixr_users_list_text(self, amixr_users):
if amixr_users:
amixr_users_names = self.render_names_and_data_for_amixr_users(amixr_users)
rendered_amixr_users = TerraformFileRenderer.AMIXR_USERS_LIST_TEMPLATE.format(
", ".join([f"data.amixr_user.{user_verbal}.id" for user_verbal in amixr_users_names])
)
else:
rendered_amixr_users = TerraformFileRenderer.AMIXR_USERS_LIST_TEMPLATE_EMPTY
return rendered_amixr_users
def render_name(self, obj, obj_verbal, name_attr):
if obj is None:
return None
obj_name = slugify(getattr(obj, name_attr))
obj_data_name = obj_name
counter = 1
while (
obj_data_name in self.used_names.setdefault(obj_verbal, {})
and self.used_names[obj_verbal][obj_data_name] != obj.pk
):
counter += 1
obj_data_name = f"{obj_name}-{counter}"
self.used_names[obj_verbal][obj_data_name] = obj.pk
return obj_data_name
def render_schedule_data_source(self, schedule, schedule_data_name):
if schedule is None:
return None
if schedule.pk not in self.data.setdefault("schedules", {}):
formatted_schedule_name = self.escape_string_for_terraform(schedule.name)
data_result = TerraformFileRenderer.SCHEDULE_DATA_TEMPLATE.format(
schedule_data_name, formatted_schedule_name
)
self.data["schedules"][schedule.pk] = data_result
def render_user_group_name(self, user_group):
if user_group is None:
return None
user_group_data_name = slugify(user_group.handle)
if user_group.pk not in self.data.setdefault("user_groups", {}):
data_result = TerraformFileRenderer.USER_GROUP_DATA_TEMPLATE.format(user_group_data_name, user_group.handle)
self.data["user_groups"][user_group.pk] = data_result
return user_group_data_name
def render_team_name(self, team):
if team is None:
return None
team_data_name = slugify(team.name)
if team.pk not in self.data.setdefault("teams", {}):
data_result = TerraformFileRenderer.TEAM_DATA_TEMPLATE.format(team_data_name, team.name)
self.data["teams"][team.pk] = data_result
return team_data_name
def render_custom_action_data_source(self, custom_action, custom_action_data_name, integration_resource_name):
if custom_action is None:
return None
if custom_action.pk not in self.data.setdefault("custom_actions", {}):
formatted_action_name = self.escape_string_for_terraform(custom_action.name)
data_result = TerraformFileRenderer.CUSTOM_ACTION_DATA_TEMPLATE.format(
custom_action_data_name,
formatted_action_name,
integration_resource_name,
)
self.data["custom_actions"][custom_action.pk] = data_result
def render_integration_template(self, integration):
result_template = None
slack_template = self.render_integration_slack_template(integration)
template_fields = {
"resolve_signal": integration.resolve_condition_template,
"grouping_key": integration.grouping_id_template,
}
templates = {}
for template_type, template in template_fields.items():
if template is not None:
if "\n" in template:
result = ""
template_lines = template.split("\n")
for line in template_lines:
result += f" {line}\n"
result = "<<-EOT\n" "{}" " EOT".format(result)
templates[template_type] = result
else:
template = self.escape_string_for_terraform(template)
templates[template_type] = f'"{template}"'
if len(templates) > 0 or slack_template:
result_template = "{\n"
for template_type, template in templates.items():
result_template += f" {template_type} = {template}\n"
if slack_template:
result_template += f" slack {slack_template}"
result_template += " }"
return result_template
def render_integration_slack_template(self, integration):
slack_template = None
slack_fields = {
"title": integration.slack_title_template,
"message": integration.slack_message_template,
"image_url": integration.slack_image_url_template,
}
slack_templates = {}
for template_type, template in slack_fields.items():
if template is not None:
if "\n" in template:
result = ""
template_lines = template.split("\n")
for line in template_lines:
result += f" {line}\n"
result = "<<-EOT\n" "{}" " EOT".format(result)
slack_templates[template_type] = result
else:
template = self.escape_string_for_terraform(template)
slack_templates[template_type] = f'"{template}"'
if len(slack_templates) > 0:
slack_template = "{\n"
for template_type, template in slack_templates.items():
slack_template += f" {template_type} = {template}\n"
slack_template += " }\n"
return slack_template
def render_time_string(self, time_obj):
result = f"\"{time_obj.strftime('%H:%M:%SZ')}\"" if time_obj is not None else "null"
return result
def escape_string_for_terraform(self, template_line):
template_line = template_line.replace("\\", "\\\\")
template_line = template_line.replace('"', r"\"")
return template_line
def replace_quotes(self, template_line):
template_line = template_line.replace("'", r'"')
return template_line

View file

@ -1,121 +0,0 @@
from django.utils.text import slugify
from apps.schedules.models import OnCallScheduleCalendar
class TerraformStateRenderer:
STATE_INTEGRATION_TEMPLATE = "terraform import amixr_integration.{} {}\n"
STATE_ROUTE_TEMPLATE = "terraform import amixr_route.{} {}\n"
STATE_ESCALATION_CHAIN_TEMPLATE = "terraform import amixr_escalation_chain.{} {}\n"
STATE_ESCALATION_TEMPLATE = "terraform import amixr_escalation.{} {}\n"
STATE_SCHEDULE_TEMPLATE = "terraform import amixr_schedule.{} {}\n"
STATE_ON_CALL_SHIFT_TEMPLATE = "terraform import amixr_on_call_shift.{} {}\n"
def __init__(self, organization):
self.organization = organization
self.used_names = {}
def render_state(self):
result = self.render_state_text()
if len(result) == 0:
result += "There is nothing here yet. Check Settings to add integration and come back!"
return result
def render_state_text(self):
result = ""
result += self.render_escalation_chains_related_states_text()
integrations_related_states_text = self.render_integrations_related_states_text()
result += integrations_related_states_text
schedules_related_states_text = self.render_schedule_related_states_text()
result += schedules_related_states_text
return result
def render_escalation_chains_related_states_text(self):
result = ""
escalation_chains = self.organization.escalation_chains.all()
for escalation_chain in escalation_chains:
resource_name = self.render_name(escalation_chain, "escalation_chains", "name")
result += self.STATE_ESCALATION_CHAIN_TEMPLATE.format(resource_name, escalation_chain.public_primary_key)
result += self.render_escalation_policy_state_text(escalation_chain, resource_name)
return result
def render_integrations_related_states_text(self):
result = ""
integrations = self.organization.alert_receive_channels.all().order_by("created_at")
for integration in integrations:
integration_resource_name = self.render_name(integration, "integrations", "verbal_name")
result += TerraformStateRenderer.STATE_INTEGRATION_TEMPLATE.format(
integration_resource_name,
integration.public_primary_key,
)
route_text = self.render_route_state_text(integration, integration_resource_name)
result += route_text
return result
def render_schedule_related_states_text(self):
result = ""
ical_schedules = self.organization.oncall_schedules.order_by("pk")
for schedule in ical_schedules:
schedule_resource_name = self.render_name(schedule, "schedules", "name")
result += TerraformStateRenderer.STATE_SCHEDULE_TEMPLATE.format(
schedule_resource_name,
schedule.public_primary_key,
)
if isinstance(schedule, OnCallScheduleCalendar):
on_call_shifts_text = self.render_on_call_shift_state_text(schedule)
result += on_call_shifts_text
return result
def render_route_state_text(self, integration, integration_resource_name):
routes = integration.channel_filters.all()
result = ""
for num, route in enumerate(routes, start=1):
if route.is_default:
continue
route_name = f"route-{num}-{integration_resource_name}"
result += TerraformStateRenderer.STATE_ROUTE_TEMPLATE.format(
route_name,
route.public_primary_key,
)
return result
def render_escalation_policy_state_text(self, escalation_chain, escalation_chain_resource_name):
result = ""
escalation_policies = escalation_chain.escalation_policies.all()
for num, escalation_policy in enumerate(escalation_policies, start=1):
escalation_name = f"escalation-{num}-{escalation_chain_resource_name}"
result += TerraformStateRenderer.STATE_ESCALATION_TEMPLATE.format(
escalation_name,
escalation_policy.public_primary_key,
)
return result
def render_on_call_shift_state_text(self, schedule):
result = ""
on_call_shifts = schedule.custom_on_call_shifts.all().order_by("pk")
for shift in on_call_shifts:
shift_name = self.render_name(shift, "on_call_shifts", "name")
result += TerraformStateRenderer.STATE_ON_CALL_SHIFT_TEMPLATE.format(
shift_name,
shift.public_primary_key,
)
return result
def render_name(self, obj, obj_verbal, name_attr):
obj_name = slugify(getattr(obj, name_attr))
obj_data_name = obj_name
counter = 1
while (
obj_data_name in self.used_names.setdefault(obj_verbal, {})
and self.used_names[obj_verbal][obj_data_name] != obj.pk
):
counter += 1
obj_data_name = f"{obj_name}-{counter}"
self.used_names[obj_verbal][obj_data_name] = obj.pk
return obj_data_name

View file

@ -110,7 +110,6 @@ def test_send_demo_alert(mocked_create_alert, make_organization, make_alert_rece
mocked_create_alert.call_args.args[1]["raw_request_data"] == payload
or alert_receive_channel.config.example_payload
)
assert mocked_create_alert.call_args.args[1]["force_route_id"] is None
@mock.patch("apps.integrations.tasks.create_alertmanager_alerts.apply_async", return_value=None)
@ -143,7 +142,6 @@ def test_send_demo_alert_alertmanager_payload_shape(
if payload
else alert_receive_channel.config.example_payload["alerts"][0]
)
assert mocked_create_alert.call_args.args[1]["force_route_id"] is None
@mock.patch("apps.integrations.tasks.create_alert.apply_async", return_value=None)

View file

@ -1,8 +1,6 @@
from unittest import mock
import pytest
from apps.alerts.models import AlertReceiveChannel, ChannelFilter
from apps.alerts.models import ChannelFilter
@pytest.mark.django_db
@ -93,48 +91,3 @@ def test_channel_filter_select_filter_jinja2(make_organization, make_alert_recei
alert_receive_channel, raw_request_data, force_route_id=channel_filter.pk
)
assert satisfied_filter == channel_filter
@mock.patch("apps.integrations.tasks.create_alert.apply_async", return_value=None)
@pytest.mark.django_db
def test_send_demo_alert(
mocked_create_alert,
make_organization,
make_alert_receive_channel,
make_channel_filter,
):
organization = make_organization()
alert_receive_channel = make_alert_receive_channel(
organization, integration=AlertReceiveChannel.INTEGRATION_WEBHOOK
)
filtering_term = "test alert"
channel_filter = make_channel_filter(alert_receive_channel, filtering_term=filtering_term, is_default=False)
channel_filter.send_demo_alert()
assert mocked_create_alert.called
assert mocked_create_alert.call_args.args[1]["is_demo"]
assert mocked_create_alert.call_args.args[1]["force_route_id"] == channel_filter.id
@mock.patch("apps.integrations.tasks.create_alertmanager_alerts.apply_async", return_value=None)
@pytest.mark.django_db
@pytest.mark.parametrize(
"integration",
[
AlertReceiveChannel.INTEGRATION_ALERTMANAGER,
AlertReceiveChannel.INTEGRATION_GRAFANA,
AlertReceiveChannel.INTEGRATION_GRAFANA_ALERTING,
],
)
def test_send_demo_alert_alertmanager_payload_shape(
mocked_create_alert, make_organization, make_alert_receive_channel, make_channel_filter, integration
):
organization = make_organization()
alert_receive_channel = make_alert_receive_channel(organization)
filtering_term = "test alert"
channel_filter = make_channel_filter(alert_receive_channel, filtering_term=filtering_term, is_default=False)
channel_filter.send_demo_alert()
assert mocked_create_alert.called
assert mocked_create_alert.call_args.args[1]["is_demo"]
assert mocked_create_alert.call_args.args[1]["force_route_id"] == channel_filter.pk

View file

@ -1,151 +0,0 @@
import datetime
import pytest
from django.utils.text import slugify
from apps.alerts.models import EscalationPolicy
from apps.alerts.terraform_renderer import TerraformFileRenderer, TerraformStateRenderer
from apps.schedules.models import CustomOnCallShift, OnCallScheduleCalendar
terraform_file_renderer_data = {
"filtering_term": "\\[test\\]",
"escaped_filtering_term": "\\\\[test\\\\]",
}
rendered_terraform_file_template = """
data "amixr_user" "{user_name}" {{
username = "{user_name}"
}}
resource "amixr_escalation_chain" "{escalation_chain_name}" {{
name = "{escalation_chain_name}"
team_id = null
}}
resource "amixr_escalation" "escalation-1-{escalation_chain_name}" {{
escalation_chain_id = amixr_escalation_chain.{escalation_chain_name}.id
type = "notify_persons"
important = false
persons_to_notify = [
data.amixr_user.{user_name}.id
]
position = 0
}}
resource "amixr_integration" "{integration_name}" {{
name = "{integration_verbal_name}"
type = "grafana"
team_id = null
}}
resource "amixr_on_call_shift" "{shift_name}" {{
name = "{shift_name}"
type = "rolling_users"
team_id = null
start = "2021-08-16T17:00:00"
duration = 3600
level = 0
frequency = "weekly"
interval = 1
week_start = "MO"
by_day = ["MO", "SA"]
by_month = null
by_monthday = null
rolling_users = [
[data.amixr_user.{user_name}.id],
]
}}
resource "amixr_schedule" "{schedule_name}" {{
name = "{schedule_name}"
type = "calendar"
team_id = null
time_zone = "UTC"
}}
"""
rendered_terraform_imports_template = """terraform import amixr_escalation_chain.{escalation_chain_name} {escalation_chain_public_primary_key}
terraform import amixr_escalation.escalation-1-{escalation_chain_name} {escalation_1_public_primary_key}
terraform import amixr_integration.{integration_name} {integration_public_primary_key}
"""
@pytest.mark.django_db
def test_render_terraform_file(
make_organization_and_user_with_slack_identities,
make_integration_escalation_chain_route_escalation_policy,
make_escalation_chain,
make_escalation_policy,
make_on_call_shift,
make_schedule,
):
organization, user, _, _ = make_organization_and_user_with_slack_identities()
(integration, escalation_chain, _, escalation_policy) = make_integration_escalation_chain_route_escalation_policy(
organization,
EscalationPolicy.STEP_NOTIFY_MULTIPLE_USERS,
)
escalation_policy.notify_to_users_queue.add(user)
schedule = make_schedule(
organization=organization,
schedule_class=OnCallScheduleCalendar,
name="test_calendar_schedule",
)
start = datetime.datetime.fromisoformat("2021-08-16T17:00:00Z")
shift = make_on_call_shift(
organization=organization,
name="test_shift",
shift_type=CustomOnCallShift.TYPE_ROLLING_USERS_EVENT,
frequency=CustomOnCallShift.FREQUENCY_WEEKLY,
interval=1,
week_start=CustomOnCallShift.MONDAY,
start=start,
rotation_start=start,
duration=datetime.timedelta(seconds=3600),
by_day=["MO", "SA"],
rolling_users=[{user.pk: user.public_primary_key}],
)
renderer = TerraformFileRenderer(organization)
result = renderer.render_terraform_file()
expected_result = rendered_terraform_file_template.format(
user_name=slugify(user.username),
escalation_chain_name=escalation_chain.name,
integration_name=slugify(integration.verbal_name),
integration_verbal_name=integration.verbal_name,
routing_regex=terraform_file_renderer_data["escaped_filtering_term"],
schedule_name=schedule.name,
shift_name=shift.name,
)
assert result == expected_result
@pytest.mark.django_db
def test_render_terraform_imports(
make_organization_and_user_with_slack_identities,
make_integration_escalation_chain_route_escalation_policy,
make_escalation_chain,
make_escalation_policy,
):
organization, user, _, _ = make_organization_and_user_with_slack_identities()
integration, escalation_chain, _, escalation_policy = make_integration_escalation_chain_route_escalation_policy(
organization,
EscalationPolicy.STEP_NOTIFY_MULTIPLE_USERS,
)
renderer = TerraformStateRenderer(organization)
result = renderer.render_state()
expected_result = rendered_terraform_imports_template.format(
escalation_chain_name=slugify(escalation_chain.name),
escalation_chain_public_primary_key=escalation_chain.public_primary_key,
integration_name=slugify(integration.verbal_name),
integration_public_primary_key=integration.public_primary_key,
escalation_1_public_primary_key=escalation_policy.public_primary_key,
)
assert result == expected_result

View file

@ -1,13 +0,0 @@
from rest_framework import renderers
class PlainTextRenderer(renderers.BaseRenderer):
media_type = "text/plain"
def render(self, data, media_type=None, renderer_context=None):
if type(data) == dict:
result = ""
for k, v in data.items():
result += f"{k}: {v}\n"
return result
return data.encode(self.charset)

View file

@ -219,41 +219,6 @@ def test_channel_filter_move_to_position_permissions(
assert response.status_code == expected_status
@pytest.mark.django_db
@pytest.mark.parametrize(
"role,expected_status",
[
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
],
)
def test_alert_receive_channel_send_demo_alert_permissions(
make_organization_and_user_with_plugin_token,
make_user_auth_headers,
make_alert_receive_channel,
make_channel_filter,
role,
expected_status,
):
organization, user, token = make_organization_and_user_with_plugin_token(role)
alert_receive_channel = make_alert_receive_channel(organization)
channel_filter = make_channel_filter(alert_receive_channel, is_default=True)
client = APIClient()
url = reverse("api-internal:channel_filter-send-demo-alert", kwargs={"pk": channel_filter.public_primary_key})
with patch(
"apps.api.views.channel_filter.ChannelFilterView.send_demo_alert",
return_value=Response(
status=status.HTTP_200_OK,
),
):
response = client.post(url, format="json", **make_user_auth_headers(user, token))
assert response.status_code == expected_status
@pytest.mark.django_db
def test_channel_filter_create_with_order(
make_organization_and_user_with_plugin_token,

View file

@ -1,56 +0,0 @@
import pytest
from django.urls import reverse
from rest_framework import status
from rest_framework.test import APIClient
from apps.api.permissions import LegacyAccessControlRole
@pytest.mark.django_db
@pytest.mark.parametrize(
"role,expected_status",
[
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
(LegacyAccessControlRole.VIEWER, status.HTTP_200_OK),
],
)
def test_terraform_gitops_permissions(
make_organization_and_user_with_plugin_token,
make_escalation_chain,
make_user_auth_headers,
role,
expected_status,
):
organization, user, token = make_organization_and_user_with_plugin_token(role=role)
make_escalation_chain(organization)
client = APIClient()
url = reverse("api-internal:terraform_file")
response = client.get(url, format="json", **make_user_auth_headers(user, token))
assert response.status_code == expected_status
@pytest.mark.django_db
@pytest.mark.parametrize(
"role,expected_status",
[
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
(LegacyAccessControlRole.VIEWER, status.HTTP_200_OK),
],
)
def test_terraform_state_permissions(
make_organization_and_user_with_plugin_token, make_user_auth_headers, role, expected_status
):
_, user, token = make_organization_and_user_with_plugin_token(role=role)
client = APIClient()
url = reverse("api-internal:terraform_imports")
response = client.get(url, format="json", **make_user_auth_headers(user, token))
assert response.status_code == expected_status

View file

@ -222,6 +222,9 @@ def test_list_on_call_shift(
"updated_shift": None,
}
],
"current_page_number": 1,
"page_size": 50,
"total_pages": 1,
}
assert response.status_code == status.HTTP_200_OK
@ -280,6 +283,9 @@ def test_list_on_call_shift_filter_schedule_id(
"updated_shift": None,
}
],
"current_page_number": 1,
"page_size": 50,
"total_pages": 1,
}
assert response.status_code == status.HTTP_200_OK
@ -290,6 +296,9 @@ def test_list_on_call_shift_filter_schedule_id(
"next": None,
"previous": None,
"results": [],
"current_page_number": 1,
"page_size": 50,
"total_pages": 1,
}
response = client.get(

View file

@ -137,6 +137,9 @@ def test_get_list_schedules(
"enable_web_overrides": True,
},
],
"current_page_number": 1,
"page_size": 15,
"total_pages": 1,
}
response = client.get(url, format="json", **make_user_auth_headers(user, token))
assert response.status_code == status.HTTP_200_OK
@ -247,6 +250,9 @@ def test_get_list_schedules_pagination(
"next": next_url,
"previous": previous_url,
"results": [schedule],
"current_page_number": p,
"page_size": 1,
"total_pages": 3,
}
assert response.json() == expected_payload
@ -334,6 +340,9 @@ def test_get_list_schedules_by_type(
"next": None,
"previous": None,
"results": [available_schedules[schedule_type]],
"current_page_number": 1,
"page_size": 15,
"total_pages": 1,
}
assert response.json() == expected_payload
@ -346,6 +355,9 @@ def test_get_list_schedules_by_type(
"next": None,
"previous": None,
"results": [available_schedules[0], available_schedules[1]],
"current_page_number": 1,
"page_size": 15,
"total_pages": 1,
}
assert response.json() == expected_payload

View file

@ -1,25 +0,0 @@
import pytest
from django.urls import reverse
from rest_framework import status
from rest_framework.test import APIClient
@pytest.mark.django_db
def test_get_terraform_file(
make_organization_and_user_with_plugin_token, make_user_auth_headers, make_escalation_chain
):
organization, user, token = make_organization_and_user_with_plugin_token()
make_escalation_chain(organization)
client = APIClient()
url = reverse("api-internal:terraform_file")
response = client.get(url, format="text/plain", **make_user_auth_headers(user, token))
assert response.status_code == status.HTTP_200_OK
@pytest.mark.django_db
def test_get_terraform_imports(make_organization_and_user_with_plugin_token, make_user_auth_headers):
_, user, token = make_organization_and_user_with_plugin_token()
client = APIClient()
url = reverse("api-internal:terraform_imports")
response = client.get(url, format="text/plain", **make_user_auth_headers(user, token))
assert response.status_code == status.HTTP_200_OK

View file

@ -173,6 +173,9 @@ def test_list_users(
"cloud_connection_status": None,
},
],
"current_page_number": 1,
"page_size": 100,
"total_pages": 1,
}
response = client.get(url, format="json", **make_user_auth_headers(admin, token))

View file

@ -12,7 +12,6 @@ from .views.custom_button import CustomButtonView
from .views.escalation_chain import EscalationChainViewSet
from .views.escalation_policy import EscalationPolicyView
from .views.features import FeaturesAPIView
from .views.gitops import TerraformGitOpsView, TerraformStateView
from .views.integration_heartbeat import IntegrationHeartBeatView
from .views.live_setting import LiveSettingViewSet
from .views.on_call_shifts import OnCallShiftView
@ -81,8 +80,6 @@ urlpatterns = [
GetChannelVerificationCode.as_view(),
name="api-get-channel-verification-code",
),
optional_slash_path("terraform_file", TerraformGitOpsView.as_view(), name="terraform_file"),
optional_slash_path("terraform_imports", TerraformStateView.as_view(), name="terraform_imports"),
optional_slash_path("slack_settings", SlackTeamSettingsAPIView.as_view(), name="slack-settings"),
optional_slash_path(
"slack_settings/acknowledge_remind_options",

View file

@ -12,7 +12,6 @@ from apps.api.serializers.channel_filter import (
ChannelFilterSerializer,
ChannelFilterUpdateSerializer,
)
from apps.api.throttlers import DemoAlertThrottler
from apps.auth_token.auth import PluginAuthentication
from apps.slack.models import SlackChannel
from common.api_helpers.exceptions import BadRequest
@ -23,7 +22,6 @@ from common.api_helpers.mixins import (
UpdateSerializerMixin,
)
from common.api_helpers.serializers import get_move_to_position_param
from common.exceptions import UnableToSendDemoAlert
from common.insight_log import EntityEvent, write_resource_insight_log
@ -41,7 +39,6 @@ class ChannelFilterView(
"partial_update": [RBACPermission.Permissions.INTEGRATIONS_WRITE],
"destroy": [RBACPermission.Permissions.INTEGRATIONS_WRITE],
"move_to_position": [RBACPermission.Permissions.INTEGRATIONS_WRITE],
"send_demo_alert": [RBACPermission.Permissions.INTEGRATIONS_TEST],
"convert_from_regex_to_jinja2": [RBACPermission.Permissions.INTEGRATIONS_WRITE],
}
@ -131,16 +128,6 @@ class ChannelFilterView(
return Response(status=status.HTTP_200_OK)
@action(detail=True, methods=["post"], throttle_classes=[DemoAlertThrottler])
def send_demo_alert(self, request, pk):
"""Deprecated action. May be used in the older version of the plugin."""
instance = self.get_object()
try:
instance.send_demo_alert()
except UnableToSendDemoAlert as e:
raise BadRequest(detail=str(e))
return Response(status=status.HTTP_200_OK)
@action(detail=True, methods=["post"])
def convert_from_regex_to_jinja2(self, request, pk):
instance = self.get_object()

View file

@ -1,33 +0,0 @@
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
from apps.alerts.terraform_renderer import TerraformFileRenderer, TerraformStateRenderer
from apps.api.response_renderers import PlainTextRenderer
from apps.auth_token.auth import PluginAuthentication
class TerraformGitOpsView(APIView):
authentication_classes = (PluginAuthentication,)
permission_classes = (IsAuthenticated,)
renderer_classes = [PlainTextRenderer]
def get(self, request):
organization = self.request.auth.organization
renderer = TerraformFileRenderer(organization)
terraform_file = renderer.render_terraform_file()
return Response(terraform_file)
class TerraformStateView(APIView):
authentication_classes = (PluginAuthentication,)
permission_classes = (IsAuthenticated,)
renderer_classes = (PlainTextRenderer,)
def get(self, request):
organization = self.request.auth.organization
renderer = TerraformStateRenderer(organization)
terraform_state = renderer.render_state()
return Response(terraform_state)

View file

@ -1,5 +1,3 @@
from collections import OrderedDict
from rest_framework import mixins, status, viewsets
from rest_framework.decorators import action
from rest_framework.permissions import IsAuthenticated
@ -13,12 +11,24 @@ from apps.oss_installation.serializers import CloudUserSerializer
from apps.oss_installation.utils import cloud_user_identity_status
from apps.user_management.models import User
from common.api_helpers.mixins import PublicPrimaryKeyMixin
from common.api_helpers.paginators import HundredPageSizePaginator
from common.api_helpers.paginators import HundredPageSizePaginator, PaginatedData
PERMISSIONS = [RBACPermission.Permissions.OTHER_SETTINGS_WRITE]
class CloudUsersView(HundredPageSizePaginator, APIView):
class CloudUsersPagination(HundredPageSizePaginator):
# the override ignore here is expected. The parent classes' get_paginated_response method does not
# take a matched_users_count argument. This is fine in this case
def get_paginated_response(self, data: PaginatedData, matched_users_count: int) -> Response: # type: ignore[override]
return Response(
{
**self._get_paginated_response_data(data),
"matched_users_count": matched_users_count,
}
)
class CloudUsersView(CloudUsersPagination, APIView):
authentication_classes = (PluginAuthentication,)
permission_classes = (IsAuthenticated, RBACPermission)
@ -44,14 +54,14 @@ class CloudUsersView(HundredPageSizePaginator, APIView):
cloud_identities = list(CloudUserIdentity.objects.filter(email__in=emails))
cloud_identities = {cloud_identity.email: cloud_identity for cloud_identity in cloud_identities}
response = []
data = []
connector = CloudConnector.objects.first()
for user in results:
cloud_identity = cloud_identities.get(user.email, None)
status, link = cloud_user_identity_status(connector, cloud_identity)
response.append(
data.append(
{
"id": user.public_primary_key,
"email": user.email,
@ -60,20 +70,7 @@ class CloudUsersView(HundredPageSizePaginator, APIView):
}
)
return self.get_paginated_response_with_matched_users_count(response, len(cloud_identities))
def get_paginated_response_with_matched_users_count(self, data, matched_users_count):
return Response(
OrderedDict(
[
("count", self.page.paginator.count),
("matched_users_count", matched_users_count),
("next", self.get_next_link()),
("previous", self.get_previous_link()),
("results", data),
]
)
)
return self.get_paginated_response(data, len(cloud_identities))
def post(self, request):
connector = CloudConnector.objects.first()

View file

@ -90,6 +90,9 @@ def test_get_list_alerts(
},
},
],
"current_page_number": 1,
"page_size": 50,
"total_pages": 1,
}
assert response.status_code == status.HTTP_200_OK
assert response.json() == expected_response

View file

@ -37,6 +37,9 @@ def test_get_custom_actions(
"forward_whole_payload": custom_action.forward_whole_payload,
}
],
"current_page_number": 1,
"page_size": 50,
"total_pages": 1,
}
assert response.status_code == status.HTTP_200_OK
@ -74,6 +77,9 @@ def test_get_custom_actions_filter_by_name(
"forward_whole_payload": custom_action.forward_whole_payload,
}
],
"current_page_number": 1,
"page_size": 50,
"total_pages": 1,
}
assert response.status_code == status.HTTP_200_OK
@ -94,7 +100,15 @@ def test_get_custom_actions_filter_by_name_empty_result(
response = client.get(f"{url}?name=NonExistentName", format="json", HTTP_AUTHORIZATION=f"{token}")
expected_payload = {"count": 0, "next": None, "previous": None, "results": []}
expected_payload = {
"count": 0,
"next": None,
"previous": None,
"results": [],
"current_page_number": 1,
"page_size": 50,
"total_pages": 1,
}
assert response.status_code == status.HTTP_200_OK
assert response.data == expected_payload

View file

@ -25,6 +25,9 @@ def test_get_escalation_chains(make_organization_and_user_with_token):
"name": "test",
}
],
"current_page_number": 1,
"page_size": 50,
"total_pages": 1,
}
assert response.status_code == status.HTTP_200_OK

View file

@ -62,6 +62,9 @@ def escalation_policies_setup():
escalation_policy_wait_payload,
escalation_policy_notify_persons_empty_payload,
],
"current_page_number": 1,
"page_size": 50,
"total_pages": 1,
}
return (
escalation_chain,

View file

@ -46,8 +46,15 @@ def construct_expected_response_from_incidents(incidents):
},
}
)
expected_response = {"count": incidents.count(), "next": None, "previous": None, "results": results}
return expected_response
return {
"count": incidents.count(),
"next": None,
"previous": None,
"results": results,
"current_page_number": 1,
"page_size": 50,
"total_pages": 1,
}
@pytest.fixture()
@ -275,7 +282,7 @@ def test_pagination(settings, incident_public_api_setup):
url = reverse("api-public:alert_groups-list")
with patch("common.api_helpers.paginators.PathPrefixedPagination.get_page_size", return_value=1):
with patch("common.api_helpers.paginators.PathPrefixedPagePagination.get_page_size", return_value=1):
response = client.get(url, HTTP_AUTHORIZATION=f"{token}")
assert response.status_code == status.HTTP_200_OK

View file

@ -74,6 +74,9 @@ def test_get_list_integrations(
"maintenance_end_at": None,
}
],
"current_page_number": 1,
"page_size": 50,
"total_pages": 1,
}
url = reverse("api-public:integrations-list")
response = client.get(url, format="json", HTTP_AUTHORIZATION=f"{token}")

View file

@ -98,6 +98,9 @@ def test_get_personal_notification_rules_list(personal_notification_rule_public_
"important": True,
},
],
"current_page_number": 1,
"page_size": 50,
"total_pages": 1,
}
assert response.status_code == status.HTTP_200_OK
@ -126,6 +129,9 @@ def test_get_personal_notification_rules_list_important(personal_notification_ru
"important": True,
}
],
"current_page_number": 1,
"page_size": 50,
"total_pages": 1,
}
assert response.status_code == status.HTTP_200_OK
@ -169,6 +175,9 @@ def test_get_personal_notification_rules_list_non_important(personal_notificatio
"important": False,
},
],
"current_page_number": 1,
"page_size": 50,
"total_pages": 1,
}
assert response.status_code == status.HTTP_200_OK

View file

@ -57,6 +57,9 @@ def test_get_resolution_notes(
"text": resolution_note_1.text,
},
],
"current_page_number": 1,
"page_size": 50,
"total_pages": 1,
}
assert response.status_code == status.HTTP_200_OK

View file

@ -86,6 +86,9 @@ def test_get_routes_list(
TEST_MESSAGING_BACKEND_FIELD: {"id": None, "enabled": False},
}
],
"current_page_number": 1,
"page_size": 25,
"total_pages": 1,
}
assert response.status_code == status.HTTP_200_OK
@ -123,6 +126,9 @@ def test_get_routes_filter_by_integration_id(
TEST_MESSAGING_BACKEND_FIELD: {"id": None, "enabled": False},
}
],
"current_page_number": 1,
"page_size": 25,
"total_pages": 1,
}
assert response.status_code == status.HTTP_200_OK

View file

@ -702,6 +702,9 @@ def test_get_schedule_list(
"slack": {"channel_id": slack_channel_id, "user_group_id": user_group_id},
},
],
"current_page_number": 1,
"page_size": 50,
"total_pages": 1,
}
assert response.status_code == status.HTTP_200_OK
@ -912,6 +915,12 @@ def test_oncall_shifts_export(
assert total_time_on_call[user2_public_primary_key] == expected_time_on_call
# pagination parameters are mocked out for now
assert response_json["next"] is None
assert response_json["previous"] is None
assert response_json["count"] == len(shifts)
del response_json["results"]
assert response_json == {
"next": None,
"previous": None,
"count": len(shifts),
"current_page_number": 1,
"page_size": 50,
"total_pages": 1,
}

View file

@ -32,6 +32,9 @@ def test_get_slack_channels_list(
"next": None,
"previous": None,
"results": [{"name": slack_channel.name, "slack_id": slack_channel.slack_id}],
"current_page_number": 1,
"page_size": 50,
"total_pages": 1,
}
assert response.status_code == status.HTTP_200_OK

View file

@ -38,6 +38,9 @@ def test_get_teams_list(team_public_api_setup):
"avatar_url": team.avatar_url,
}
],
"current_page_number": 1,
"page_size": 50,
"total_pages": 1,
}
assert response.status_code == status.HTTP_200_OK

View file

@ -42,6 +42,9 @@ def test_get_user_groups(
},
}
],
"current_page_number": 1,
"page_size": 50,
"total_pages": 1,
}
assert response.status_code == status.HTTP_200_OK
@ -80,6 +83,9 @@ def test_get_user_groups_filter_by_handle(
},
}
],
"current_page_number": 1,
"page_size": 50,
"total_pages": 1,
}
assert response.status_code == status.HTTP_200_OK
@ -98,7 +104,15 @@ def test_get_user_groups_filter_by_handle_empty_result(
response = client.get(f"{url}?slack_handle=NonExistentSlackHandle", format="json", HTTP_AUTHORIZATION=f"{token}")
expected_payload = {"count": 0, "next": None, "previous": None, "results": []}
expected_payload = {
"count": 0,
"next": None,
"previous": None,
"results": [],
"current_page_number": 1,
"page_size": 50,
"total_pages": 1,
}
assert response.status_code == status.HTTP_200_OK
assert response.data == expected_payload

View file

@ -82,6 +82,9 @@ def test_get_users_list(
"is_phone_number_verified": False,
},
],
"current_page_number": 1,
"page_size": 100,
"total_pages": 1,
}
assert response.status_code == status.HTTP_200_OK
@ -121,6 +124,9 @@ def test_get_users_list_short(
"is_phone_number_verified": False,
},
],
"current_page_number": 1,
"page_size": 100,
"total_pages": 1,
}
assert response.status_code == status.HTTP_200_OK
@ -170,6 +176,9 @@ def test_get_users_list_all_role_users(user_public_api_setup, make_user_for_orga
}
for user, role in expected_users
],
"current_page_number": 1,
"page_size": 100,
"total_pages": 1,
}
assert response.status_code == status.HTTP_200_OK

View file

@ -175,5 +175,8 @@ class OnCallScheduleChannelView(RateLimitHeadersMixin, UpdateSerializerMixin, Mo
"next": None,
"previous": None,
"results": data,
"current_page_number": 1,
"page_size": 50,
"total_pages": 1,
}
)

View file

@ -96,9 +96,10 @@ def _build_payload(webhook, alert_group, user):
def mask_authorization_header(headers):
if "Authorization" in headers:
headers["Authorization"] = WEBHOOK_FIELD_PLACEHOLDER
return headers
masked_headers = headers.copy()
if "Authorization" in masked_headers:
masked_headers["Authorization"] = WEBHOOK_FIELD_PLACEHOLDER
return masked_headers
def make_request(webhook, alert_group, data):
@ -123,8 +124,8 @@ def make_request(webhook, alert_group, data):
if triggered:
status["url"] = webhook.build_url(data)
request_kwargs = webhook.build_request_kwargs(data, raise_data_errors=True)
headers = mask_authorization_header(request_kwargs.get("headers", {}))
status["request_headers"] = json.dumps(headers)
display_headers = mask_authorization_header(request_kwargs.get("headers", {}))
status["request_headers"] = json.dumps(display_headers)
if "json" in request_kwargs:
status["request_data"] = json.dumps(request_kwargs["json"])
else:

View file

@ -1,45 +1,82 @@
from rest_framework.pagination import CursorPagination, PageNumberPagination
import typing
from rest_framework.pagination import BasePagination, CursorPagination, PageNumberPagination
from rest_framework.response import Response
from common.api_helpers.utils import create_engine_url
MAX_PAGE_SIZE = 100
PAGE_QUERY_PARAM = "page"
PAGE_SIZE_QUERY_PARAM = "perpage"
PaginatedData = typing.List[typing.Any]
class PathPrefixedPagination(PageNumberPagination):
max_page_size = MAX_PAGE_SIZE
page_query_param = PAGE_QUERY_PARAM
page_size_query_param = PAGE_SIZE_QUERY_PARAM
class BasePaginatedResponseData(typing.TypedDict):
next: str | None
previous: str | None
results: PaginatedData
page_size: int
class PageBasedPaginationResponseData(BasePaginatedResponseData):
count: int
current_page_number: int
total_pages: int
class BasePathPrefixedPagination(BasePagination):
max_page_size = 100
page_query_param = "page"
page_size_query_param = "perpage"
def paginate_queryset(self, queryset, request, view=None):
request.build_absolute_uri = lambda: create_engine_url(request.get_full_path())
# we're setting the request object explicitly here because the way the paginate_quersey works
# between PageNumberPagination and CursorPagination is slightly different. In the latter class,
# it does not set self.request in the paginate_queryset method, whereas in the former it does.
# this leads to an issue in _get_base_paginated_response_data where the self.request would not be set
self.request = request
return super().paginate_queryset(queryset, request, view)
class PathPrefixedCursorPagination(CursorPagination):
max_page_size = MAX_PAGE_SIZE
page_query_param = PAGE_QUERY_PARAM
page_size_query_param = PAGE_SIZE_QUERY_PARAM
def paginate_queryset(self, queryset, request, view=None):
request.build_absolute_uri = lambda: create_engine_url(request.get_full_path())
return super().paginate_queryset(queryset, request, view)
def _get_base_paginated_response_data(self, data: PaginatedData) -> BasePaginatedResponseData:
return {
"next": self.get_next_link(),
"previous": self.get_previous_link(),
"results": data,
"page_size": self.get_page_size(self.request),
}
class HundredPageSizePaginator(PathPrefixedPagination):
class PathPrefixedPagePagination(BasePathPrefixedPagination, PageNumberPagination):
def _get_paginated_response_data(self, data: PaginatedData) -> PageBasedPaginationResponseData:
return {
**self._get_base_paginated_response_data(data),
"count": self.page.paginator.count,
"current_page_number": self.page.number,
"total_pages": self.page.paginator.num_pages,
}
def get_paginated_response(self, data: PaginatedData) -> Response:
return Response(self._get_paginated_response_data(data))
class PathPrefixedCursorPagination(BasePathPrefixedPagination, CursorPagination):
def get_paginated_response(self, data: PaginatedData) -> Response:
return Response(self._get_base_paginated_response_data(data))
class HundredPageSizePaginator(PathPrefixedPagePagination):
page_size = 100
class FiftyPageSizePaginator(PathPrefixedPagination):
class FiftyPageSizePaginator(PathPrefixedPagePagination):
page_size = 50
class TwentyFivePageSizePaginator(PathPrefixedPagination):
class TwentyFivePageSizePaginator(PathPrefixedPagePagination):
page_size = 25
class FifteenPageSizePaginator(PathPrefixedPagination):
class FifteenPageSizePaginator(PathPrefixedPagePagination):
page_size = 15

View file

@ -32,7 +32,6 @@ disable_error_code = [
"call-arg",
"call-overload",
"has-type",
"import",
"index",
"misc",
"name-defined",
@ -54,6 +53,7 @@ module = [
# we can slowly either start to add library stubs ourselves, or try and upgrade these libraries to see if
# a newer version includes type stubs
"anymail.*",
"celery.utils.debug",
"debug_toolbar.*",
"django_deprecate_fields.*",
"django_sns_view.*",
@ -78,6 +78,7 @@ module = [
"twilio.*",
"uwsgidecorators.*",
"whitenoise.*",
"uwsgi.*",
]
ignore_missing_imports = true

View file

@ -12,17 +12,5 @@
margin-bottom: 20px;
width: 100%;
background: var(--background-secondary);
& > li .hover-button {
display: none;
}
& > li:hover .hover-button {
display: inline-flex;
}
& > li {
padding: 10px 12px;
width: 100%;
}
padding: 10px 12px;
}

View file

@ -141,20 +141,19 @@ const ManualAlertGroup: FC<ManualAlertGroupProps> = (props) => {
<HorizontalGroup>
{chatOpsAvailableChannels && (
<>
<Text type="secondary">ChatOps:</Text>{' '}
{chatOpsAvailableChannels.map(
(chatOpsChannel: { name: string; icon: IconName }, chatOpsIndex) => (
<div
key={`${chatOpsChannel.name}-${chatOpsIndex}`}
className={cx({
'u-margin-right-xs': chatOpsIndex !== chatOpsAvailableChannels.length,
})}
>
{chatOpsChannel.icon && <Icon name={chatOpsChannel.icon} className={cx('icon')} />}
<div key={`${chatOpsChannel.name}-${chatOpsIndex}`}>
{chatOpsChannel.icon && <Icon name={chatOpsChannel.icon} />}
<Text type="primary">{chatOpsChannel.name || ''}</Text>
</div>
)
)}
{chatOpsAvailableChannels && (
<Tooltip content="Alert group will be posted to these chatops channels according to integration configuration">
<Icon name="info-circle" />
</Tooltip>
)}
</>
)}
</HorizontalGroup>

View file

@ -27,16 +27,16 @@ const IntegrationHearbeatForm = observer(({ alertReceveChannelId, onClose }: Int
const { heartbeatStore, alertReceiveChannelStore } = useStore();
const alertReceiveChannel = alertReceiveChannelStore.items[alertReceveChannelId];
const heartbeatId = alertReceiveChannelStore.alertReceiveChannelToHeartbeat[alertReceiveChannel.id];
const heartbeat = heartbeatStore.items[heartbeatId];
useEffect(() => {
heartbeatStore.updateTimeoutOptions();
}, [heartbeatStore]);
}, []);
useEffect(() => {
if (alertReceiveChannel.heartbeat) {
setInterval(alertReceiveChannel.heartbeat.timeout_seconds);
}
}, [alertReceiveChannel]);
setInterval(heartbeat.timeout_seconds);
}, [heartbeat]);
const timeoutOptions = heartbeatStore.timeoutOptions;
@ -66,22 +66,30 @@ const IntegrationHearbeatForm = observer(({ alertReceveChannelId, onClose }: Int
</WithPermissionControlTooltip>
</Field>
</div>
<div className={cx('u-width-100')}>
<Field label="Endpoint" description="Use the following unique Grafana link to send GET and POST requests">
<IntegrationInputField value={alertReceiveChannel?.integration_url} showEye={false} isMasked={false} />
<IntegrationInputField value={heartbeat?.link} showEye={false} isMasked={false} />
</Field>
</div>
{/* <p>
To send periodic heartbeat alerts from <Emoji text={alertReceiveChannel?.verbal_name || ''} /> to OnCall, do
the following:
<span
dangerouslySetInnerHTML={{
__html: heartbeat.instruction,
}}
/>
</p> */}
</VerticalGroup>
<VerticalGroup style={{ marginTop: 'auto' }}>
<HorizontalGroup className={cx('buttons')} justify="flex-end">
<Button variant={'secondary'} onClick={onClose}>
Cancel
{heartbeat ? 'Close' : 'Cancel'}
</Button>
<WithPermissionControlTooltip key="ok" userAction={UserActions.IntegrationsWrite}>
<Button variant="primary" onClick={onSave}>
{alertReceiveChannel.heartbeat ? 'Save' : 'Create'}
{heartbeat ? 'Save' : 'Create'}
</Button>
</WithPermissionControlTooltip>
</HorizontalGroup>
@ -91,24 +99,14 @@ const IntegrationHearbeatForm = observer(({ alertReceveChannelId, onClose }: Int
);
async function onSave() {
const heartbeat = alertReceiveChannel.heartbeat;
await heartbeatStore.saveHeartbeat(heartbeat.id, {
alert_receive_channel: heartbeat.alert_receive_channel,
timeout_seconds: interval,
});
if (heartbeat) {
await heartbeatStore.saveHeartbeat(heartbeat.id, {
alert_receive_channel: heartbeat.alert_receive_channel,
timeout_seconds: interval,
});
onClose();
onClose();
} else {
await heartbeatStore.createHeartbeat(alertReceveChannelId, {
timeout_seconds: interval,
});
onClose();
}
await alertReceiveChannelStore.updateItem(alertReceveChannelId);
await alertReceiveChannelStore.loadItem(alertReceveChannelId);
}
});

View file

@ -91,11 +91,14 @@ export class AlertReceiveChannelStore extends BaseStore {
async loadItem(id: AlertReceiveChannel['id'], skipErrorHandling = false): Promise<AlertReceiveChannel> {
const alertReceiveChannel = await this.getById(id, skipErrorHandling);
// @ts-ignore
this.items = {
...this.items,
[id]: alertReceiveChannel,
[id]: omit(alertReceiveChannel, 'heartbeat'),
};
this.populateHearbeats([alertReceiveChannel]);
return alertReceiveChannel;
}
@ -118,31 +121,7 @@ export class AlertReceiveChannelStore extends BaseStore {
this.searchResult = results.map((item: AlertReceiveChannel) => item.id);
const heartbeats = results.reduce((acc: any, alertReceiveChannel: AlertReceiveChannel) => {
if (alertReceiveChannel.heartbeat) {
acc[alertReceiveChannel.heartbeat.id] = alertReceiveChannel.heartbeat;
}
return acc;
}, {});
this.rootStore.heartbeatStore.items = {
...this.rootStore.heartbeatStore.items,
...heartbeats,
};
const alertReceiveChannelToHeartbeat = results.reduce((acc: any, alertReceiveChannel: AlertReceiveChannel) => {
if (alertReceiveChannel.heartbeat) {
acc[alertReceiveChannel.id] = alertReceiveChannel.heartbeat.id;
}
return acc;
}, {});
this.alertReceiveChannelToHeartbeat = {
...this.alertReceiveChannelToHeartbeat,
...alertReceiveChannelToHeartbeat,
};
this.populateHearbeats(results);
this.updateCounters();
@ -170,7 +149,15 @@ export class AlertReceiveChannelStore extends BaseStore {
results: results.map((item: AlertReceiveChannel) => item.id),
};
const heartbeats = results.reduce((acc: any, alertReceiveChannel: AlertReceiveChannel) => {
this.populateHearbeats(results);
this.updateCounters();
return results;
}
populateHearbeats(alertReceiveChannels: AlertReceiveChannel[]) {
const heartbeats = alertReceiveChannels.reduce((acc: any, alertReceiveChannel: AlertReceiveChannel) => {
if (alertReceiveChannel.heartbeat) {
acc[alertReceiveChannel.heartbeat.id] = alertReceiveChannel.heartbeat;
}
@ -183,22 +170,21 @@ export class AlertReceiveChannelStore extends BaseStore {
...heartbeats,
};
const alertReceiveChannelToHeartbeat = results.reduce((acc: any, alertReceiveChannel: AlertReceiveChannel) => {
if (alertReceiveChannel.heartbeat) {
acc[alertReceiveChannel.id] = alertReceiveChannel.heartbeat.id;
}
const alertReceiveChannelToHeartbeat = alertReceiveChannels.reduce(
(acc: any, alertReceiveChannel: AlertReceiveChannel) => {
if (alertReceiveChannel.heartbeat) {
acc[alertReceiveChannel.id] = alertReceiveChannel.heartbeat.id;
}
return acc;
}, {});
return acc;
},
{}
);
this.alertReceiveChannelToHeartbeat = {
...this.alertReceiveChannelToHeartbeat,
...alertReceiveChannelToHeartbeat,
};
this.updateCounters();
return results;
}
@action

View file

@ -726,7 +726,7 @@ const IntegrationActions: React.FC<IntegrationActionsProps> = ({
alertReceiveChannel,
changeIsTemplateSettingsOpen,
}) => {
const { alertReceiveChannelStore, heartbeatStore } = useStore();
const { alertReceiveChannelStore } = useStore();
const history = useHistory();
@ -927,9 +927,7 @@ const IntegrationActions: React.FC<IntegrationActionsProps> = ({
);
function showHeartbeatSettings() {
const heartbeatId = alertReceiveChannelStore.alertReceiveChannelToHeartbeat[alertReceiveChannel.id];
const heartbeat = heartbeatStore.items[heartbeatId];
return !!heartbeat?.last_heartbeat_time_verbal;
return alertReceiveChannel.is_available_for_integration_heartbeat;
}
function deleteIntegration() {
@ -1159,22 +1157,19 @@ const IntegrationHeader: React.FC<IntegrationHeaderProps> = ({
const heartbeatId = alertReceiveChannelStore.alertReceiveChannelToHeartbeat[alertReceiveChannel.id];
const heartbeat = heartbeatStore.items[heartbeatId];
const heartbeatStatus = Boolean(heartbeat?.status);
if (
!alertReceiveChannel.is_available_for_integration_heartbeat ||
!alertReceiveChannel.heartbeat?.last_heartbeat_time_verbal
) {
if (!alertReceiveChannel.is_available_for_integration_heartbeat || !heartbeat?.last_heartbeat_time_verbal) {
return null;
}
const heartbeatStatus = Boolean(heartbeat?.status);
return (
<TooltipBadge
text={undefined}
className={cx('heartbeat-badge')}
borderType={heartbeatStatus ? 'success' : 'danger'}
customIcon={heartbeatStatus ? <HeartIcon /> : <HeartRedIcon />}
tooltipTitle={`Last heartbeat: ${alertReceiveChannel.heartbeat?.last_heartbeat_time_verbal}`}
tooltipTitle={`Last heartbeat: ${heartbeat?.last_heartbeat_time_verbal}`}
tooltipContent={undefined}
/>
);