Revert "Alert group payload labels" (#3433)

Reverts grafana/oncall#3385
This commit is contained in:
Vadim Stepanov 2023-11-27 17:28:34 +00:00 committed by GitHub
parent 5fac6aeac5
commit e09422a07d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 162 additions and 935 deletions

View file

@ -1,23 +0,0 @@
# Generated by Django 4.2.7 on 2023-11-22 12:14
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('alerts', '0039_remove_alertreceivechannel_unique_integration_name'),
]
operations = [
migrations.AddField(
model_name='alertreceivechannel',
name='alert_group_labels_custom',
field=models.JSONField(default=list, null=True),
),
migrations.AddField(
model_name='alertreceivechannel',
name='alert_group_labels_template',
field=models.TextField(default=None, null=True),
),
]

View file

@ -12,7 +12,7 @@ from django.db.models import JSONField
from apps.alerts import tasks
from apps.alerts.constants import TASK_DELAY_SECONDS
from apps.alerts.incident_appearance.templaters import TemplateLoader
from apps.labels.alert_group_labels import assign_labels
from apps.labels.utils import assign_labels
from common.jinja_templater import apply_jinja_template
from common.jinja_templater.apply_jinja_template import JinjaTemplateError, JinjaTemplateWarning
from common.public_primary_keys import generate_public_primary_key, increase_public_primary_key_length
@ -108,7 +108,7 @@ class Alert(models.Model):
)
if group_created:
assign_labels(group, alert_receive_channel, raw_request_data)
assign_labels(group, alert_receive_channel)
group.log_records.create(type=AlertGroupLogRecord.TYPE_REGISTERED)
group.log_records.create(type=AlertGroupLogRecord.TYPE_ROUTE_ASSIGNED)

View file

@ -43,7 +43,6 @@ if typing.TYPE_CHECKING:
ResolutionNoteSlackMessage,
)
from apps.base.models import UserNotificationPolicyLogRecord
from apps.labels.models import AlertGroupAssociatedLabel
from apps.slack.models import SlackMessage
logger = logging.getLogger(__name__)
@ -195,7 +194,6 @@ class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models.
slack_log_message: typing.Optional["SlackMessage"]
slack_messages: "RelatedManager['SlackMessage']"
users: "RelatedManager['User']"
labels: "RelatedManager['AlertGroupAssociatedLabel']"
objects: models.Manager["AlertGroup"] = AlertGroupQuerySet.as_manager()

View file

