diff --git a/docs/sources/configure/integrations/outgoing-webhooks/index.md b/docs/sources/configure/integrations/outgoing-webhooks/index.md
index 526d5422..0bd79870 100644
--- a/docs/sources/configure/integrations/outgoing-webhooks/index.md
+++ b/docs/sources/configure/integrations/outgoing-webhooks/index.md
@@ -108,7 +108,7 @@ This setting does not restrict outgoing webhook execution to events from the sel
The type of event that will cause this outgoing webhook to execute. The types of triggers are:
-- [Escalation Step](#escalation-step)
+- [Manual or Escalation Step](#escalation-step)
- [Alert Group Created](#alert-group-created)
- [Acknowledged](#acknowledged)
- [Resolved](#resolved)
@@ -480,6 +480,7 @@ Now the result is correct:
`event.type` `escalation`
This event will trigger when the outgoing webhook is included as a step in an escalation chain.
+Webhooks with this trigger type can also be manually triggered in the context of an alert group in the web UI.
### Alert Group Created
diff --git a/engine/apps/api/tests/test_alert_receive_channel.py b/engine/apps/api/tests/test_alert_receive_channel.py
index 396bb80e..6aaed48c 100644
--- a/engine/apps/api/tests/test_alert_receive_channel.py
+++ b/engine/apps/api/tests/test_alert_receive_channel.py
@@ -2130,7 +2130,7 @@ def _webhook_data(webhook_id=ANY, webhook_name=ANY, webhook_url=ANY, alert_recei
"team": None,
"trigger_template": None,
"trigger_type": "0",
- "trigger_type_name": "Escalation step",
+ "trigger_type_name": "Manual or escalation step",
"url": webhook_url,
"username": None,
}
diff --git a/engine/apps/api/tests/test_webhooks.py b/engine/apps/api/tests/test_webhooks.py
index 101c9412..a3b91096 100644
--- a/engine/apps/api/tests/test_webhooks.py
+++ b/engine/apps/api/tests/test_webhooks.py
@@ -69,7 +69,7 @@ def test_get_list_webhooks(webhook_internal_api_setup, make_custom_webhook, make
},
"trigger_template": None,
"trigger_type": "0",
- "trigger_type_name": "Escalation step",
+ "trigger_type_name": "Manual or escalation step",
"preset": None,
}
]
@@ -113,7 +113,7 @@ def test_get_detail_webhook(webhook_internal_api_setup, make_user_auth_headers):
},
"trigger_template": None,
"trigger_type": "0",
- "trigger_type_name": "Escalation step",
+ "trigger_type_name": "Manual or escalation step",
"preset": None,
}
@@ -161,7 +161,7 @@ def test_get_detail_connected_integration_webhook(
},
"trigger_template": None,
"trigger_type": "0",
- "trigger_type_name": "Escalation step",
+ "trigger_type_name": "Manual or escalation step",
"preset": None,
}
@@ -858,6 +858,90 @@ def test_create_invalid_missing_fields(webhook_internal_api_setup, make_user_aut
assert response.json()["trigger_type"][0] == "This field is required."
+@pytest.mark.django_db
+def test_webhook_filter_by_trigger_type(
+ make_organization_and_user_with_plugin_token,
+ make_custom_webhook,
+ make_user_auth_headers,
+):
+ organization, user, token = make_organization_and_user_with_plugin_token()
+ webhook_on_ack = make_custom_webhook(organization, trigger_type=Webhook.TRIGGER_ACKNOWLEDGE)
+ make_custom_webhook(organization, trigger_type=Webhook.TRIGGER_MANUAL)
+
+ client = APIClient()
+
+ # no filter
+ url = reverse("api-internal:webhooks-list")
+ response = client.get(
+ url,
+ content_type="application/json",
+ **make_user_auth_headers(user, token),
+ )
+ assert response.status_code == status.HTTP_200_OK
+ assert len(response.json()) == 2
+
+ # test filter on type
+ url = reverse("api-internal:webhooks-list")
+ response = client.get(
+ f"{url}?trigger_type={Webhook.TRIGGER_ACKNOWLEDGE}",
+ content_type="application/json",
+ **make_user_auth_headers(user, token),
+ )
+
+ assert response.status_code == status.HTTP_200_OK
+ assert len(response.json()) == 1
+ assert response.json()[0]["id"] == webhook_on_ack.public_primary_key
+
+ # test filter empty results
+ response = client.get(
+ f"{url}?trigger_type={Webhook.TRIGGER_STATUS_CHANGE}",
+ content_type="application/json",
+ **make_user_auth_headers(user, token),
+ )
+ assert len(response.json()) == 0
+
+
+@pytest.mark.django_db
+def test_webhook_filter_by_integration(
+ make_organization_and_user_with_plugin_token,
+ make_alert_receive_channel,
+ make_custom_webhook,
+ make_user_auth_headers,
+):
+ organization, user, token = make_organization_and_user_with_plugin_token()
+ webhook_all = make_custom_webhook(organization)
+ integration = make_alert_receive_channel(organization)
+ webhook_for_integration = make_custom_webhook(organization)
+ webhook_for_integration.filtered_integrations.add(integration)
+ another_integration = make_alert_receive_channel(organization)
+ another_webhook = make_custom_webhook(organization)
+ another_webhook.filtered_integrations.add(another_integration)
+
+ client = APIClient()
+
+ # no filter
+ url = reverse("api-internal:webhooks-list")
+ response = client.get(
+ url,
+ content_type="application/json",
+ **make_user_auth_headers(user, token),
+ )
+ assert response.status_code == status.HTTP_200_OK
+ assert len(response.json()) == 3
+
+ # test filter on integration
+ url = reverse("api-internal:webhooks-list")
+ response = client.get(
+ f"{url}?integration={integration.public_primary_key}",
+ content_type="application/json",
+ **make_user_auth_headers(user, token),
+ )
+ assert response.status_code == status.HTTP_200_OK
+ assert len(response.json()) == 2
+ expected = {webhook_all.public_primary_key, webhook_for_integration.public_primary_key}
+ assert set(w["id"] for w in response.json()) == expected
+
+
@pytest.mark.django_db
def test_webhook_filter_by_labels(
make_organization_and_user_with_plugin_token,
@@ -1079,3 +1163,93 @@ def test_team_not_updated_if_not_in_data(
webhook.refresh_from_db()
assert webhook.team == team
+
+
+@pytest.mark.django_db
+def test_webhook_trigger_manual(
+ make_organization_and_user_with_plugin_token,
+ make_organization,
+ make_alert_receive_channel,
+ make_alert_group,
+ make_custom_webhook,
+ make_user_auth_headers,
+):
+ organization, user, token = make_organization_and_user_with_plugin_token()
+ integration = make_alert_receive_channel(organization)
+ alert_group = make_alert_group(integration)
+ webhook_on_ack = make_custom_webhook(organization, trigger_type=Webhook.TRIGGER_ACKNOWLEDGE)
+ webhook_manual = make_custom_webhook(organization, trigger_type=Webhook.TRIGGER_MANUAL)
+
+ client = APIClient()
+ url = reverse("api-internal:webhooks-trigger-manual", kwargs={"pk": webhook_manual.public_primary_key})
+ data = {"alert_group": alert_group.public_primary_key}
+
+ # success
+ with patch("apps.api.views.webhooks.execute_webhook") as mock_execute:
+ response = client.post(
+ url,
+ data=json.dumps(data),
+ content_type="application/json",
+ **make_user_auth_headers(user, token),
+ )
+ assert response.status_code == status.HTTP_200_OK
+ mock_execute.apply_async.assert_called_once_with(
+ (webhook_manual.pk, alert_group.pk, user.pk, None), kwargs={"trigger_type": Webhook.TRIGGER_MANUAL}
+ )
+
+ # filtering integration
+ webhook_manual.filtered_integrations.add(integration)
+ with patch("apps.api.views.webhooks.execute_webhook") as mock_execute:
+ response = client.post(
+ url,
+ data=json.dumps(data),
+ content_type="application/json",
+ **make_user_auth_headers(user, token),
+ )
+ assert response.status_code == status.HTTP_200_OK
+ mock_execute.apply_async.assert_called_once_with(
+ (webhook_manual.pk, alert_group.pk, user.pk, None), kwargs={"trigger_type": Webhook.TRIGGER_MANUAL}
+ )
+
+ # exclude integration
+ another_integration = make_alert_receive_channel(organization)
+ webhook_manual.filtered_integrations.set([another_integration])
+ with patch("apps.api.views.webhooks.execute_webhook") as mock_execute:
+ response = client.post(
+ url,
+ data=json.dumps(data),
+ content_type="application/json",
+ **make_user_auth_headers(user, token),
+ )
+ assert response.status_code == status.HTTP_404_NOT_FOUND
+ assert mock_execute.apply_async.call_count == 0
+
+ # invalid trigger type
+ url = reverse("api-internal:webhooks-trigger-manual", kwargs={"pk": webhook_on_ack.public_primary_key})
+ data = {"alert_group": alert_group.public_primary_key}
+
+ with patch("apps.api.views.webhooks.execute_webhook") as mock_execute:
+ response = client.post(
+ url,
+ data=json.dumps(data),
+ content_type="application/json",
+ **make_user_auth_headers(user, token),
+ )
+ assert response.status_code == status.HTTP_400_BAD_REQUEST
+ assert mock_execute.apply_async.call_count == 0
+
+ # alert group from different org
+ another_org = make_organization()
+ another_org_integration = make_alert_receive_channel(another_org)
+ another_org_alert_group = make_alert_group(another_org_integration)
+ url = reverse("api-internal:webhooks-trigger-manual", kwargs={"pk": webhook_manual.public_primary_key})
+ data = {"alert_group": another_org_alert_group.public_primary_key}
+ with patch("apps.api.views.webhooks.execute_webhook") as mock_execute:
+ response = client.post(
+ url,
+ data=json.dumps(data),
+ content_type="application/json",
+ **make_user_auth_headers(user, token),
+ )
+ assert response.status_code == status.HTTP_404_NOT_FOUND
+ assert mock_execute.apply_async.call_count == 0
diff --git a/engine/apps/api/views/webhooks.py b/engine/apps/api/views/webhooks.py
index f32b31ff..d249ef87 100644
--- a/engine/apps/api/views/webhooks.py
+++ b/engine/apps/api/views/webhooks.py
@@ -3,7 +3,8 @@ from dataclasses import asdict
from django.core.exceptions import ObjectDoesNotExist
from django_filters import rest_framework as filters
-from rest_framework import status
+from drf_spectacular.utils import extend_schema, inline_serializer
+from rest_framework import serializers, status
from rest_framework.decorators import action
from rest_framework.exceptions import NotFound
from rest_framework.filters import SearchFilter
@@ -11,6 +12,7 @@ from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet
+from apps.alerts.models import AlertGroup, AlertReceiveChannel
from apps.api.label_filtering import parse_label_query
from apps.api.permissions import RBACPermission
from apps.api.serializers.webhook import WebhookResponseSerializer, WebhookSerializer
@@ -19,9 +21,15 @@ from apps.auth_token.auth import PluginAuthentication
from apps.labels.utils import is_labels_feature_enabled
from apps.webhooks.models import Webhook, WebhookResponse
from apps.webhooks.presets.preset_options import WebhookPresetOptions
+from apps.webhooks.tasks import execute_webhook
from apps.webhooks.utils import apply_jinja_template_for_json
from common.api_helpers.exceptions import BadRequest
-from common.api_helpers.filters import ByTeamModelFieldFilterMixin, ModelFieldFilterMixin, TeamModelMultipleChoiceFilter
+from common.api_helpers.filters import (
+ ByTeamModelFieldFilterMixin,
+ ModelFieldFilterMixin,
+ MultipleChoiceCharFilter,
+ TeamModelMultipleChoiceFilter,
+)
from common.api_helpers.mixins import PublicPrimaryKeyMixin, TeamFilteringMixin
from common.insight_log import EntityEvent, write_resource_insight_log
from common.jinja_templater.apply_jinja_template import JinjaTemplateError, JinjaTemplateWarning
@@ -38,8 +46,30 @@ WEBHOOK_TRIGGER_DATA = "data"
WEBHOOK_TEMPLATE_NAMES = [WEBHOOK_URL, WEBHOOK_HEADERS, WEBHOOK_TRIGGER_TEMPLATE, WEBHOOK_TRIGGER_DATA]
+def get_integration_queryset(request):
+ if request is None:
+ return AlertReceiveChannel.objects.none()
+
+ return AlertReceiveChannel.objects_with_maintenance.filter(organization=request.user.organization)
+
+
class WebhooksFilter(ByTeamModelFieldFilterMixin, ModelFieldFilterMixin, filters.FilterSet):
team = TeamModelMultipleChoiceFilter()
+ trigger_type = filters.MultipleChoiceFilter(choices=Webhook.TRIGGER_TYPES)
+ integration = MultipleChoiceCharFilter(
+ field_name="filtered_integrations",
+ queryset=get_integration_queryset,
+ to_field_name="public_primary_key",
+ method="filter_integration",
+ )
+
+ def filter_integration(self, queryset, name, value):
+ if not value:
+ return queryset
+ lookup_kwargs = {f"{name}__in": value}
+ # include webhooks without filtered_integrations set (ie. apply to all integrations)
+ queryset = queryset.filter(**lookup_kwargs) | queryset.filter(filtered_integrations__isnull=True)
+ return queryset
class WebhooksView(TeamFilteringMixin, PublicPrimaryKeyMixin[Webhook], ModelViewSet):
@@ -58,6 +88,7 @@ class WebhooksView(TeamFilteringMixin, PublicPrimaryKeyMixin[Webhook], ModelView
"responses": [RBACPermission.Permissions.OUTGOING_WEBHOOKS_READ],
"preview_template": [RBACPermission.Permissions.OUTGOING_WEBHOOKS_WRITE],
"preset_options": [RBACPermission.Permissions.OUTGOING_WEBHOOKS_READ],
+ "trigger_manual": [RBACPermission.Permissions.OUTGOING_WEBHOOKS_READ],
}
model = Webhook
@@ -138,6 +169,19 @@ class WebhooksView(TeamFilteringMixin, PublicPrimaryKeyMixin[Webhook], ModelView
return obj
+ @extend_schema(
+ responses=inline_serializer(
+ name="WebhookFilters",
+ fields={
+ "name": serializers.CharField(),
+ "display_name": serializers.CharField(required=False),
+ "type": serializers.CharField(),
+ "href": serializers.CharField(),
+ "global": serializers.BooleanField(required=False),
+ },
+ many=True,
+ )
+ )
@action(methods=["get"], detail=False)
def filters(self, request):
api_root = "/api/internal/v1/"
@@ -150,6 +194,12 @@ class WebhooksView(TeamFilteringMixin, PublicPrimaryKeyMixin[Webhook], ModelView
"href": api_root + "teams/",
"global": True,
},
+ {
+ "name": "trigger_type",
+ "type": "options",
+ "options": [{"display_name": label, "value": value} for value, label in Webhook.TRIGGER_TYPES],
+ },
+ {"name": "integration", "type": "options", "href": api_root + "alert_receive_channels/?filters=true"},
]
if is_labels_feature_enabled(self.request.auth.organization):
@@ -163,8 +213,10 @@ class WebhooksView(TeamFilteringMixin, PublicPrimaryKeyMixin[Webhook], ModelView
return Response(filter_options)
+ @extend_schema(responses=WebhookResponseSerializer(many=True))
@action(methods=["get"], detail=True)
def responses(self, request, pk):
+ """Return recent responses data for the webhook."""
if pk == NEW_WEBHOOK_PK:
return Response([], status=status.HTTP_200_OK)
@@ -175,8 +227,25 @@ class WebhooksView(TeamFilteringMixin, PublicPrimaryKeyMixin[Webhook], ModelView
response_serializer = WebhookResponseSerializer(queryset, many=True)
return Response(response_serializer.data)
+ @extend_schema(
+ request=inline_serializer(
+ name="WebhookPreviewTemplateRequest",
+ fields={
+ "template_body": serializers.CharField(required=False, allow_null=True),
+ "template_name": serializers.CharField(required=False, allow_null=True),
+ "payload": serializers.DictField(required=False, allow_null=True),
+ },
+ ),
+ responses=inline_serializer(
+ name="WebhookPreviewTemplateResponse",
+ fields={
+ "preview": serializers.CharField(allow_null=True),
+ },
+ ),
+ )
@action(methods=["post"], detail=True)
def preview_template(self, request, pk):
+ """Return webhook template preview."""
if pk != NEW_WEBHOOK_PK:
self.get_object() # Check webhook exists
@@ -209,7 +278,61 @@ class WebhooksView(TeamFilteringMixin, PublicPrimaryKeyMixin[Webhook], ModelView
response = {"preview": result}
return Response(response, status=status.HTTP_200_OK)
+ @extend_schema(
+ responses={
+ status.HTTP_200_OK: inline_serializer(
+ name="WebhookPresetOptions",
+ fields={
+ "id": serializers.CharField(),
+ "name": serializers.CharField(),
+ "logo": serializers.CharField(),
+ "description": serializers.CharField(),
+ "controlled_fields": serializers.ListField(child=serializers.CharField()),
+ },
+ )
+ },
+ )
@action(methods=["get"], detail=False)
def preset_options(self, request):
+ """Return available webhook preset options."""
result = [asdict(preset) for preset in WebhookPresetOptions.WEBHOOK_PRESET_CHOICES]
return Response(result)
+
+ @extend_schema(
+ request=inline_serializer(
+ name="WebhookTriggerManual",
+ fields={
+ "alert_group": serializers.CharField(),
+ },
+ ),
+ responses={status.HTTP_200_OK: None},
+ )
+ @action(methods=["post"], detail=True)
+ def trigger_manual(self, request, pk):
+ """Trigger specified webhook in the context of the given alert group."""
+ user = self.request.user
+ organization = self.request.auth.organization
+ webhook = self.get_object()
+ if webhook.trigger_type != Webhook.TRIGGER_MANUAL:
+ raise BadRequest(detail={"trigger_type": "This webhook is not manually triggerable."})
+
+ alert_group_ppk = request.data.get("alert_group")
+ if not alert_group_ppk:
+ raise BadRequest(detail={"alert_group": "This field is required."})
+
+ alert_groups = AlertGroup.objects.filter(
+ channel__organization=organization,
+ public_primary_key=alert_group_ppk,
+ )
+ # check for filtered integrations
+ if webhook.filtered_integrations.exists():
+ alert_groups = alert_groups.filter(channel_id__in=webhook.filtered_integrations.all())
+ try:
+ alert_group = alert_groups.get()
+ except ObjectDoesNotExist:
+ raise NotFound
+
+ execute_webhook.apply_async(
+ (webhook.pk, alert_group.pk, user.pk, None), kwargs={"trigger_type": Webhook.TRIGGER_MANUAL}
+ )
+ return Response(status=status.HTTP_200_OK)
diff --git a/engine/apps/webhooks/migrations/0008_auto_20230712_1613.py b/engine/apps/webhooks/migrations/0008_auto_20230712_1613.py
index 4d4a107c..0421630e 100644
--- a/engine/apps/webhooks/migrations/0008_auto_20230712_1613.py
+++ b/engine/apps/webhooks/migrations/0008_auto_20230712_1613.py
@@ -30,7 +30,7 @@ def convert_custom_button_to_webhook(apps, schema_editor):
username=cb.user,
password=cb.password,
authorization_header=cb.authorization_header,
- trigger_type=Webhook.TRIGGER_ESCALATION_STEP,
+ trigger_type=Webhook.TRIGGER_MANUAL,
forward_all=cb.forward_whole_payload,
data=cb.data,
)
diff --git a/engine/apps/webhooks/migrations/0017_alter_webhook_trigger_type_and_more.py b/engine/apps/webhooks/migrations/0017_alter_webhook_trigger_type_and_more.py
new file mode 100644
index 00000000..b4dc498b
--- /dev/null
+++ b/engine/apps/webhooks/migrations/0017_alter_webhook_trigger_type_and_more.py
@@ -0,0 +1,23 @@
+# Generated by Django 4.2.15 on 2024-08-29 17:48
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('webhooks', '0016_auto_20240402_1341'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='webhook',
+ name='trigger_type',
+ field=models.IntegerField(choices=[(0, 'Manual or escalation step'), (1, 'Alert Group Created'), (2, 'Acknowledged'), (3, 'Resolved'), (4, 'Silenced'), (5, 'Unsilenced'), (6, 'Unresolved'), (7, 'Unacknowledged'), (8, 'Status change')], default=0, null=True),
+ ),
+ migrations.AlterField(
+ model_name='webhookresponse',
+ name='trigger_type',
+ field=models.IntegerField(choices=[(0, 'Manual or escalation step'), (1, 'Alert Group Created'), (2, 'Acknowledged'), (3, 'Resolved'), (4, 'Silenced'), (5, 'Unsilenced'), (6, 'Unresolved'), (7, 'Unacknowledged'), (8, 'Status change')]),
+ ),
+ ]
diff --git a/engine/apps/webhooks/models/webhook.py b/engine/apps/webhooks/models/webhook.py
index 15cbd4bf..9d65a5d7 100644
--- a/engine/apps/webhooks/models/webhook.py
+++ b/engine/apps/webhooks/models/webhook.py
@@ -79,7 +79,7 @@ class Webhook(models.Model):
objects_with_deleted = models.Manager()
(
- TRIGGER_ESCALATION_STEP,
+ TRIGGER_MANUAL,
TRIGGER_ALERT_GROUP_CREATED,
TRIGGER_ACKNOWLEDGE,
TRIGGER_RESOLVE,
@@ -92,7 +92,7 @@ class Webhook(models.Model):
# Must be the same order as previous
TRIGGER_TYPES = (
- (TRIGGER_ESCALATION_STEP, "Escalation step"),
+ (TRIGGER_MANUAL, "Manual or escalation step"),
(TRIGGER_ALERT_GROUP_CREATED, "Alert Group Created"),
(TRIGGER_ACKNOWLEDGE, "Acknowledged"),
(TRIGGER_RESOLVE, "Resolved"),
@@ -114,7 +114,7 @@ class Webhook(models.Model):
}
PUBLIC_TRIGGER_TYPES_MAP = {
- TRIGGER_ESCALATION_STEP: "escalation",
+ TRIGGER_MANUAL: "escalation",
TRIGGER_ALERT_GROUP_CREATED: "alert group created",
TRIGGER_ACKNOWLEDGE: "acknowledge",
TRIGGER_RESOLVE: "resolve",
@@ -158,7 +158,7 @@ class Webhook(models.Model):
data = models.TextField(null=True, default=None)
forward_all = models.BooleanField(default=True)
http_method = models.CharField(max_length=32, default="POST", null=True)
- trigger_type = models.IntegerField(choices=TRIGGER_TYPES, default=TRIGGER_ESCALATION_STEP, null=True)
+ trigger_type = models.IntegerField(choices=TRIGGER_TYPES, default=TRIGGER_MANUAL, null=True)
is_webhook_enabled = models.BooleanField(null=True, default=True)
# NOTE: integration_filter is deprecated (to be removed), use filtered_integrations instead
integration_filter = models.JSONField(default=None, null=True, blank=True)
diff --git a/engine/apps/webhooks/presets/simple.py b/engine/apps/webhooks/presets/simple.py
index ab62f6d3..57453e19 100644
--- a/engine/apps/webhooks/presets/simple.py
+++ b/engine/apps/webhooks/presets/simple.py
@@ -27,7 +27,7 @@ class SimpleWebhookPreset(WebhookPreset):
def override_parameters_before_save(self, webhook: Webhook):
webhook.http_method = "POST"
- webhook.trigger_type = Webhook.TRIGGER_ESCALATION_STEP
+ webhook.trigger_type = Webhook.TRIGGER_MANUAL
webhook.forward_all = True
def override_parameters_at_runtime(self, webhook: Webhook):
diff --git a/engine/apps/webhooks/tasks/trigger_webhook.py b/engine/apps/webhooks/tasks/trigger_webhook.py
index ea89b2e2..803beb9f 100644
--- a/engine/apps/webhooks/tasks/trigger_webhook.py
+++ b/engine/apps/webhooks/tasks/trigger_webhook.py
@@ -8,6 +8,7 @@ import requests
from celery.utils.log import get_task_logger
from django.conf import settings
from django.db.models import Prefetch
+from django.utils import timezone
from apps.alerts.models import AlertGroup, AlertGroupLogRecord, EscalationPolicy
from apps.base.models import UserNotificationPolicyLogRecord
@@ -42,7 +43,7 @@ TRIGGER_TYPE_TO_LABEL = {
Webhook.TRIGGER_SILENCE: "silence",
Webhook.TRIGGER_UNSILENCE: "unsilence",
Webhook.TRIGGER_UNRESOLVE: "unresolve",
- Webhook.TRIGGER_ESCALATION_STEP: "escalation",
+ Webhook.TRIGGER_MANUAL: "escalation",
Webhook.TRIGGER_UNACKNOWLEDGE: "unacknowledge",
Webhook.TRIGGER_STATUS_CHANGE: "status change",
}
@@ -106,6 +107,8 @@ def _build_payload(
elif payload_trigger_type == Webhook.TRIGGER_SILENCE:
event["time"] = _isoformat_date(alert_group.silenced_at)
event["until"] = _isoformat_date(alert_group.silenced_until)
+ elif payload_trigger_type == Webhook.TRIGGER_MANUAL:
+ event["time"] = _isoformat_date(timezone.now())
# include latest response data per webhook in the event input data
# exclude past responses from webhook being executed
@@ -248,8 +251,9 @@ def execute_webhook(webhook_pk, alert_group_id, user_id, escalation_policy_id, t
triggered, status, error, exception = make_request(webhook, alert_group, data)
# create response entry only if webhook was triggered
+ response = None
if triggered:
- WebhookResponse.objects.create(
+ response = WebhookResponse.objects.create(
alert_group=alert_group,
trigger_type=trigger_type or webhook.trigger_type,
**status,
@@ -266,6 +270,9 @@ def execute_webhook(webhook_pk, alert_group_id, user_id, escalation_policy_id, t
# create log record
error_code = None
log_type = AlertGroupLogRecord.TYPE_CUSTOM_WEBHOOK_TRIGGERED
+ trigger_log = TRIGGER_TYPE_TO_LABEL[webhook.trigger_type]
+ if webhook.trigger_type == Webhook.TRIGGER_MANUAL and escalation_policy is None:
+ trigger_log = None # triggered manually
reason = str(status["status_code"])
if error is not None:
log_type = AlertGroupLogRecord.TYPE_ESCALATION_FAILED
@@ -281,7 +288,8 @@ def execute_webhook(webhook_pk, alert_group_id, user_id, escalation_policy_id, t
step_specific_info={
"webhook_name": webhook.name,
"webhook_id": webhook.public_primary_key,
- "trigger": TRIGGER_TYPE_TO_LABEL[webhook.trigger_type],
+ "trigger": trigger_log,
+ "response_id": response.pk if response else None,
},
escalation_policy=escalation_policy,
escalation_policy_step=step,
diff --git a/engine/apps/webhooks/tests/test_trigger_webhook.py b/engine/apps/webhooks/tests/test_trigger_webhook.py
index 85b3966a..152020f6 100644
--- a/engine/apps/webhooks/tests/test_trigger_webhook.py
+++ b/engine/apps/webhooks/tests/test_trigger_webhook.py
@@ -269,6 +269,7 @@ def test_execute_webhook_ok(
"trigger": "acknowledge",
"webhook_id": webhook.public_primary_key,
"webhook_name": webhook.name,
+ "response_id": log.id,
}
assert log_record.step_specific_info == expected_info
assert log_record.escalation_policy is None
@@ -296,7 +297,7 @@ def test_execute_webhook_via_escalation_ok(
organization=organization,
url="https://something/{{ alert_group_id }}/",
http_method="POST",
- trigger_type=Webhook.TRIGGER_ESCALATION_STEP,
+ trigger_type=Webhook.TRIGGER_MANUAL,
trigger_template="{{{{ alert_group.integration_id == '{}' }}}}".format(
alert_receive_channel.public_primary_key
),
@@ -325,6 +326,7 @@ def test_execute_webhook_via_escalation_ok(
"trigger": "escalation",
"webhook_id": webhook.public_primary_key,
"webhook_name": webhook.name,
+ "response_id": webhook.responses.all()[0].id,
}
assert log_record.step_specific_info == expected_info
assert log_record.escalation_policy == escalation_policy
@@ -728,6 +730,7 @@ def test_execute_webhook_errors(
"trigger": "resolve",
"webhook_id": webhook.public_primary_key,
"webhook_name": webhook.name,
+ "response_id": log.id,
}
assert log_record.step_specific_info == expected_info
assert log_record.reason == expected_error
@@ -779,6 +782,7 @@ def test_execute_webhook_ssl_error(
"trigger": "resolve",
"webhook_id": webhook.public_primary_key,
"webhook_name": webhook.name,
+ "response_id": webhook.responses.all()[0].id,
}
assert log_record.reason == expected_error
assert (
diff --git a/engine/settings/base.py b/engine/settings/base.py
index 2957c2ab..d2bac3ff 100644
--- a/engine/settings/base.py
+++ b/engine/settings/base.py
@@ -346,6 +346,7 @@ SPECTACULAR_INCLUDED_PATHS = [
"/features",
"/alertgroups",
"/alert_receive_channels",
+ "/webhooks",
# current user endpoint 👇, without trailing slash we pick-up /user_group endpoints, which we don't want for now
"/user/",
"/users",
diff --git a/grafana-plugin/package.json b/grafana-plugin/package.json
index 7a5cd856..b5da7fd1 100644
--- a/grafana-plugin/package.json
+++ b/grafana-plugin/package.json
@@ -106,7 +106,7 @@
"lodash-es": "^4.17.21",
"mailslurp-client": "^15.14.1",
"moment-timezone": "0.5.45",
- "openapi-typescript": "^7.0.0-next.4",
+ "openapi-typescript": "^7.4.0",
"postcss-loader": "^7.0.1",
"prettier": "^2.8.7",
"react-test-renderer": "^18.0.2",
diff --git a/grafana-plugin/pnpm-lock.yaml b/grafana-plugin/pnpm-lock.yaml
index 54791ebf..1264b592 100644
--- a/grafana-plugin/pnpm-lock.yaml
+++ b/grafana-plugin/pnpm-lock.yaml
@@ -320,8 +320,8 @@ importers:
specifier: 0.5.45
version: 0.5.45
openapi-typescript:
- specifier: ^7.0.0-next.4
- version: 7.3.3(typescript@5.1.6)
+ specifier: ^7.4.0
+ version: 7.4.0(typescript@5.1.6)
postcss-loader:
specifier: ^7.0.1
version: 7.3.4(postcss@8.4.43)(typescript@5.1.6)(webpack@5.94.0(@swc/core@1.7.22(@swc/helpers@0.5.12))(webpack-cli@5.1.4))
@@ -2151,6 +2151,9 @@ packages:
change-case@4.1.2:
resolution: {integrity: sha512-bSxY2ws9OtviILG1EiY5K7NNxkqg/JnRnFxLtKQ96JaviiIxi7djMrSd0ECT9AC+lttClmYwKw53BWpOMblo7A==}
+ change-case@5.4.4:
+ resolution: {integrity: sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==}
+
char-regex@1.0.2:
resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==}
engines: {node: '>=10'}
@@ -4475,8 +4478,8 @@ packages:
openapi-typescript-helpers@0.0.5:
resolution: {integrity: sha512-MRffg93t0hgGZbYTxg60hkRIK2sRuEOHEtCUgMuLgbCC33TMQ68AmxskzUlauzZYD47+ENeGV/ElI7qnWqrAxA==}
- openapi-typescript@7.3.3:
- resolution: {integrity: sha512-NkUBI8fr5mg/3s001UPfUiBpKmHtSjkvFQO/IipCrQal5d5nGFoev1OXdxr7J9PHTswrAqU2hKdpoCL6OnammA==}
+ openapi-typescript@7.4.0:
+ resolution: {integrity: sha512-u4iVuTGkzKG4rHFUMA/IFXTks9tYVQzkowZsScMOdzJSvIF10qSNySWHTwnN2fD+MEeWFAM8i1f3IUBlgS92eQ==}
hasBin: true
peerDependencies:
typescript: ^5.x
@@ -8757,6 +8760,8 @@ snapshots:
snake-case: 3.0.4
tslib: 2.5.3
+ change-case@5.4.4: {}
+
char-regex@1.0.2: {}
chokidar@3.6.0:
@@ -11516,10 +11521,11 @@ snapshots:
openapi-typescript-helpers@0.0.5: {}
- openapi-typescript@7.3.3(typescript@5.1.6):
+ openapi-typescript@7.4.0(typescript@5.1.6):
dependencies:
'@redocly/openapi-core': 1.22.1(supports-color@9.4.0)
ansi-colors: 4.1.3
+ change-case: 5.4.4
parse-json: 8.1.0
supports-color: 9.4.0
typescript: 5.1.6
diff --git a/grafana-plugin/src/components/ExtensionLinkMenu/ExtensionLinkDropdown.tsx b/grafana-plugin/src/components/ExtensionLinkMenu/ExtensionLinkDropdown.tsx
index 29b0545e..c432dc23 100644
--- a/grafana-plugin/src/components/ExtensionLinkMenu/ExtensionLinkDropdown.tsx
+++ b/grafana-plugin/src/components/ExtensionLinkMenu/ExtensionLinkDropdown.tsx
@@ -1,20 +1,24 @@
-import React, { ReactElement, useMemo, useState } from 'react';
+import React, { ReactElement, useEffect, useMemo, useState } from 'react';
-import { PluginExtensionLink } from '@grafana/data';
+import { PluginExtensionLink, SelectableValue } from '@grafana/data';
import {
type GetPluginExtensionsOptions,
getPluginLinkExtensions,
usePluginLinks as originalUsePluginLinks,
} from '@grafana/runtime';
-import { Dropdown, ToolbarButton } from '@grafana/ui';
+import { Button, Dropdown, Modal, Select, Stack, ToolbarButton } from '@grafana/ui';
import { OnCallPluginExtensionPoints } from 'app-types';
+import { StackSize } from 'helpers/consts';
+import { observer } from 'mobx-react';
+import { ActionKey } from 'models/loader/action-keys';
import { ApiSchemas } from 'network/oncall-api/api.types';
+import { useStore } from 'state/useStore';
import { ExtensionLinkMenu } from './ExtensionLinkMenu';
interface Props {
- incident: ApiSchemas['AlertGroup'];
+ alertGroup: ApiSchemas['AlertGroup'];
extensionPointId: OnCallPluginExtensionPoints;
declareIncidentLink?: string;
grafanaIncidentId: string | null;
@@ -24,36 +28,116 @@ interface Props {
const usePluginLinks = originalUsePluginLinks === undefined ? usePluginLinksFallback : originalUsePluginLinks;
export function ExtensionLinkDropdown({
- incident,
+ alertGroup,
extensionPointId,
declareIncidentLink,
grafanaIncidentId,
}: Props): ReactElement | null {
const [isOpen, setIsOpen] = useState(false);
- const context = useExtensionPointContext(incident);
+ const [isTriggerWebhookModalOpen, setIsTriggerWebhookModalOpen] = useState(false);
+ const context = useExtensionPointContext(alertGroup);
const { links, isLoading } = usePluginLinks({ context, extensionPointId, limitPerPlugin: 3 });
- if (links.length === 0 || isLoading) {
+ if (isLoading) {
return null;
}
+ const onOpenTriggerWebhookModal = async () => {
+ setIsOpen(false);
+ setIsTriggerWebhookModalOpen(true);
+ };
+
const menu = (
);
return (
-
-
- Actions
-
-
+
+
+
+
+
+ Actions
+
+
+
);
}
+interface TriggerManualWebhookModalProps {
+ alertGroup: ApiSchemas['AlertGroup'];
+ isModalOpen: boolean;
+ setIsModalOpen: (isOpen: boolean) => void;
+}
+
+const TriggerManualWebhookModal = observer(
+ ({ isModalOpen, setIsModalOpen, alertGroup }: TriggerManualWebhookModalProps) => {
+ const store = useStore();
+ const [selectedWebhookOption, setSelectedWebhookOption] = useState>(null);
+
+ useEffect(() => {
+ (async () => {
+ if (isModalOpen) {
+ await store.outgoingWebhookStore.updateItems(
+ {
+ trigger_type: 0,
+ integration: alertGroup.alert_receive_channel.id,
+ },
+ true
+ );
+ }
+ })();
+ }, [isModalOpen]);
+
+ return (
+ setIsModalOpen(false)}>
+
+
+
+ );
+
+ async function onTriggerWebhook() {
+ await store.outgoingWebhookStore.triggerManualWebhook(selectedWebhookOption.value, alertGroup.pk);
+ setIsModalOpen(false);
+ setSelectedWebhookOption(null);
+ }
+ }
+);
+
function useExtensionPointContext(incident: ApiSchemas['AlertGroup']): PluginExtensionOnCallAlertGroupContext {
return { alertGroup: incident };
}
diff --git a/grafana-plugin/src/components/ExtensionLinkMenu/ExtensionLinkMenu.tsx b/grafana-plugin/src/components/ExtensionLinkMenu/ExtensionLinkMenu.tsx
index f8f530f8..ecc3ebe9 100644
--- a/grafana-plugin/src/components/ExtensionLinkMenu/ExtensionLinkMenu.tsx
+++ b/grafana-plugin/src/components/ExtensionLinkMenu/ExtensionLinkMenu.tsx
@@ -6,8 +6,13 @@ import { getPluginId } from 'helpers/consts';
import { truncateTitle } from 'helpers/string';
import { PluginBridge, SupportedPlugin } from 'components/PluginBridge/PluginBridge';
+import { RenderConditionally } from 'components/RenderConditionally/RenderConditionally';
type Props = {
+ webhookModal: {
+ onOpenModal: () => void;
+ };
+
extensions: PluginExtensionLink[];
// We require this to be passed in so we can continue to
// create a custom Declare incident link. Once the Incident plugin
@@ -16,34 +21,63 @@ type Props = {
grafanaIncidentId?: string;
};
-export function ExtensionLinkMenu({ extensions, declareIncidentLink, grafanaIncidentId }: Props): ReactElement | null {
+export function ExtensionLinkMenu({
+ extensions,
+ declareIncidentLink,
+ grafanaIncidentId,
+ webhookModal,
+}: Props): ReactElement | null {
const { categorised, uncategorised } = useExtensionLinksByCategory(extensions);
const showDivider = uncategorised.length > 0 && Object.keys(categorised).length > 0;
return (
);
}
+const IRMActionSection: React.FC = ({ webhookModal, extensions, declareIncidentLink, grafanaIncidentId }) => {
+ return (
+
+
+
+ {extensions.length > 0 && (
+
+ )}
+
+ );
+};
+
// This menu item is a temporary workaround for the fact that the Incident plugin doesn't
// register its own extension link.
// TODO: remove this once Incident is definitely registering its own extension link.
-function DeclareIncidentMenuItem({ extensions, declareIncidentLink, grafanaIncidentId }: Props): ReactElement | null {
+function DeclareIncidentMenuItem({
+ extensions,
+ declareIncidentLink,
+ grafanaIncidentId,
+}: Omit): ReactElement | null {
const declareIncidentExtensionLink = extensions.find(
(extension) => extension.pluginId === 'grafana-incident-app' && extension.title === 'Declare incident'
);
@@ -61,7 +95,7 @@ function DeclareIncidentMenuItem({ extensions, declareIncidentLink, grafanaIncid
return (
-
+ <>
{renderItems([
{
type: PluginExtensionTypes.link,
@@ -72,7 +106,7 @@ function DeclareIncidentMenuItem({ extensions, declareIncidentLink, grafanaIncid
pluginId: getPluginId(),
} as Partial,
])}
-
+ >
);
}
diff --git a/grafana-plugin/src/components/Policy/EscalationPolicy.tsx b/grafana-plugin/src/components/Policy/EscalationPolicy.tsx
index c9a145d4..7f341eb9 100644
--- a/grafana-plugin/src/components/Policy/EscalationPolicy.tsx
+++ b/grafana-plugin/src/components/Policy/EscalationPolicy.tsx
@@ -475,7 +475,7 @@ class _EscalationPolicy extends React.Component {
width={'auto'}
filterOptions={(id) => {
const webhook = outgoingWebhookStore.items[id];
- return webhook.trigger_type_name === 'Escalation step';
+ return webhook.trigger_type === '0';
}}
/>
diff --git a/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsModal.tsx b/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsModal.tsx
index 8648d6c9..866480df 100644
--- a/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsModal.tsx
+++ b/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsModal.tsx
@@ -61,20 +61,22 @@ export const ColumnsModal: React.FC = observer(
-
+
+
- {inputRef?.current?.value === '' && (
-
- {availableKeysForSearching.length} {pluralize('item', availableKeysForSearching.length)} available.
- Type to see suggestions
-
- )}
+ {inputRef?.current?.value === '' && (
+
+ {availableKeysForSearching.length} {pluralize('item', availableKeysForSearching.length)} available.
+ Type to see suggestions
+
+ )}
+
{inputRef?.current?.value && searchResults.length && (
diff --git a/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.tsx b/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.tsx
index c7a9bada..5d9d7d97 100644
--- a/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.tsx
+++ b/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.tsx
@@ -147,7 +147,7 @@ export const ColumnsSelectorWrapper: React.FC = obs
id="toggletip-button"
onClick={() => setIsFloatingDisplayOpen(!isFloatingDisplayOpen)}
>
-
+
Columns
diff --git a/grafana-plugin/src/containers/TemplatePreview/TemplatePreview.tsx b/grafana-plugin/src/containers/TemplatePreview/TemplatePreview.tsx
index d993af33..dca26505 100644
--- a/grafana-plugin/src/containers/TemplatePreview/TemplatePreview.tsx
+++ b/grafana-plugin/src/containers/TemplatePreview/TemplatePreview.tsx
@@ -52,9 +52,9 @@ export const TemplatePreview = observer((props: TemplatePreviewProps) => {
templatePage,
} = props;
- const [result, setResult] = useState<{ preview: string | null; is_valid_json_object?: boolean } | undefined>(
- undefined
- );
+ const [result, setResult] = useState<
+ ApiSchemas['WebhookPreviewTemplateResponse'] & { is_valid_json_object?: boolean }
+ >(undefined);
const [conditionalResult, setConditionalResult] = useState({});
const store = useStore();
@@ -62,11 +62,21 @@ export const TemplatePreview = observer((props: TemplatePreviewProps) => {
const handleTemplateBodyChange = useDebouncedCallback(async () => {
try {
- const data = await (templatePage === TemplatePage.Webhooks
- ? outgoingWebhookStore.renderPreview(outgoingWebhookId, templateName, templateBody, payload)
- : alertGroupId
- ? AlertGroupHelper.renderPreview(alertGroupId, templateName, templateBody)
- : AlertReceiveChannelHelper.renderPreview(alertReceiveChannelId, templateName, templateBody, payload));
+ let data: ApiSchemas['WebhookPreviewTemplateResponse'] & { is_valid_json_object?: boolean } = undefined;
+
+ if (templatePage === TemplatePage.Webhooks) {
+ data = await outgoingWebhookStore.renderPreview(outgoingWebhookId, templateName, templateBody, payload);
+ } else if (alertGroupId) {
+ data = await AlertGroupHelper.renderPreview(alertGroupId, templateName, templateBody);
+ } else {
+ data = await AlertReceiveChannelHelper.renderPreview(
+ alertReceiveChannelId,
+ templateName,
+ templateBody,
+ payload
+ );
+ }
+
setResult(data);
if (data?.preview === 'True') {
diff --git a/grafana-plugin/src/models/loader/action-keys.ts b/grafana-plugin/src/models/loader/action-keys.ts
index f89476f1..824ae3c4 100644
--- a/grafana-plugin/src/models/loader/action-keys.ts
+++ b/grafana-plugin/src/models/loader/action-keys.ts
@@ -19,4 +19,6 @@ export enum ActionKey {
FETCH_INTEGRATION_CHANNELS = 'FETCH_INTEGRATION_CHANNELS',
CONNECT_INTEGRATION_CHANNELS = 'CONNECT_INTEGRATION_CHANNELS',
FETCH_INTEGRATIONS_AVAILABLE_FOR_CONNECTION = 'FETCH_INTEGRATIONS_AVAILABLE_FOR_CONNECTION',
+ FETCH_WEBHOOKS = 'FETCH_WEBHOOKS',
+ TRIGGER_MANUAL_WEBHOOK = 'TRIGGER_MANUAL_WEBHOOK',
}
diff --git a/grafana-plugin/src/models/outgoing_webhook/outgoing_webhook.ts b/grafana-plugin/src/models/outgoing_webhook/outgoing_webhook.ts
index e5a36e5f..30ca4638 100644
--- a/grafana-plugin/src/models/outgoing_webhook/outgoing_webhook.ts
+++ b/grafana-plugin/src/models/outgoing_webhook/outgoing_webhook.ts
@@ -1,8 +1,11 @@
+import { AutoLoadingState, WithGlobalNotification } from 'helpers/decorators';
import { action, observable, makeObservable, runInAction } from 'mobx';
import { BaseStore } from 'models/base_store';
+import { ActionKey } from 'models/loader/action-keys';
import { makeRequest } from 'network/network';
import { ApiSchemas } from 'network/oncall-api/api.types';
+import { onCallApi } from 'network/oncall-api/http-client';
import { RootStore } from 'state/rootStore';
import { OutgoingWebhookPreset } from './outgoing_webhook.types';
@@ -64,7 +67,8 @@ export class OutgoingWebhookStore extends BaseStore {
}
@action.bound
- async updateItems(query: any = '') {
+ @AutoLoadingState(ActionKey.FETCH_WEBHOOKS)
+ async updateItems(query: any = '', forceUpdate = false) {
const params = typeof query === 'string' ? { search: query } : query;
const results = await makeRequest(`${this.path}`, {
@@ -73,7 +77,7 @@ export class OutgoingWebhookStore extends BaseStore {
runInAction(() => {
this.items = {
- ...this.items,
+ ...(forceUpdate ? {} : this.items),
...results.reduce(
(acc: { [key: number]: ApiSchemas['Webhook'] }, item: ApiSchemas['Webhook']) => ({
...acc,
@@ -92,6 +96,18 @@ export class OutgoingWebhookStore extends BaseStore {
});
}
+ @action.bound
+ @AutoLoadingState(ActionKey.TRIGGER_MANUAL_WEBHOOK)
+ @WithGlobalNotification({ success: 'Webhook has been triggered successfully.', failure: 'Failed to trigger webhook' })
+ async triggerManualWebhook(id: ApiSchemas['Webhook']['id'], alertGroupId: ApiSchemas['AlertGroup']['pk']) {
+ await onCallApi().POST(`/webhooks/{id}/trigger_manual/`, {
+ params: { path: { id } },
+ body: {
+ alert_group: alertGroupId,
+ },
+ });
+ }
+
getSearchResult = (query = '') => {
if (!this.searchResult[query]) {
return undefined;
@@ -108,11 +124,18 @@ export class OutgoingWebhookStore extends BaseStore {
return result;
}
- async renderPreview(id: ApiSchemas['Webhook']['id'], template_name: string, template_body: string, payload) {
- return await makeRequest(`${this.path}${id}/preview_template/`, {
- method: 'POST',
- data: { template_name, template_body, payload },
- });
+ async renderPreview(
+ id: ApiSchemas['Webhook']['id'],
+ template_name: string,
+ template_body: string,
+ payload: { [key: string]: unknown } = undefined
+ ) {
+ return (
+ await onCallApi().POST('/webhooks/{id}/preview_template/', {
+ params: { path: { id } },
+ body: { template_name, template_body, payload },
+ })
+ ).data;
}
@action.bound
diff --git a/grafana-plugin/src/models/outgoing_webhook/outgoing_webhook.types.ts b/grafana-plugin/src/models/outgoing_webhook/outgoing_webhook.types.ts
index 661404ef..5ba38036 100644
--- a/grafana-plugin/src/models/outgoing_webhook/outgoing_webhook.types.ts
+++ b/grafana-plugin/src/models/outgoing_webhook/outgoing_webhook.types.ts
@@ -20,7 +20,7 @@ export interface OutgoingWebhookPreset {
}
export const WebhookTriggerType = {
- EscalationStep: new KeyValuePair('0', 'Escalation Step'),
+ EscalationStep: new KeyValuePair('0', 'Manual or Escalation Step'),
AlertGroupCreated: new KeyValuePair('1', 'Alert Group Created'),
Acknowledged: new KeyValuePair('2', 'Acknowledged'),
Resolved: new KeyValuePair('3', 'Resolved'),
diff --git a/grafana-plugin/src/network/oncall-api/autogenerated-api.types.d.ts b/grafana-plugin/src/network/oncall-api/autogenerated-api.types.d.ts
index 25c6f076..19836b72 100644
--- a/grafana-plugin/src/network/oncall-api/autogenerated-api.types.d.ts
+++ b/grafana-plugin/src/network/oncall-api/autogenerated-api.types.d.ts
@@ -1347,6 +1347,136 @@ export interface paths {
patch?: never;
trace?: never;
};
+ '/webhooks/': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** @description This mixin returns 403 and {"error_code": "wrong_team", "owner_team": {"name", "id", "email", "avatar_url"}}
+ * in case a requested instance doesn't belong to user's current_team. */
+ get: operations['webhooks_list'];
+ put?: never;
+ /** @description This mixin returns 403 and {"error_code": "wrong_team", "owner_team": {"name", "id", "email", "avatar_url"}}
+ * in case a requested instance doesn't belong to user's current_team. */
+ post: operations['webhooks_create'];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/webhooks/{id}/': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** @description This mixin returns 403 and {"error_code": "wrong_team", "owner_team": {"name", "id", "email", "avatar_url"}}
+ * in case a requested instance doesn't belong to user's current_team. */
+ get: operations['webhooks_retrieve'];
+ /** @description This mixin returns 403 and {"error_code": "wrong_team", "owner_team": {"name", "id", "email", "avatar_url"}}
+ * in case a requested instance doesn't belong to user's current_team. */
+ put: operations['webhooks_update'];
+ post?: never;
+ /** @description This mixin returns 403 and {"error_code": "wrong_team", "owner_team": {"name", "id", "email", "avatar_url"}}
+ * in case a requested instance doesn't belong to user's current_team. */
+ delete: operations['webhooks_destroy'];
+ options?: never;
+ head?: never;
+ /** @description This mixin returns 403 and {"error_code": "wrong_team", "owner_team": {"name", "id", "email", "avatar_url"}}
+ * in case a requested instance doesn't belong to user's current_team. */
+ patch: operations['webhooks_partial_update'];
+ trace?: never;
+ };
+ '/webhooks/{id}/preview_template/': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /** @description Return webhook template preview. */
+ post: operations['webhooks_preview_template_create'];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/webhooks/{id}/responses/': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** @description Return recent responses data for the webhook. */
+ get: operations['webhooks_responses_list'];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/webhooks/{id}/trigger_manual/': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /** @description Trigger specified webhook in the context of the given alert group. */
+ post: operations['webhooks_trigger_manual_create'];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/webhooks/filters/': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** @description This mixin returns 403 and {"error_code": "wrong_team", "owner_team": {"name", "id", "email", "avatar_url"}}
+ * in case a requested instance doesn't belong to user's current_team. */
+ get: operations['webhooks_filters_list'];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/webhooks/preset_options/': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** @description Return available webhook preset options. */
+ get: operations['webhooks_preset_options_retrieve'];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
}
export type webhooks = Record;
export interface components {
@@ -1396,7 +1526,7 @@ export interface components {
silenced_until?: string | null;
/** Format: date-time */
readonly started_at: string;
- readonly related_users: Array;
+ readonly related_users: components['schemas']['UserShort'][];
readonly render_for_web:
| {
title: string;
@@ -1405,22 +1535,22 @@ export interface components {
source_link: string | null;
}
| Record;
- dependent_alert_groups: Array;
+ dependent_alert_groups: components['schemas']['ShortAlertGroup'][];
root_alert_group: components['schemas']['ShortAlertGroup'];
readonly status: number;
/** @description Generate a link for AlertGroup to declare Grafana Incident by click */
readonly declare_incident_link: string;
team: string | null;
grafana_incident_id?: string | null;
- readonly labels: Array;
+ readonly labels: components['schemas']['AlertGroupLabel'][];
readonly permalinks: {
slack: string | null;
slack_app: string | null;
telegram: string | null;
web: string;
};
- readonly alerts: Array;
- readonly render_after_resolve_report_json: Array<{
+ readonly alerts: components['schemas']['Alert'][];
+ readonly render_after_resolve_report_json: {
time: string;
action: string;
/** @enum {string} */
@@ -1433,11 +1563,11 @@ export interface components {
avatar: string;
avatar_full: string;
};
- }>;
+ }[];
readonly slack_permalink: string | null;
/** Format: date-time */
readonly last_alert_at: string;
- readonly paged_users: Array<{
+ readonly paged_users: {
id: number;
username: string;
name: string;
@@ -1445,13 +1575,13 @@ export interface components {
avatar: string;
avatar_full: string;
important: boolean;
- }>;
- readonly external_urls: Array<{
+ }[];
+ readonly external_urls: {
integration: string;
integration_type: string;
external_id: string;
url: string;
- }>;
+ }[];
};
AlertGroupAttach: {
root_alert_group_pk: string;
@@ -1508,7 +1638,7 @@ export interface components {
silenced_until?: string | null;
/** Format: date-time */
readonly started_at: string;
- readonly related_users: Array;
+ readonly related_users: components['schemas']['UserShort'][];
readonly render_for_web:
| {
title: string;
@@ -1517,14 +1647,14 @@ export interface components {
source_link: string | null;
}
| Record;
- dependent_alert_groups: Array;
+ dependent_alert_groups: components['schemas']['ShortAlertGroup'][];
root_alert_group: components['schemas']['ShortAlertGroup'];
readonly status: number;
/** @description Generate a link for AlertGroup to declare Grafana Incident by click */
readonly declare_incident_link: string;
team: string | null;
grafana_incident_id?: string | null;
- readonly labels: Array;
+ readonly labels: components['schemas']['AlertGroupLabel'][];
readonly permalinks: {
slack: string | null;
slack_app: string | null;
@@ -1624,7 +1754,7 @@ export interface components {
readonly is_based_on_alertmanager: boolean;
readonly inbound_email: string;
readonly is_legacy: boolean;
- labels?: Array;
+ labels?: components['schemas']['LabelPair'][];
alert_group_labels?: components['schemas']['IntegrationAlertGroupLabels'];
/** Format: date-time */
readonly alertmanager_v2_migrated_at: string | null;
@@ -1641,15 +1771,15 @@ export interface components {
AlertReceiveChannelConnectedContactPoints: {
uid: string;
name: string;
- contact_points: Array;
+ contact_points: components['schemas']['AlertReceiveChannelConnectedContactPointsInner'][];
};
AlertReceiveChannelConnectedContactPointsInner: {
name: string;
notification_connected: boolean;
};
AlertReceiveChannelConnection: {
- readonly source_alert_receive_channels: Array;
- readonly connected_alert_receive_channels: Array;
+ readonly source_alert_receive_channels: components['schemas']['AlertReceiveChannelSourceChannel'][];
+ readonly connected_alert_receive_channels: components['schemas']['AlertReceiveChannelConnectedChannel'][];
};
AlertReceiveChannelContactPoints: {
uid: string;
@@ -1691,7 +1821,7 @@ export interface components {
readonly is_based_on_alertmanager: boolean;
readonly inbound_email: string;
readonly is_legacy: boolean;
- labels?: Array;
+ labels?: components['schemas']['LabelPair'][];
alert_group_labels?: components['schemas']['IntegrationAlertGroupLabels'];
/** Format: date-time */
readonly alertmanager_v2_migrated_at: string | null;
@@ -1776,7 +1906,7 @@ export interface components {
readonly is_based_on_alertmanager: boolean;
readonly inbound_email: string;
readonly is_legacy: boolean;
- labels?: Array;
+ labels?: components['schemas']['LabelPair'][];
alert_group_labels?: components['schemas']['IntegrationAlertGroupLabels'];
/** Format: date-time */
readonly alertmanager_v2_migrated_at: string | null;
@@ -1795,7 +1925,6 @@ export interface components {
readonly role: components['schemas']['RoleEnum'];
/** Format: uri */
readonly avatar: string;
- /** Format: uri */
readonly avatar_full: string;
timezone?: string | null;
working_hours?: components['schemas']['WorkingHours'];
@@ -1805,11 +1934,9 @@ export interface components {
readonly slack_user_identity: components['schemas']['SlackUserIdentity'];
readonly telegram_configuration: components['schemas']['TelegramToUserConnector'];
readonly messaging_backends: {
- [key: string]:
- | {
- [key: string]: unknown;
- }
- | undefined;
+ [key: string]: {
+ [key: string]: unknown;
+ };
};
readonly notification_chain_verbal: {
default: string;
@@ -1820,7 +1947,7 @@ export interface components {
readonly has_google_oauth2_connected: boolean;
readonly is_currently_oncall: boolean;
google_calendar_settings?: components['schemas']['GoogleCalendarSettings'];
- readonly rbac_permissions: Array;
+ readonly rbac_permissions: components['schemas']['UserPermission'][];
readonly google_oauth2_token_is_missing_scopes: boolean;
};
/** @description This serializer is consistent with apps.api.serializers.labels.LabelPairSerializer, but allows null for value ID. */
@@ -1885,9 +2012,9 @@ export interface components {
/** @description Alert group labels configuration for the integration. See AlertReceiveChannel.alert_group_labels for details. */
IntegrationAlertGroupLabels: {
inheritable: {
- [key: string]: boolean | undefined;
+ [key: string]: boolean;
};
- custom: Array;
+ custom: components['schemas']['CustomLabel'][];
template: string | null;
};
/**
@@ -1980,7 +2107,7 @@ export interface components {
};
LabelCreate: {
key: components['schemas']['LabelRepr'];
- values: Array;
+ values: components['schemas']['LabelRepr'][];
};
LabelKey: {
id: string;
@@ -1990,7 +2117,7 @@ export interface components {
};
LabelOption: {
key: components['schemas']['LabelKey'];
- values: Array;
+ values: components['schemas']['LabelValue'][];
};
LabelPair: {
key: components['schemas']['LabelKey'];
@@ -2016,7 +2143,6 @@ export interface components {
readonly role: components['schemas']['RoleEnum'];
/** Format: uri */
readonly avatar: string;
- /** Format: uri */
readonly avatar_full: string;
timezone?: string | null;
working_hours?: components['schemas']['WorkingHours'];
@@ -2026,11 +2152,9 @@ export interface components {
readonly slack_user_identity: components['schemas']['SlackUserIdentity'];
readonly telegram_configuration: components['schemas']['TelegramToUserConnector'];
readonly messaging_backends: {
- [key: string]:
- | {
- [key: string]: unknown;
- }
- | undefined;
+ [key: string]: {
+ [key: string]: unknown;
+ };
};
readonly notification_chain_verbal: {
default: string;
@@ -2065,7 +2189,7 @@ export interface components {
* @example http://api.example.org/accounts/?cursor=cj0xJnA9NDg3
*/
previous?: string | null;
- results: Array;
+ results: components['schemas']['AlertGroupList'][];
page_size?: number;
};
PaginatedAlertReceiveChannelPolymorphicList: {
@@ -2081,7 +2205,7 @@ export interface components {
* @example http://api.example.org/accounts/?page=2
*/
previous?: string | null;
- results: Array;
+ results: components['schemas']['AlertReceiveChannelPolymorphic'][];
page_size?: number;
current_page_number?: number;
total_pages?: number;
@@ -2099,7 +2223,7 @@ export interface components {
* @example http://api.example.org/accounts/?page=2
*/
previous?: string | null;
- results: Array;
+ results: components['schemas']['UserPolymorphic'][];
page_size?: number;
current_page_number?: number;
total_pages?: number;
@@ -2139,7 +2263,7 @@ export interface components {
readonly is_based_on_alertmanager?: boolean;
readonly inbound_email?: string;
readonly is_legacy?: boolean;
- labels?: Array;
+ labels?: components['schemas']['LabelPair'][];
alert_group_labels?: components['schemas']['IntegrationAlertGroupLabels'];
/** Format: date-time */
readonly alertmanager_v2_migrated_at?: string | null;
@@ -2156,7 +2280,6 @@ export interface components {
readonly role?: components['schemas']['RoleEnum'];
/** Format: uri */
readonly avatar?: string;
- /** Format: uri */
readonly avatar_full?: string;
timezone?: string | null;
working_hours?: components['schemas']['WorkingHours'];
@@ -2166,11 +2289,9 @@ export interface components {
readonly slack_user_identity?: components['schemas']['SlackUserIdentity'];
readonly telegram_configuration?: components['schemas']['TelegramToUserConnector'];
readonly messaging_backends?: {
- [key: string]:
- | {
- [key: string]: unknown;
- }
- | undefined;
+ [key: string]: {
+ [key: string]: unknown;
+ };
};
readonly notification_chain_verbal?: {
default: string;
@@ -2182,6 +2303,28 @@ export interface components {
readonly is_currently_oncall?: boolean;
google_calendar_settings?: components['schemas']['GoogleCalendarSettings'];
};
+ PatchedWebhook: {
+ readonly id?: string;
+ name?: string | null;
+ is_webhook_enabled?: boolean | null;
+ is_legacy?: boolean | null;
+ team?: string | null;
+ username?: string | null;
+ password?: string | null;
+ authorization_header?: string | null;
+ trigger_template?: string | null;
+ headers?: string | null;
+ url?: string | null;
+ data?: string | null;
+ forward_all?: boolean | null;
+ http_method?: string | null;
+ trigger_type?: string | null;
+ readonly trigger_type_name?: string;
+ readonly last_response_log?: string;
+ integration_filter?: string[];
+ preset?: string | null;
+ labels?: components['schemas']['LabelPair'][];
+ };
PreviewTemplateRequest: {
template_body?: string | null;
template_name?: string | null;
@@ -2283,7 +2426,6 @@ export interface components {
readonly role: components['schemas']['RoleEnum'];
/** Format: uri */
readonly avatar: string;
- /** Format: uri */
readonly avatar_full: string;
timezone?: string | null;
working_hours?: components['schemas']['WorkingHours'];
@@ -2293,11 +2435,9 @@ export interface components {
readonly slack_user_identity: components['schemas']['SlackUserIdentity'];
readonly telegram_configuration: components['schemas']['TelegramToUserConnector'];
readonly messaging_backends: {
- [key: string]:
- | {
- [key: string]: unknown;
- }
- | undefined;
+ [key: string]: {
+ [key: string]: unknown;
+ };
};
readonly notification_chain_verbal: {
default: string;
@@ -2345,10 +2485,10 @@ export interface components {
username: string;
pk: string;
avatar: string;
- avatar_full: string;
+ readonly avatar_full: string;
name: string;
readonly timezone: string | null;
- readonly teams: Array;
+ readonly teams: components['schemas']['FastTeam'][];
readonly is_currently_oncall: boolean;
};
UserPermission: {
@@ -2362,7 +2502,7 @@ export interface components {
username: string;
pk: string;
avatar: string;
- avatar_full: string;
+ readonly avatar_full: string;
};
Value: {
id: string;
@@ -2388,16 +2528,54 @@ export interface components {
readonly last_response_log: string;
integration_filter?: string[];
preset?: string | null;
- labels?: Array;
+ labels?: components['schemas']['LabelPair'][];
+ };
+ WebhookFilters: {
+ name: string;
+ display_name?: string;
+ type: string;
+ href: string;
+ global?: boolean;
+ };
+ WebhookPresetOptions: {
+ id: string;
+ name: string;
+ logo: string;
+ description: string;
+ controlled_fields: string[];
+ };
+ WebhookPreviewTemplateRequest: {
+ template_body?: string | null;
+ template_name?: string | null;
+ payload?: {
+ [key: string]: unknown;
+ } | null;
+ };
+ WebhookPreviewTemplateResponse: {
+ preview: string | null;
+ };
+ WebhookResponse: {
+ /** Format: date-time */
+ timestamp?: string;
+ url?: string | null;
+ request_trigger?: string | null;
+ request_headers?: string | null;
+ request_data?: string | null;
+ status_code?: number | null;
+ content?: string | null;
+ event_data?: string | null;
+ };
+ WebhookTriggerManual: {
+ alert_group: string;
};
WorkingHours: {
- monday: Array;
- tuesday: Array;
- wednesday: Array;
- thursday: Array;
- friday: Array;
- saturday: Array;
- sunday: Array;
+ monday: components['schemas']['WorkingHoursPeriod'][];
+ tuesday: components['schemas']['WorkingHoursPeriod'][];
+ wednesday: components['schemas']['WorkingHoursPeriod'][];
+ thursday: components['schemas']['WorkingHoursPeriod'][];
+ friday: components['schemas']['WorkingHoursPeriod'][];
+ saturday: components['schemas']['WorkingHoursPeriod'][];
+ sunday: components['schemas']['WorkingHoursPeriod'][];
};
WorkingHoursPeriod: {
start: string;
@@ -2448,7 +2626,7 @@ export interface operations {
* * `jira` - Jira
* * `zendesk` - Zendesk
* * `appdynamics` - AppDynamics */
- integration?: Array<
+ integration?: (
| 'alertmanager'
| 'amazon_sns'
| 'appdynamics'
@@ -2481,7 +2659,7 @@ export interface operations {
| 'webhook'
| 'zabbix'
| 'zendesk'
- >;
+ )[];
/** @description * `grafana_alerting` - Grafana Alerting
* * `webhook` - Webhook
* * `alertmanager` - Alertmanager
@@ -2514,7 +2692,7 @@ export interface operations {
* * `jira` - Jira
* * `zendesk` - Zendesk
* * `appdynamics` - AppDynamics */
- integration_ne?: Array<
+ integration_ne?: (
| 'alertmanager'
| 'amazon_sns'
| 'appdynamics'
@@ -2547,10 +2725,10 @@ export interface operations {
| 'webhook'
| 'zabbix'
| 'zendesk'
- >;
+ )[];
/** @description * `0` - Debug
* * `1` - Maintenance */
- maintenance_mode?: Array<0 | 1>;
+ maintenance_mode?: (0 | 1 | null)[];
/** @description A page number within the paginated result set. */
page?: number;
/** @description Number of results to return per page. */
@@ -2826,9 +3004,9 @@ export interface operations {
};
requestBody: {
content: {
- 'application/json': Array;
- 'application/x-www-form-urlencoded': Array;
- 'multipart/form-data': Array;
+ 'application/json': components['schemas']['AlertReceiveChannelNewConnection'][];
+ 'application/x-www-form-urlencoded': components['schemas']['AlertReceiveChannelNewConnection'][];
+ 'multipart/form-data': components['schemas']['AlertReceiveChannelNewConnection'][];
};
};
responses: {
@@ -2910,7 +3088,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
- 'application/json': Array;
+ 'application/json': components['schemas']['AlertReceiveChannelConnectedContactPoints'][];
};
};
};
@@ -2933,12 +3111,10 @@ export interface operations {
};
content: {
'application/json': {
- [key: string]:
- | {
- alerts_count: number;
- alert_groups_count: number;
- }
- | undefined;
+ [key: string]: {
+ alerts_count: number;
+ alert_groups_count: number;
+ };
};
};
};
@@ -3188,7 +3364,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
- 'application/json': Array;
+ 'application/json': components['schemas']['Webhook'][];
};
};
};
@@ -3286,7 +3462,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
- 'application/json': Array;
+ 'application/json': components['schemas']['AlertReceiveChannelContactPoints'][];
};
};
};
@@ -3306,12 +3482,10 @@ export interface operations {
};
content: {
'application/json': {
- [key: string]:
- | {
- alerts_count: number;
- alert_groups_count: number;
- }
- | undefined;
+ [key: string]: {
+ alerts_count: number;
+ alert_groups_count: number;
+ };
};
};
};
@@ -3331,7 +3505,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
- 'application/json': Array;
+ 'application/json': components['schemas']['AlertReceiveChannelFilters'][];
};
};
};
@@ -3350,7 +3524,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
- 'application/json': Array;
+ 'application/json': components['schemas']['AlertReceiveChannelIntegrationOptions'][];
};
};
};
@@ -3430,7 +3604,7 @@ export interface operations {
* * `1` - Acknowledged
* * `2` - Resolved
* * `3` - Silenced */
- status?: Array<0 | 1 | 2 | 3>;
+ status?: (0 | 1 | 2 | 3)[];
with_resolution_note?: boolean;
};
header?: never;
@@ -3801,7 +3975,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
- 'application/json': Array;
+ 'application/json': components['schemas']['AlertGroupBulkActionOptions'][];
};
};
};
@@ -3820,7 +3994,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
- 'application/json': Array;
+ 'application/json': components['schemas']['AlertGroupFilters'][];
};
};
};
@@ -3860,7 +4034,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
- 'application/json': Array;
+ 'application/json': components['schemas']['LabelKey'][];
};
};
};
@@ -3879,7 +4053,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
- 'application/json': Array;
+ 'application/json': components['schemas']['AlertGroupSilenceOptions'][];
};
};
};
@@ -3904,7 +4078,7 @@ export interface operations {
* * `1` - Acknowledged
* * `2` - Resolved
* * `3` - Silenced */
- status?: Array<0 | 1 | 2 | 3>;
+ status?: (0 | 1 | 2 | 3)[];
with_resolution_note?: boolean;
};
header?: never;
@@ -3977,7 +4151,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
- 'application/json': Array<
+ 'application/json': (
| 'msteams'
| 'slack'
| 'unified_slack'
@@ -3988,7 +4162,7 @@ export interface operations {
| 'grafana_alerting_v2'
| 'labels'
| 'google_oauth2'
- >;
+ )[];
};
};
};
@@ -4002,9 +4176,9 @@ export interface operations {
};
requestBody: {
content: {
- 'application/json': Array;
- 'application/x-www-form-urlencoded': Array;
- 'multipart/form-data': Array;
+ 'application/json': components['schemas']['LabelCreate'][];
+ 'application/x-www-form-urlencoded': components['schemas']['LabelCreate'][];
+ 'multipart/form-data': components['schemas']['LabelCreate'][];
};
};
responses: {
@@ -4157,7 +4331,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
- 'application/json': Array;
+ 'application/json': components['schemas']['LabelKey'][];
};
};
};
@@ -4309,7 +4483,7 @@ export interface operations {
* * `1` - EDITOR
* * `2` - VIEWER
* * `3` - NONE */
- roles?: Array<0 | 1 | 2 | 3>;
+ roles?: (0 | 1 | 2 | 3)[];
/** @description A search term. */
search?: string;
team?: string[];
@@ -4730,7 +4904,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
- 'application/json': Array<{
+ 'application/json': {
schedule_id: string;
schedule_name: string;
is_oncall: boolean;
@@ -4740,7 +4914,7 @@ export interface operations {
start: string;
/** Format: date-time */
end: string;
- users: Array<{
+ users: {
display_name: string;
pk: string;
email: string;
@@ -4754,7 +4928,7 @@ export interface operations {
avatar_full: string;
} | null;
} | null;
- }>;
+ }[];
missing_users: string[];
priority_level: number | null;
source: string | null;
@@ -4772,7 +4946,7 @@ export interface operations {
start: string;
/** Format: date-time */
end: string;
- users: Array<{
+ users: {
display_name: string;
pk: string;
email: string;
@@ -4786,7 +4960,7 @@ export interface operations {
avatar_full: string;
} | null;
} | null;
- }>;
+ }[];
missing_users: string[];
priority_level: number | null;
source: string | null;
@@ -4798,7 +4972,7 @@ export interface operations {
pk: string;
};
} | null;
- }>;
+ }[];
};
};
};
@@ -4840,7 +5014,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
- 'application/json': Array;
+ 'application/json': components['schemas']['UserFilters'][];
};
};
};
@@ -4864,4 +5038,265 @@ export interface operations {
};
};
};
+ webhooks_list: {
+ parameters: {
+ query?: {
+ /** @description A search term. */
+ search?: string;
+ };
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['Webhook'][];
+ };
+ };
+ };
+ };
+ webhooks_create: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ 'application/json': components['schemas']['Webhook'];
+ 'application/x-www-form-urlencoded': components['schemas']['Webhook'];
+ 'multipart/form-data': components['schemas']['Webhook'];
+ };
+ };
+ responses: {
+ 201: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['Webhook'];
+ };
+ };
+ };
+ };
+ webhooks_retrieve: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ /** @description A string identifying this resource. */
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['Webhook'];
+ };
+ };
+ };
+ };
+ webhooks_update: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ /** @description A string identifying this resource. */
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ 'application/json': components['schemas']['Webhook'];
+ 'application/x-www-form-urlencoded': components['schemas']['Webhook'];
+ 'multipart/form-data': components['schemas']['Webhook'];
+ };
+ };
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['Webhook'];
+ };
+ };
+ };
+ };
+ webhooks_destroy: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ /** @description A string identifying this resource. */
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description No response body */
+ 204: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ };
+ };
+ webhooks_partial_update: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ /** @description A string identifying this resource. */
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: {
+ content: {
+ 'application/json': components['schemas']['PatchedWebhook'];
+ 'application/x-www-form-urlencoded': components['schemas']['PatchedWebhook'];
+ 'multipart/form-data': components['schemas']['PatchedWebhook'];
+ };
+ };
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['Webhook'];
+ };
+ };
+ };
+ };
+ webhooks_preview_template_create: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ /** @description A string identifying this resource. */
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: {
+ content: {
+ 'application/json': components['schemas']['WebhookPreviewTemplateRequest'];
+ 'application/x-www-form-urlencoded': components['schemas']['WebhookPreviewTemplateRequest'];
+ 'multipart/form-data': components['schemas']['WebhookPreviewTemplateRequest'];
+ };
+ };
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['WebhookPreviewTemplateResponse'];
+ };
+ };
+ };
+ };
+ webhooks_responses_list: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ /** @description A string identifying this resource. */
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['WebhookResponse'][];
+ };
+ };
+ };
+ };
+ webhooks_trigger_manual_create: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ /** @description A string identifying this resource. */
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ 'application/json': components['schemas']['WebhookTriggerManual'];
+ 'application/x-www-form-urlencoded': components['schemas']['WebhookTriggerManual'];
+ 'multipart/form-data': components['schemas']['WebhookTriggerManual'];
+ };
+ };
+ responses: {
+ /** @description No response body */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ };
+ };
+ webhooks_filters_list: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['WebhookFilters'][];
+ };
+ };
+ };
+ };
+ webhooks_preset_options_retrieve: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['WebhookPresetOptions'];
+ };
+ };
+ };
+ };
}
diff --git a/grafana-plugin/src/pages/incident/Incident.tsx b/grafana-plugin/src/pages/incident/Incident.tsx
index bedd9368..bb2a8984 100644
--- a/grafana-plugin/src/pages/incident/Incident.tsx
+++ b/grafana-plugin/src/pages/incident/Incident.tsx
@@ -473,7 +473,7 @@ class _IncidentPage extends React.Component