@ -42,7 +42,6 @@ if typing.TYPE_CHECKING:
from django.db.models.manager import RelatedManager
from apps.alerts.models import AlertGroup, ChannelFilter
from apps.labels.models import AlertReceiveChannelAssociatedLabel
from apps.user_management.models import Organization, Team
logger = logging.getLogger(__name__)
@ -88,6 +87,10 @@ def number_to_smiles_translator(number):
return "".join(reversed(smileset))
class IntegrationAlertGroupLabels(typing.TypedDict):
inheritable: typing.Dict[str, bool]
class AlertReceiveChannelQueryset(models.QuerySet):
def delete(self):
self.update(deleted_at=timezone.now())
@ -120,7 +123,6 @@ class AlertReceiveChannel(IntegrationOptionsMixin, MaintainableObject):
channel_filters: "RelatedManager['ChannelFilter']"
organization: "Organization"
team: typing.Optional["Team"]
labels: "RelatedManager['AlertReceiveChannelAssociatedLabel']"
objects = AlertReceiveChannelManager()
objects_with_maintenance = AlertReceiveChannelManagerWithMaintenance()
@ -204,17 +206,6 @@ class AlertReceiveChannel(IntegrationOptionsMixin, MaintainableObject):
rate_limited_in_slack_at = models.DateTimeField(null=True, default=None)
rate_limit_message_task_id = models.CharField(max_length=100, null=True, default=None)
AlertGroupCustomLabels = list[tuple[str, str | None, str | None]]
alert_group_labels_custom: AlertGroupCustomLabels = models.JSONField(null=True, default=list)
"""
Stores "custom labels" for alert group labels. Custom labels can be either "plain" or "templated".
For plain labels, the format is: [<LABEL_KEY_ID>, <LABEL_VALUE_ID>, None]
For templated labels, the format is: [<LABEL_KEY_ID>, None, <JINJA2_TEMPLATE>]
"""
alert_group_labels_template: str | None = models.TextField(null=True, default=None)
"""Stores a Jinja2 template for "advanced label templating" for alert group labels."""
def __str__(self):
short_name_with_emojis = emojize(self.short_name, language="alias")
return f"{self.pk}: {short_name_with_emojis}"
@ -644,6 +635,21 @@ class AlertReceiveChannel(IntegrationOptionsMixin, MaintainableObject):
result["team"] = "General"
return result
@property
def alert_group_labels(self) -> IntegrationAlertGroupLabels:
"""
Alert group labels configuration for the integration used by AlertReceiveChannelSerializer.
See AlertReceiveChannelAssociatedLabel.inheritable for more details.
"""
return {"inheritable": {label.key_id: label.inheritable for label in self.labels.all()}}
@alert_group_labels.setter
def alert_group_labels(self, value: IntegrationAlertGroupLabels) -> None:
"""Setter for alert_group_labels used by AlertReceiveChannelSerializer"""
inheritable_key_ids = [key_id for key_id, inheritable in value["inheritable"].items() if inheritable]
self.labels.filter(key_id__in=inheritable_key_ids).update(inheritable=True)
self.labels.filter(~Q(key_id__in=inheritable_key_ids)).update(inheritable=False)
@receiver(post_save, sender=AlertReceiveChannel)
def listen_for_alertreceivechannel_model_save(

View file

@ -4,7 +4,6 @@ from collections import OrderedDict
from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist
from django.core.exceptions import ValidationError as DjangoValidationError
from django.db.models import Q
from jinja2 import TemplateSyntaxError
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
@ -15,188 +14,30 @@ from apps.alerts.models import AlertReceiveChannel
from apps.alerts.models.channel_filter import ChannelFilter
from apps.base.messaging import get_messaging_backends
from apps.integrations.legacy_prefix import has_legacy_prefix
from apps.labels.models import LabelKeyCache, LabelValueCache
from apps.user_management.models import Organization
from common.api_helpers.custom_fields import TeamPrimaryKeyRelatedField
from common.api_helpers.exceptions import BadRequest
from common.api_helpers.mixins import APPEARANCE_TEMPLATE_NAMES, EagerLoadingMixin
from common.api_helpers.utils import CurrentTeamDefault
from common.jinja_templater import jinja_template_env
from common.jinja_templater import apply_jinja_template, jinja_template_env
from common.jinja_templater.apply_jinja_template import JinjaTemplateWarning
from .integration_heartbeat import IntegrationHeartBeatSerializer
from .labels import LabelsSerializerMixin
class AlertGroupCustomLabelKey(typing.TypedDict):
id: str
name: str
class AlertGroupCustomLabelValue(typing.TypedDict):
id: str | None # None for templated labels, label value ID for plain labels
name: str # Jinja template for templated labels, label value name for plain labels
class AlertGroupCustomLabel(typing.TypedDict):
key: AlertGroupCustomLabelKey
value: AlertGroupCustomLabelValue
AlertGroupCustomLabels = list[AlertGroupCustomLabel]
class IntegrationAlertGroupLabels(typing.TypedDict):
inheritable: dict[str, bool]
custom: AlertGroupCustomLabels
template: str | None
class CustomLabelSerializer(serializers.Serializer):
"""This serializer is consistent with apps.api.serializers.labels.LabelSerializer, but allows null for value ID."""
class KeySerializer(serializers.Serializer):
id = serializers.CharField()
name = serializers.CharField()
class ValueSerializer(serializers.Serializer):
# ID is null for templated labels. For such labels, the "name" value is a Jinja2 template.
id = serializers.CharField(allow_null=True)
name = serializers.CharField()
key = KeySerializer()
value = ValueSerializer()
def valid_jinja_template_for_serializer_method_field(template):
for _, val in template.items():
try:
apply_jinja_template(val, payload={})
except JinjaTemplateWarning:
# Suppress warnings, template may be valid with payload
pass
class IntegrationAlertGroupLabelsSerializer(serializers.Serializer):
"""Alert group labels configuration for the integration. See AlertReceiveChannel.alert_group_labels for details."""
inheritable = serializers.DictField(child=serializers.BooleanField())
custom = CustomLabelSerializer(many=True)
template = serializers.CharField(allow_null=True)
@staticmethod
def pop_alert_group_labels(validated_data: dict) -> IntegrationAlertGroupLabels | None:
"""Get alert group labels from validated data."""
# the "alert_group_labels" field is optional, so either all 3 fields are present or none
if "inheritable" not in validated_data:
return None
return {
"inheritable": validated_data.pop("inheritable"),
"custom": validated_data.pop("custom"),
"template": validated_data.pop("template"),
}
@classmethod
def update(
cls, instance: AlertReceiveChannel, alert_group_labels: IntegrationAlertGroupLabels | None
) -> AlertReceiveChannel:
if alert_group_labels is None:
return instance
# update inheritable labels
inheritable_key_ids = [
key_id for key_id, inheritable in alert_group_labels["inheritable"].items() if inheritable
]
instance.labels.filter(key_id__in=inheritable_key_ids).update(inheritable=True)
instance.labels.filter(~Q(key_id__in=inheritable_key_ids)).update(inheritable=False)
# update DB cache for custom labels
cls._create_custom_labels(instance.organization, alert_group_labels["custom"])
# update custom labels
instance.alert_group_labels_custom = cls._custom_labels_to_internal_value(alert_group_labels["custom"])
# update template
instance.alert_group_labels_template = alert_group_labels["template"]
instance.save(update_fields=["alert_group_labels_custom", "alert_group_labels_template"])
return instance
@staticmethod
def _create_custom_labels(organization: Organization, labels: AlertGroupCustomLabels) -> None:
"""Create LabelKeyCache and LabelValueCache objects for custom labels."""
label_keys = [
LabelKeyCache(id=label["key"]["id"], name=label["key"]["name"], organization=organization)
for label in labels
]
label_values = [
LabelValueCache(id=label["value"]["id"], name=label["value"]["name"], key_id=label["key"]["id"])
for label in labels
if label["value"]["id"] # don't create LabelValueCache objects for templated labels
]
LabelKeyCache.objects.bulk_create(label_keys, ignore_conflicts=True, batch_size=5000)
LabelValueCache.objects.bulk_create(label_values, ignore_conflicts=True, batch_size=5000)
@classmethod
def to_representation(cls, instance: AlertReceiveChannel) -> IntegrationAlertGroupLabels:
"""
The API representation of alert group labels is very different from the underlying model.
"inheritable" is based on AlertReceiveChannelAssociatedLabel.inheritable, a property of another model.
"custom" is based on AlertReceiveChannel.alert_group_labels_custom, a JSONField with a different schema.
"template" is based on AlertReceiveChannel.alert_group_labels_template, this one is straightforward.
"""
return {
"inheritable": {label.key_id: label.inheritable for label in instance.labels.all()},
"custom": cls._custom_labels_to_representation(instance.alert_group_labels_custom),
"template": instance.alert_group_labels_template,
}
@staticmethod
def _custom_labels_to_internal_value(
custom_labels: AlertGroupCustomLabels,
) -> AlertReceiveChannel.AlertGroupCustomLabels:
"""Convert custom labels from API representation to the schema used by the JSONField on the model."""
return [
[label["key"]["id"], label["value"]["id"], None if label["value"]["id"] else label["value"]["name"]]
for label in custom_labels
]
@staticmethod
def _custom_labels_to_representation(
custom_labels: AlertReceiveChannel.AlertGroupCustomLabels,
) -> AlertGroupCustomLabels:
"""
Inverse of the _custom_labels_to_internal_value method above.
Fetches label names from DB cache, so the API response schema is consistent with other label endpoints.
"""
from apps.labels.models import LabelKeyCache, LabelValueCache
# get up-to-date label key names
label_key_names = {
k.id: k.name
for k in LabelKeyCache.objects.filter(id__in=[label[0] for label in custom_labels]).only("id", "name")
}
# get up-to-date label value names
label_value_names = {
v.id: v.name
for v in LabelValueCache.objects.filter(id__in=[label[1] for label in custom_labels if label[1]]).only(
"id", "name"
)
}
return [
{
"key": {
"id": key_id,
"name": label_key_names[key_id],
},
"value": {
"id": value_id if value_id else None,
"name": label_value_names[value_id] if value_id else typing.cast(str, template),
},
}
for key_id, value_id, template in custom_labels
if key_id in label_key_names and (value_id in label_value_names or not value_id)
]
class AlertReceiveChannelSerializer(
@ -223,7 +64,7 @@ class AlertReceiveChannelSerializer(
connected_escalations_chains_count = serializers.SerializerMethodField()
inbound_email = serializers.CharField(required=False)
is_legacy = serializers.SerializerMethodField()
alert_group_labels = IntegrationAlertGroupLabelsSerializer(source="*", required=False)
alert_group_labels = IntegrationAlertGroupLabelsSerializer(required=False)
# integration heartbeat is in PREFETCH_RELATED not by mistake.
# With using of select_related ORM builds strange join
@ -297,10 +138,8 @@ class AlertReceiveChannelSerializer(
if _integration.slug == integration:
is_able_to_autoresolve = _integration.is_able_to_autoresolve
# pop associated labels and alert group labels, so they are not passed to AlertReceiveChannel.create
labels = validated_data.pop("labels", None)
alert_group_labels = IntegrationAlertGroupLabelsSerializer.pop_alert_group_labels(validated_data)
alert_group_labels = validated_data.pop("alert_group_labels", None)
try:
instance = AlertReceiveChannel.create(
**validated_data,
@ -311,22 +150,17 @@ class AlertReceiveChannelSerializer(
except AlertReceiveChannel.DuplicateDirectPagingError:
raise BadRequest(detail=AlertReceiveChannel.DuplicateDirectPagingError.DETAIL)
# Create label associations first, then update alert group labels
# Create label associations first, then update inheritable labels
self.update_labels_association_if_needed(labels, instance, organization)
instance = IntegrationAlertGroupLabelsSerializer.update(instance, alert_group_labels)
if alert_group_labels:
instance.alert_group_labels = alert_group_labels
return instance
def update(self, instance, validated_data):
# update associated labels
labels = validated_data.pop("labels", None)
self.update_labels_association_if_needed(labels, instance, self.context["request"].auth.organization)
# update alert group labels
instance = IntegrationAlertGroupLabelsSerializer.update(
instance, IntegrationAlertGroupLabelsSerializer.pop_alert_group_labels(validated_data)
)
organization = self.context["request"].auth.organization
self.update_labels_association_if_needed(labels, instance, organization)
try:
return super().update(instance, validated_data)
except AlertReceiveChannel.DuplicateDirectPagingError:

View file

@ -3,12 +3,12 @@ import typing
from rest_framework import serializers
from apps.alerts.models import AlertReceiveChannel, ChannelFilter, EscalationChain
from apps.api.serializers.alert_receive_channel import valid_jinja_template_for_serializer_method_field
from apps.base.messaging import get_messaging_backend_from_id
from apps.telegram.models import TelegramToOrganizationConnector
from common.api_helpers.custom_fields import OrganizationFilteredPrimaryKeyRelatedField
from common.api_helpers.exceptions import BadRequest
from common.api_helpers.mixins import EagerLoadingMixin
from common.api_helpers.utils import valid_jinja_template_for_serializer_method_field
from common.jinja_templater.apply_jinja_template import JinjaTemplateError
from common.utils import is_regex_valid

View file

@ -9,7 +9,6 @@ from rest_framework.test import APIClient
from apps.alerts.models import AlertReceiveChannel, EscalationPolicy
from apps.api.permissions import LegacyAccessControlRole
from apps.labels.models import LabelKeyCache, LabelValueCache
@pytest.fixture()
@ -1384,49 +1383,23 @@ def test_update_alert_receive_channel_labels_duplicate_key(
def test_alert_group_labels_get(
make_organization_and_user_with_plugin_token,
make_alert_receive_channel,
make_label_key_and_value,
make_integration_label_association,
make_user_auth_headers,
):
organization, user, token = make_organization_and_user_with_plugin_token()
alert_receive_channel = make_alert_receive_channel(organization)
label_key, label_value = make_label_key_and_value(organization)
label_key_1, _ = make_label_key_and_value(organization)
client = APIClient()
url = reverse("api-internal:alert_receive_channel-detail", kwargs={"pk": alert_receive_channel.public_primary_key})
response = client.get(url, **make_user_auth_headers(user, token))
assert response.status_code == status.HTTP_200_OK
assert response.json()["alert_group_labels"] == {"inheritable": {}, "custom": [], "template": None}
assert response.json()["alert_group_labels"] == {"inheritable": {}}
label = make_integration_label_association(organization, alert_receive_channel)
template = "{{ payload.labels | tojson }}"
alert_receive_channel.alert_group_labels_template = template
alert_receive_channel.alert_group_labels_custom = [
(label_key.id, label_value.id, None),
(label_key_1.id, None, "{{ payload.foo }}"),
]
alert_receive_channel.save(update_fields=["alert_group_labels_custom", "alert_group_labels_template"])
response = client.get(url, **make_user_auth_headers(user, token))
assert response.status_code == status.HTTP_200_OK
assert response.json()["alert_group_labels"] == {
"inheritable": {label.key_id: True},
"custom": [
{
"key": {"id": label_key.id, "name": label_key.name},
"value": {"id": label_value.id, "name": label_value.name},
},
{
"key": {"id": label_key_1.id, "name": label_key_1.name},
"value": {"id": None, "name": "{{ payload.foo }}"},
},
],
"template": template,
}
assert response.json()["alert_group_labels"] == {"inheritable": {label.key_id: True}}
@pytest.mark.django_db
@ -1440,75 +1413,14 @@ def test_alert_group_labels_put(
alert_receive_channel = make_alert_receive_channel(organization)
label_1 = make_integration_label_association(organization, alert_receive_channel)
label_2 = make_integration_label_association(organization, alert_receive_channel, inheritable=False)
label_3 = make_integration_label_association(organization, alert_receive_channel, inheritable=False)
custom = [
# plain label
{
"key": {"id": label_2.key.id, "name": label_2.key.name},
"value": {"id": label_2.value.id, "name": label_2.value.name},
},
# plain label not present in DB cache
{
"key": {"id": "hello", "name": "world"},
"value": {"id": "foo", "name": "bar"},
},
# templated label
{
"key": {"id": label_3.key.id, "name": label_3.key.name},
"value": {"id": None, "name": "{{ payload.foo }}"},
},
]
template = "{{ payload.labels | tojson }}" # advanced template
client = APIClient()
url = reverse("api-internal:alert_receive_channel-detail", kwargs={"pk": alert_receive_channel.public_primary_key})
data = {
"alert_group_labels": {
"inheritable": {label_1.key_id: False, label_2.key_id: True, label_3.key_id: False},
"custom": custom,
"template": template,
}
}
data = {"alert_group_labels": {"inheritable": {label_1.key_id: False, label_2.key_id: True}}}
response = client.put(url, data, format="json", **make_user_auth_headers(user, token))
assert response.status_code == status.HTTP_200_OK
assert response.json()["alert_group_labels"] == {
"inheritable": {label_1.key_id: False, label_2.key_id: True, label_3.key_id: False},
"custom": custom,
"template": template,
}
alert_receive_channel.refresh_from_db()
assert alert_receive_channel.alert_group_labels_custom == [
[label_2.key_id, label_2.value_id, None],
["hello", "foo", None],
[label_3.key_id, None, "{{ payload.foo }}"],
]
assert alert_receive_channel.alert_group_labels_template == template
# check label keys & values are created
key = LabelKeyCache.objects.filter(id="hello", name="world", organization=organization).first()
assert key is not None
assert LabelValueCache.objects.filter(key=key, id="foo", name="bar").exists()
@pytest.mark.django_db
def test_alert_group_labels_put_none(
make_organization_and_user_with_plugin_token,
make_alert_receive_channel,
make_user_auth_headers,
):
organization, user, token = make_organization_and_user_with_plugin_token()
alert_receive_channel = make_alert_receive_channel(organization)
client = APIClient()
url = reverse("api-internal:alert_receive_channel-detail", kwargs={"pk": alert_receive_channel.public_primary_key})
response = client.put(url, {"verbal_name": "123"}, format="json", **make_user_auth_headers(user, token))
assert response.status_code == status.HTTP_200_OK
assert response.json()["verbal_name"] == "123"
assert response.json()["alert_group_labels"] == {"inheritable": {}, "custom": [], "template": None}
assert response.json()["alert_group_labels"] == {"inheritable": {label_1.key_id: False, label_2.key_id: True}}
@pytest.mark.django_db
@ -1516,11 +1428,7 @@ def test_alert_group_labels_post(alert_receive_channel_internal_api_setup, make_
user, token, _ = alert_receive_channel_internal_api_setup
labels = [{"key": {"id": "test", "name": "test"}, "value": {"id": "123", "name": "123"}}]
alert_group_labels = {
"inheritable": {"test": False},
"custom": [{"key": {"id": "test", "name": "test"}, "value": {"id": "123", "name": "123"}}],
"template": "{{ payload.labels | tojson }}",
}
alert_group_labels = {"inheritable": {"test": False}}
data = {
"integration": AlertReceiveChannel.INTEGRATION_GRAFANA,
"team": None,
@ -1535,7 +1443,3 @@ def test_alert_group_labels_post(alert_receive_channel_internal_api_setup, make_
assert response.status_code == status.HTTP_201_CREATED
assert response.json()["labels"] == labels
assert response.json()["alert_group_labels"] == alert_group_labels
alert_receive_channel = AlertReceiveChannel.objects.get(public_primary_key=response.json()["id"])
assert alert_receive_channel.alert_group_labels_custom == [["test", "123", None]]
assert alert_receive_channel.alert_group_labels_template == "{{ payload.labels | tojson }}"

View file

@ -337,37 +337,6 @@ def test_preview_alert_receive_channel_backend_templater(
assert response.json() == {"preview": "title: alert!"}
@pytest.mark.django_db
def test_preview_alert_group_labels(
make_organization_and_user_with_plugin_token,
make_user_auth_headers,
make_alert_receive_channel,
make_channel_filter,
make_alert_group,
make_alert,
):
organization, user, token = make_organization_and_user_with_plugin_token()
alert_receive_channel = make_alert_receive_channel(organization)
default_channel_filter = make_channel_filter(alert_receive_channel, is_default=True)
alert_group = make_alert_group(alert_receive_channel, channel_filter=default_channel_filter)
make_alert(alert_group=alert_group, raw_request_data={"labels": {"1": "2"}})
client = APIClient()
url = reverse(
"api-internal:alert_receive_channel-preview-template",
kwargs={"pk": alert_receive_channel.public_primary_key},
)
data = {
"template_body": "{{ payload.labels | tojson }}",
"template_name": "alert_group_labels",
}
response = client.post(url, format="json", data=data, **make_user_auth_headers(user, token))
assert response.status_code == status.HTTP_200_OK
assert response.json() == {"preview": '{"1": "2"}'}
@pytest.mark.django_db
def test_update_alert_receive_channel_templates(
make_organization_and_user_with_plugin_token,

View file

@ -1,158 +0,0 @@
import json
import logging
import typing
from apps.labels.utils import is_labels_feature_enabled
from common.jinja_templater import apply_jinja_template
from common.jinja_templater.apply_jinja_template import JinjaTemplateError, JinjaTemplateWarning
if typing.TYPE_CHECKING:
from apps.alerts.models import AlertGroup, AlertReceiveChannel
logger = logging.getLogger(__name__)
# What can be used as a label key/value coming out from the template
LABEL_VALUE_TYPES = (str, int, float, bool)
def assign_labels(
alert_group: "AlertGroup", alert_receive_channel: "AlertReceiveChannel", raw_request_data: typing.Any
) -> None:
from apps.labels.models import AlertGroupAssociatedLabel
if not is_labels_feature_enabled(alert_receive_channel.organization):
return
# inherit labels from the integration
labels = {
label.key.name: label.value.name
for label in alert_receive_channel.labels.filter(inheritable=True).select_related("key", "value")
}
# apply custom labels
labels.update(_custom_labels(alert_receive_channel, raw_request_data))
# apply template labels
labels.update(_template_labels(alert_receive_channel, raw_request_data))
# create associated labels
alert_group_labels = [
AlertGroupAssociatedLabel(
alert_group=alert_group,
organization=alert_receive_channel.organization,
key_name=key,
value_name=value,
)
for key, value in labels.items()
]
# sort associated labels by key and value
alert_group_labels.sort(key=lambda label: (label.key_name, label.value_name))
# bulk create associated labels
AlertGroupAssociatedLabel.objects.bulk_create(alert_group_labels)
def _custom_labels(alert_receive_channel: "AlertReceiveChannel", raw_request_data: typing.Any) -> dict[str, str]:
from apps.labels.models import MAX_VALUE_NAME_LENGTH, LabelKeyCache, LabelValueCache
# fetch up-to-date label key names
label_key_names = {
k.id: k.name
for k in LabelKeyCache.objects.filter(
id__in=[label[0] for label in alert_receive_channel.alert_group_labels_custom]
).only("id", "name")
}
# fetch up-to-date label value names
label_value_names = {
v.id: v.name
for v in LabelValueCache.objects.filter(
id__in=[label[1] for label in alert_receive_channel.alert_group_labels_custom if label[1]]
).only("id", "name")
}
rendered_labels = {}
for label in alert_receive_channel.alert_group_labels_custom:
key_id, value_id, template = label
if key_id in label_key_names:
key = label_key_names[key_id]
else:
logger.warning("Label key cache not found. %s", key_id)
continue
if value_id:
if value_id in label_value_names:
rendered_labels[key] = label_value_names[value_id]
else:
logger.warning("Label value cache not found. %s", value_id)
continue
else:
try:
rendered_labels[key] = apply_jinja_template(template, raw_request_data)
except (JinjaTemplateError, JinjaTemplateWarning) as e:
logger.warning("Failed to apply template. %s", e.fallback_message)
continue
labels = {}
for key in rendered_labels:
value = rendered_labels[key]
# check value length
if len(value) > MAX_VALUE_NAME_LENGTH:
logger.warning("Template result value is too long. %s", value)
continue
labels[key] = value
return labels
def _template_labels(alert_receive_channel: "AlertReceiveChannel", raw_request_data: typing.Any) -> dict[str, str]:
from apps.labels.models import MAX_KEY_NAME_LENGTH, MAX_VALUE_NAME_LENGTH
if not alert_receive_channel.alert_group_labels_template:
return {}
try:
rendered = apply_jinja_template(alert_receive_channel.alert_group_labels_template, raw_request_data)
except (JinjaTemplateError, JinjaTemplateWarning) as e:
logger.warning("Failed to apply template. %s", e.fallback_message)
return {}
try:
rendered_labels = json.loads(rendered)
except (TypeError, json.JSONDecodeError):
logger.warning("Failed to parse template result. %s", rendered)
return {}
if not isinstance(rendered_labels, dict):
logger.warning("Template result is not a dict. %s", rendered_labels)
return {}
labels = {}
for key in rendered_labels:
value = rendered_labels[key]
# check value type
if not isinstance(value, LABEL_VALUE_TYPES):
logger.warning("Template result value has invalid type. %s", value)
continue
# convert value to string
value = str(value)
# check key length
if len(key) > MAX_KEY_NAME_LENGTH:
logger.warning("Template result key is too long. %s", key)
continue
# check value length
if len(value) > MAX_VALUE_NAME_LENGTH:
logger.warning("Template result value is too long. %s", value)
continue
labels[key] = value
return labels

View file

@ -10,13 +10,9 @@ if typing.TYPE_CHECKING:
from apps.user_management.models import Organization
MAX_KEY_NAME_LENGTH = 200
MAX_VALUE_NAME_LENGTH = 200
class LabelKeyCache(models.Model):
id = models.CharField(primary_key=True, editable=False, max_length=36)
name = models.CharField(max_length=MAX_KEY_NAME_LENGTH)
name = models.CharField(max_length=200)
organization = models.ForeignKey("user_management.Organization", on_delete=models.CASCADE)
last_synced = models.DateTimeField(auto_now=True)
@ -27,7 +23,7 @@ class LabelKeyCache(models.Model):
class LabelValueCache(models.Model):
id = models.CharField(primary_key=True, editable=False, max_length=36)
name = models.CharField(max_length=MAX_VALUE_NAME_LENGTH)
name = models.CharField(max_length=200)
key = models.ForeignKey("labels.LabelKeyCache", on_delete=models.CASCADE, related_name="values")
last_synced = models.DateTimeField(auto_now=True)
@ -133,8 +129,8 @@ class AlertGroupAssociatedLabel(models.Model):
"user_management.Organization", on_delete=models.CASCADE, related_name="alert_group_labels"
)
key_name = models.CharField(max_length=MAX_KEY_NAME_LENGTH)
value_name = models.CharField(max_length=MAX_VALUE_NAME_LENGTH)
key_name = models.CharField(max_length=200)
value_name = models.CharField(max_length=200)
class Meta:
constraints = [

View file

@ -3,13 +3,9 @@ from unittest import mock
import pytest
from apps.alerts.models import Alert
from apps.labels.models import MAX_KEY_NAME_LENGTH, MAX_VALUE_NAME_LENGTH
TOO_LONG_KEY_NAME = "k" * (MAX_KEY_NAME_LENGTH + 1)
TOO_LONG_VALUE_NAME = "v" * (MAX_VALUE_NAME_LENGTH + 1)
@mock.patch("apps.labels.alert_group_labels.is_labels_feature_enabled", return_value=False)
@mock.patch("apps.labels.utils.is_labels_feature_enabled", return_value=False)
@pytest.mark.django_db
def test_assign_labels_feature_flag_disabled(
_, make_organization, make_alert_receive_channel, make_integration_label_association
@ -32,59 +28,22 @@ def test_assign_labels_feature_flag_disabled(
@pytest.mark.django_db
def test_assign_labels(
make_organization,
make_alert_receive_channel,
make_label_key_and_value,
make_label_key,
make_integration_label_association,
):
def test_assign_labels(make_organization, make_alert_receive_channel, make_integration_label_association):
organization = make_organization()
alert_receive_channel = make_alert_receive_channel(organization)
label = make_integration_label_association(organization, alert_receive_channel)
make_integration_label_association(organization, alert_receive_channel, inheritable=False)
# create label repo labels
label_key, label_value = make_label_key_and_value(organization, key_name="a", value_name="b")
label_key_1 = make_label_key(organization=organization, key_name="c")
label_key_2 = make_label_key(organization=organization)
label_key_3 = make_label_key(organization=organization)
# create alert receive channel with all 3 types of labels
alert_receive_channel = make_alert_receive_channel(
organization,
alert_group_labels_custom=[
[label_key.id, label_value.id, None], # plain label
["nonexistent", label_value.id, None], # plain label with nonexistent key ID
[label_key_2.id, "nonexistent", None], # plain label with nonexistent value ID
[label_key_1.id, None, "{{ payload.c }}"], # templated label
[label_key_3.id, None, TOO_LONG_VALUE_NAME], # templated label too long
],
alert_group_labels_template="{{ payload.advanced_template | tojson }}",
)
make_integration_label_association(organization, alert_receive_channel, key_name="e", value_name="f")
# create alert group
alert = Alert.create(
title="the title",
message="the message",
alert_receive_channel=alert_receive_channel,
raw_request_data={
"c": "d",
"advanced_template": {
"g": 123,
"too_long": TOO_LONG_VALUE_NAME,
TOO_LONG_KEY_NAME: "too_long",
"invalid_type": {"test": "test"},
},
"extra": "hi",
},
raw_request_data={},
integration_unique_data={},
image_url=None,
link_to_upstream_details=None,
)
# check alert group labels are assigned correctly, in the lexicographical order
assert [(label.key_name, label.value_name) for label in alert.group.labels.all()] == [
("a", "b"),
("c", "d"),
("e", "f"),
("g", "123"),
]
assert alert.group.labels.count() == 1
assert alert.group.labels.first().key_name == label.key.name
assert alert.group.labels.first().value_name == label.value.name

View file

@ -1,16 +1,13 @@
import logging
import typing
from django.apps import apps # noqa: I251
from django.conf import settings
if typing.TYPE_CHECKING:
from apps.alerts.models import AlertGroup
from apps.alerts.models import AlertGroup, AlertReceiveChannel
from apps.labels.models import AssociatedLabel
from apps.user_management.models import Organization
logger = logging.getLogger(__name__)
LABEL_OUTDATED_TIMEOUT_MINUTES = 30
ASSOCIATED_MODEL_NAME = "AssociatedLabel"
@ -57,10 +54,35 @@ def is_labels_feature_enabled(organization: "Organization") -> bool:
)
def get_label_verbal(obj: typing.Any) -> dict[str, str]:
return {label.key.name: label.value.name for label in obj.labels.all().select_related("key", "value")}
def assign_labels(alert_group: "AlertGroup", alert_receive_channel: "AlertReceiveChannel") -> None:
from apps.labels.models import AlertGroupAssociatedLabel
if not is_labels_feature_enabled(alert_receive_channel.organization):
return
# inherit labels from the integration
alert_group_labels = [
AlertGroupAssociatedLabel(
alert_group=alert_group,
organization=alert_receive_channel.organization,
key_name=label.key.name,
value_name=label.value.name,
)
for label in alert_receive_channel.labels.filter(inheritable=True).select_related("key", "value")
]
AlertGroupAssociatedLabel.objects.bulk_create(alert_group_labels)
def get_alert_group_label_verbal(alert_group: "AlertGroup") -> dict[str, str]:
"""This is different from get_label_verbal because alert group labels store key/value names, not IDs"""
def get_label_verbal(labelable) -> typing.Dict[str, str]:
"""
label_verbal returns dict of labels' key and values names for the given object
"""
return {label.key.name: label.value.name for label in labelable.labels.all().select_related("key", "value")}
def get_alert_group_label_verbal(alert_group: "AlertGroup") -> typing.Dict[str, str]:
"""
get_alert_group_label_verbal returns dict of labels' key and values names for the given alert group.
It's different from get_label_verbal, because AlertGroupAssociated labels store key/value_name, not key/value_id
"""
return {label.key_name: label.value_name for label in alert_group.labels.all()}

View file

@ -1,10 +1,10 @@
from rest_framework import fields, serializers
from apps.alerts.models import AlertReceiveChannel, ChannelFilter, EscalationChain
from apps.api.serializers.alert_receive_channel import valid_jinja_template_for_serializer_method_field
from apps.base.messaging import get_messaging_backend_from_id, get_messaging_backends
from common.api_helpers.custom_fields import OrganizationFilteredPrimaryKeyRelatedField
from common.api_helpers.exceptions import BadRequest
from common.api_helpers.utils import valid_jinja_template_for_serializer_method_field
from common.jinja_templater.apply_jinja_template import JinjaTemplateError
from common.ordered_model.serializer import OrderedModelSerializer
from common.utils import is_regex_valid

View file

@ -248,7 +248,6 @@ ACKNOWLEDGE_CONDITION = "acknowledge_condition"
GROUPING_ID = "grouping_id"
SOURCE_LINK = "source_link"
ROUTE = "route"
ALERT_GROUP_LABELS = "alert_group_labels"
NOTIFICATION_CHANNEL_TO_TEMPLATER_MAP = {
SLACK: AlertSlackTemplater,
@ -265,15 +264,9 @@ for backend_id, backend in get_messaging_backends():
NOTIFICATION_CHANNEL_TO_TEMPLATER_MAP[backend.slug] = backend.get_templater_class()
APPEARANCE_TEMPLATE_NAMES = [TITLE, MESSAGE, IMAGE_URL]
BEHAVIOUR_TEMPLATE_NAMES = [
RESOLVE_CONDITION,
ACKNOWLEDGE_CONDITION,
GROUPING_ID,
SOURCE_LINK,
ROUTE,
ALERT_GROUP_LABELS,
]
ALL_TEMPLATE_NAMES = APPEARANCE_TEMPLATE_NAMES + BEHAVIOUR_TEMPLATE_NAMES
BEHAVIOUR_TEMPLATE_NAMES = [RESOLVE_CONDITION, ACKNOWLEDGE_CONDITION, GROUPING_ID, SOURCE_LINK]
ROUTE_TEMPLATE_NAMES = [ROUTE]
ALL_TEMPLATE_NAMES = APPEARANCE_TEMPLATE_NAMES + BEHAVIOUR_TEMPLATE_NAMES + ROUTE_TEMPLATE_NAMES
class PreviewTemplateException(Exception):
@ -333,6 +326,11 @@ class PreviewTemplateMixin:
templated_attr = apply_jinja_template(template_body, payload=alert_to_template.raw_request_data)
except (JinjaTemplateError, JinjaTemplateWarning) as e:
return Response({"preview": e.fallback_message}, status.HTTP_200_OK)
elif attr_name in ROUTE_TEMPLATE_NAMES:
try:
templated_attr = apply_jinja_template(template_body, payload=alert_to_template.raw_request_data)
except (JinjaTemplateError, JinjaTemplateWarning) as e:
return Response({"preview": e.fallback_message}, status.HTTP_200_OK)
else:
templated_attr = None
response = {"preview": templated_attr}
@ -348,6 +346,8 @@ class PreviewTemplateMixin:
destination = None
if template_param.startswith(tuple(BEHAVIOUR_TEMPLATE_NAMES)):
attr_name = template_param
if template_param.startswith(tuple(ROUTE_TEMPLATE_NAMES)):
attr_name = template_param
elif template_param.startswith(tuple(NOTIFICATION_CHANNEL_OPTIONS)):
for notification_channel in NOTIFICATION_CHANNEL_OPTIONS:
if template_param.startswith(notification_channel):

View file

@ -14,8 +14,6 @@ from rest_framework.request import Request
from apps.schedules.ical_utils import fetch_ical_file
from common.api_helpers.exceptions import BadRequest
from common.jinja_templater import apply_jinja_template
from common.jinja_templater.apply_jinja_template import JinjaTemplateWarning
from common.timezones import raise_exception_if_not_valid_timezone
@ -167,12 +165,3 @@ def check_phone_number_is_valid(phone_number):
def serialize_datetime_as_utc_timestamp(dt: datetime.datetime) -> str:
return dt.strftime("%Y-%m-%dT%H:%M:%S.%fZ")
def valid_jinja_template_for_serializer_method_field(template):
for _, val in template.items():
try:
apply_jinja_template(val, payload={})
except JinjaTemplateWarning:
# Suppress warnings, template may be valid with payload
pass

View file

@ -954,13 +954,7 @@ def webhook_preset_api_setup():
@pytest.fixture
def make_label_key():
def _make_label_key(organization, key_id=None, key_name=None, **kwargs):
if key_id is not None:
kwargs["id"] = key_id
if key_name is not None:
kwargs["name"] = key_name
def _make_label_key(organization, **kwargs):
return LabelKeyFactory(organization=organization, **kwargs)
return _make_label_key
@ -968,13 +962,7 @@ def make_label_key():
@pytest.fixture
def make_label_value():
def _make_label_value(key, value_id=None, value_name=None, **kwargs):
if value_id is not None:
kwargs["id"] = value_id
if value_name is not None:
kwargs["name"] = value_name
def _make_label_value(key, **kwargs):
return LabelValueFactory(key=key, **kwargs)
return _make_label_value
@ -982,9 +970,9 @@ def make_label_value():
@pytest.fixture
def make_label_key_and_value(make_label_key, make_label_value):
def _make_label_key_and_value(organization, key_id=None, key_name=None, value_id=None, value_name=None):
key = make_label_key(organization=organization, key_id=key_id, key_name=key_name)
value = make_label_value(key=key, value_id=value_id, value_name=value_name)
def _make_label_key_and_value(organization):
key = make_label_key(organization=organization)
value = make_label_value(key=key)
return key, value
return _make_label_key_and_value
@ -992,12 +980,8 @@ def make_label_key_and_value(make_label_key, make_label_value):
@pytest.fixture
def make_integration_label_association(make_label_key_and_value):
def _make_integration_label_association(
organization, alert_receive_channel, key_id=None, key_name=None, value_id=None, value_name=None, **kwargs
):
key, value = make_label_key_and_value(
organization, key_id=key_id, key_name=key_name, value_id=value_id, value_name=value_name
)
def _make_integration_label_association(organization, alert_receive_channel, **kwargs):
key, value = make_label_key_and_value(organization)
return AlertReceiveChannelAssociatedLabelFactory(
alert_receive_channel=alert_receive_channel, organization=organization, key=key, value=value, **kwargs
)

View file

@ -115,7 +115,7 @@
"@grafana/data": "^9.2.4",
"@grafana/faro-web-sdk": "^1.0.0-beta4",
"@grafana/faro-web-tracing": "^1.0.0-beta4",
"@grafana/labels": "1.3.4",
"@grafana/labels": "~1.2.1",
"@grafana/runtime": "9.3.0-beta1",
"@grafana/ui": "^9.4.7",
"@opentelemetry/api": "^1.3.0",

View file

@ -1,4 +1,4 @@
import { PlaywrightTestProject, defineConfig, devices } from '@playwright/test';
import { PlaywrightTestConfig, PlaywrightTestProject, defineConfig, devices } from '@playwright/test';
import path from 'path';
/**

View file

@ -22,7 +22,7 @@ const LabelsTooltipBadge: FC<LabelsTooltipBadgeProps> = ({ labels, onClick }) =>
<VerticalGroup spacing="sm">
{labels.map((label) => (
<HorizontalGroup spacing="sm" key={label.key.id}>
<LabelTag label={label.key.name} value={label.value.name} />
<LabelTag label={label.key.name} value={label.value.name} key={label.key.id} />
<Button
size="sm"
icon="filter"

View file

@ -99,7 +99,6 @@ const MonacoEditor: FC<MonacoEditorProps> = (props) => {
height={height}
onEditorDidMount={handleMount}
getSuggestions={useAutoCompleteList ? autoCompleteList : undefined}
containerStyles="u-width-100"
/>
);
};

View file

@ -1,37 +1,18 @@
import React, { ChangeEvent, useCallback, useState } from 'react';
import React, { useState } from 'react';
import { ServiceLabels } from '@grafana/labels';
import {
Button,
Drawer,
Dropdown,
HorizontalGroup,
Icon,
InlineSwitch,
Input,
Label,
Menu,
Tooltip,
VerticalGroup,
} from '@grafana/ui';
import { Button, Drawer, HorizontalGroup, Icon, InlineSwitch, Input, Label, Tooltip, VerticalGroup } from '@grafana/ui';
import cn from 'classnames/bind';
import { observer } from 'mobx-react';
import Collapse from 'components/Collapse/Collapse';
import MonacoEditor, { MONACO_LANGUAGE } from 'components/MonacoEditor/MonacoEditor';
import Text from 'components/Text/Text';
import IntegrationTemplate from 'containers/IntegrationTemplate/IntegrationTemplate';
import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_channel.types';
import { LabelKey } from 'models/label/label.types';
import { useStore } from 'state/useStore';
import { openErrorNotification } from 'utils';
import styles from './IntegrationLabelsForm.module.css';
const cx = cn.bind(styles);
const INPUT_WIDTH = 280;
interface IntegrationLabelsFormProps {
id: AlertReceiveChannel['id'];
onSubmit: () => void;
@ -44,13 +25,9 @@ const IntegrationLabelsForm = observer((props: IntegrationLabelsFormProps) => {
const store = useStore();
const [showTemplateEditor, setShowTemplateEditor] = useState<boolean>(false);
const [customLabelIndexToShowTemplateEditor, setCustomLabelIndexToShowTemplateEditor] = useState<number>(undefined);
const { alertReceiveChannelStore } = store;
const alertReceiveChannel = alertReceiveChannelStore.items[id];
const templates = alertReceiveChannelStore.templates[id];
const [alertGroupLabels, setAlertGroupLabels] = useState(alertReceiveChannel.alert_group_labels);
@ -78,273 +55,51 @@ const IntegrationLabelsForm = observer((props: IntegrationLabelsFormProps) => {
};
return (
<>
<Drawer scrollableContent title="Alert group labels" onClose={onHide} closeOnMaskClick={false} width="640px">
<VerticalGroup spacing="lg">
<VerticalGroup>
<HorizontalGroup spacing="xs" align="flex-start">
<Label>Inherited labels</Label>
<Tooltip content="Labels inherited from integration">
<Icon name="info-circle" className={cx('extra-fields__icon')} />
</Tooltip>
</HorizontalGroup>
{alertReceiveChannel.labels.length ? (
<ul className={cx('labels-list')}>
{alertReceiveChannel.labels.map((label) => (
<li key={label.key.id}>
<HorizontalGroup spacing="xs">
<Input width={INPUT_WIDTH / 8} value={label.key.name} disabled />
<Input width={INPUT_WIDTH / 8} value={label.value.name} disabled />
<InlineSwitch
value={alertGroupLabels.inheritable[label.key.id]}
transparent
onChange={getInheritanceChangeHandler(label.key.id)}
/>
</HorizontalGroup>
</li>
))}
</ul>
) : (
<VerticalGroup>
<Text type="secondary">There are no labels to inherit yet</Text>
<Text type="link" onClick={handleOpenIntegrationSettings} clickable>
Add labels to the integration
</Text>
</VerticalGroup>
)}
</VerticalGroup>
<CustomLabels
alertGroupLabels={alertGroupLabels}
onChange={setAlertGroupLabels}
onShowTemplateEditor={setCustomLabelIndexToShowTemplateEditor}
/>
<Collapse isOpen={false} label="Advanced label templating">
<Drawer scrollableContent title="Alert group labels" onClose={onHide} closeOnMaskClick={false} width="640px">
<VerticalGroup>
<HorizontalGroup spacing="xs" align="flex-start">
<Label>Inherited labels</Label>
<Tooltip content="Labels inherited from integration">
<Icon name="info-circle" className={cx('extra-fields__icon')} />
</Tooltip>
</HorizontalGroup>
<ul className={cx('labels-list')}>
{alertReceiveChannel.labels.length ? (
alertReceiveChannel.labels.map((label) => (
<li key={label.key.id}>
<HorizontalGroup spacing="xs">
<Input width={38} value={label.key.name} disabled />
<Input width={31} value={label.value.name} disabled />
<InlineSwitch
value={alertGroupLabels.inheritable[label.key.id]}
transparent
onChange={getInheritanceChangeHandler(label.key.id)}
/>
</HorizontalGroup>
</li>
))
) : (
<VerticalGroup>
<HorizontalGroup justify="space-between" style={{ marginBottom: '10px' }}>
<Text type="secondary">Jinja2 template to parse all labels at once</Text>
<Button
variant="secondary"
icon="edit"
onClick={() => {
setShowTemplateEditor(true);
}}
/>
</HorizontalGroup>
<MonacoEditor
value={alertGroupLabels.template}
height="200px"
data={{}}
showLineNumbers={false}
language={MONACO_LANGUAGE.jinja2}
onChange={(value) => {
setAlertGroupLabels({ ...alertGroupLabels, template: value });
}}
/>
<Text type="secondary">There are no labels to inherit yet</Text>
<Text type="link" onClick={handleOpenIntegrationSettings} clickable>
Add labels to the integration
</Text>
</VerticalGroup>
</Collapse>
<div className={cx('buttons')}>
<HorizontalGroup justify="flex-end">
<Button variant="secondary" onClick={onHide}>
Close
</Button>
<Button variant="primary" onClick={handleSave}>
Save
</Button>
</HorizontalGroup>
</div>
</VerticalGroup>
</Drawer>
{customLabelIndexToShowTemplateEditor !== undefined && (
<IntegrationTemplate
id={id}
template={{
name: 'alert_group_labels',
displayName: ``,
}}
templates={templates}
templateBody={alertGroupLabels.custom[customLabelIndexToShowTemplateEditor].value.name}
onHide={() => setCustomLabelIndexToShowTemplateEditor(undefined)}
onUpdateTemplates={({ alert_group_labels }) => {
const newCustom = [...alertGroupLabels.custom];
newCustom[customLabelIndexToShowTemplateEditor].value.name = alert_group_labels;
setAlertGroupLabels({
...alertGroupLabels,
custom: newCustom,
});
setCustomLabelIndexToShowTemplateEditor(undefined);
}}
/>
)}
{showTemplateEditor && (
<IntegrationTemplate
id={id}
template={{
name: 'alert_group_labels',
displayName: ``,
}}
templates={templates}
templateBody={alertGroupLabels.template}
onHide={() => setShowTemplateEditor(false)}
onUpdateTemplates={({ alert_group_labels }) => {
setAlertGroupLabels({
...alertGroupLabels,
template: alert_group_labels,
});
setShowTemplateEditor(undefined);
}}
/>
)}
</>
)}
</ul>
<div className={cx('buttons')}>
<HorizontalGroup justify="flex-end">
<Button variant="secondary" onClick={onHide}>
Close
</Button>
<Button variant="primary" onClick={handleSave}>
Save
</Button>
</HorizontalGroup>
</div>
</VerticalGroup>
</Drawer>
);
});
interface CustomLabelsProps {
alertGroupLabels: AlertReceiveChannel['alert_group_labels'];
onChange: (value: AlertReceiveChannel['alert_group_labels']) => void;
onShowTemplateEditor: (index: number) => void;
}
const CustomLabels = (props: CustomLabelsProps) => {
const { alertGroupLabels, onChange, onShowTemplateEditor } = props;
const { labelsStore } = useStore();
const handlePlainLabelAdd = () => {
onChange({
...alertGroupLabels,
custom: [
...alertGroupLabels.custom,
{
key: { id: undefined, name: undefined },
value: { id: undefined, name: undefined },
},
],
});
};
const handleTemplatedLabelAdd = () => {
onChange({
...alertGroupLabels,
custom: [
...alertGroupLabels.custom,
{
key: { id: undefined, name: undefined },
value: { id: null, name: undefined }, // id = null means it's a templated value
},
],
});
};
const cachedOnLoadKeys = useCallback(() => {
let result = undefined;
return async (search?: string) => {
if (!result) {
try {
result = await labelsStore.loadKeys();
} catch (error) {
openErrorNotification('There was an error processing your request. Please try again');
}
}
return result.filter((k) => k.name.toLowerCase().includes(search.toLowerCase()));
};
}, []);
const cachedOnLoadValuesForKey = useCallback(() => {
let result = undefined;
return async (key: string, search?: string) => {
if (!result) {
try {
const { values } = await labelsStore.loadValuesForKey(key, search);
result = values;
} catch (error) {
openErrorNotification('There was an error processing your request. Please try again');
}
}
return result.filter((k) => k.name.toLowerCase().includes(search.toLowerCase()));
};
}, []);
return (
<VerticalGroup>
<HorizontalGroup spacing="xs" align="flex-start">
<Label>Custom labels</Label>
</HorizontalGroup>
<ServiceLabels
isAddingDisabled
loadById
inputWidth={INPUT_WIDTH}
value={alertGroupLabels.custom}
onLoadKeys={cachedOnLoadKeys()}
onLoadValuesForKey={cachedOnLoadValuesForKey()}
onCreateKey={labelsStore.createKey.bind(labelsStore)}
onUpdateKey={labelsStore.updateKey.bind(labelsStore)}
onCreateValue={labelsStore.createValue.bind(labelsStore)}
onUpdateValue={labelsStore.updateKeyValue.bind(labelsStore)}
onUpdateError={(res) => {
if (res?.response?.status === 409) {
openErrorNotification(`Duplicate values are not allowed`);
} else {
openErrorNotification('An error has occurred. Please try again');
}
}}
renderValue={(option, index, renderValueDefault) => {
if (option.value.id === null) {
return (
<Input
placeholder="Jinja2 template"
autoFocus
disabled={!alertGroupLabels.custom[index].key.id}
width={INPUT_WIDTH / 8}
value={option.value.name}
addonAfter={
<Button
variant="secondary"
icon="edit"
onClick={() => {
onShowTemplateEditor(index);
}}
/>
}
onChange={(e: ChangeEvent<HTMLInputElement>) => {
const newCustom = [...alertGroupLabels.custom];
newCustom[index].value.name = e.currentTarget.value;
onChange({ ...alertGroupLabels, custom: newCustom });
}}
/>
);
} else {
return renderValueDefault(option, index);
}
}}
onDataUpdate={(value) => {
onChange({
...alertGroupLabels,
custom: value,
});
}}
/>
<Dropdown
overlay={
<Menu>
<Menu.Item label="Plain label" onClick={handlePlainLabelAdd} />
<Menu.Item label="Templated label" onClick={handleTemplatedLabelAdd} />
</Menu>
}
>
<Button variant="secondary" icon="plus">
Add
</Button>
</Dropdown>
</VerticalGroup>
);
};
export default IntegrationLabelsForm;

View file

@ -5,7 +5,6 @@ import cn from 'classnames/bind';
import { debounce } from 'lodash-es';
import { observer } from 'mobx-react';
import { templateForEdit } from 'components/AlertTemplates/AlertTemplatesForm.config';
import { TemplateForEdit } from 'components/AlertTemplates/CommonAlertTemplatesForm.config';
import CheatSheet from 'components/CheatSheet/CheatSheet';
import {
@ -39,7 +38,7 @@ interface IntegrationTemplateProps {
templates: AlertTemplatesDTO[];
onHide: () => void;
onUpdateTemplates: (values: any) => void;
onUpdateRoute?: (values: any, channelFilterId?: ChannelFilter['id']) => void;
onUpdateRoute: (values: any, channelFilterId?: ChannelFilter['id']) => void;
}
const IntegrationTemplate = observer((props: IntegrationTemplateProps) => {
@ -54,13 +53,11 @@ const IntegrationTemplate = observer((props: IntegrationTemplateProps) => {
const [isRecentAlertGroupExisting, setIsRecentAlertGroupExisting] = useState<boolean>(false);
useEffect(() => {
if (templateForEdit[template.name]) {
const locationParams: any = { template: template.name };
if (template.isRoute) {
locationParams.routeId = channelFilterId;
}
LocationHelper.update(locationParams, 'partial');
const locationParams: any = { template: template.name };
if (template.isRoute) {
locationParams.routeId = channelFilterId;
}
LocationHelper.update(locationParams, 'partial');
}, []);
useEffect(() => {

View file

@ -1,6 +1,6 @@
import React, { forwardRef, useCallback, useImperativeHandle, useState } from 'react';
import { ServiceLabels, ServiceLabelsProps } from '@grafana/labels';
import ServiceLabels, { ServiceLabelsProps } from '@grafana/labels';
import { Field } from '@grafana/ui';
import cn from 'classnames/bind';
import { isEmpty } from 'lodash-es';

View file

@ -49,11 +49,7 @@ export interface AlertReceiveChannel {
allow_delete: boolean;
deleted?: boolean;
labels: LabelKeyValue[];
alert_group_labels: {
inheritable: Record<LabelKeyValue['key']['id'], boolean>;
custom: LabelKeyValue[];
template: string;
};
alert_group_labels: { inheritable: Record<LabelKeyValue['key']['id'], boolean> };
}
export interface AlertReceiveChannelChoice {

View file

@ -1966,10 +1966,10 @@
"@opentelemetry/sdk-trace-web" "^1.8.0"
"@opentelemetry/semantic-conventions" "^1.8.0"
"@grafana/labels@1.3.4":
version "1.3.4"
resolved "https://registry.yarnpkg.com/@grafana/labels/-/labels-1.3.4.tgz#8d9cdd215a80a1da1045d402c037be85d7efd6f5"
integrity sha512-YYCuLGvtrMz7KkbMc6qoNJQr6drDLo6mMI27LcqsTDMHCNO3uJWpzC1Q2Y9MIwctIuTFYhbgfLvIunEegCx6PQ==
"@grafana/labels@~1.2.1":
version "1.2.1"
resolved "https://registry.yarnpkg.com/@grafana/labels/-/labels-1.2.1.tgz#4113d584bf5cd826d011f957cb69c90bd0416ea8"
integrity sha512-Nlqqvjwh0MjWsqnfpYbKdYwByeKSmEpiit5mKd6Mnnbc5Hxb8ORIruMr40lTxxWLEnDfhENcAs6pvlBuIMG7tQ==
dependencies:
"@emotion/css" "^11.11.2"
"@grafana/ui" "^10.0.0"