Rares/add template editor to webhooks (#2455)
# What this PR does Bring new Jinja editor to webhooks ## Which issue(s) this PR fixes https://github.com/grafana/oncall/issues/2344 ## Checklist - [ ] Unit, integration, and e2e (if applicable) tests updated - [ ] Documentation added (or `pr:no public docs` PR label added if not required) - [x] `CHANGELOG.md` updated (or `pr:no changelog` PR label added if not required) --------- Co-authored-by: Maxim <maxim.mordasov@grafana.com> Co-authored-by: Michael Derynck <michael.derynck@grafana.com>
This commit is contained in:
parent
b951b6b6bd
commit
5ebf437283
27 changed files with 1024 additions and 385 deletions
|
|
@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
## Unreleased
|
||||
|
||||
### Added
|
||||
|
||||
- Bring new Jinja editor to webhooks ([2344](https://github.com/grafana/oncall/issues/2344))
|
||||
|
||||
### Fixed
|
||||
|
||||
- Add debounce on Select UI components to avoid making API search requests on each key-down event by
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ class WebhookResponseSerializer(serializers.ModelSerializer):
|
|||
"request_data",
|
||||
"status_code",
|
||||
"content",
|
||||
"event_data",
|
||||
]
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ from rest_framework.response import Response
|
|||
from rest_framework.test import APIClient
|
||||
|
||||
from apps.api.permissions import LegacyAccessControlRole
|
||||
from apps.api.views.webhooks import RECENT_RESPONSE_LIMIT, WEBHOOK_URL
|
||||
from apps.webhooks.models import Webhook
|
||||
from apps.webhooks.models.webhook import WEBHOOK_FIELD_PLACEHOLDER
|
||||
|
||||
|
|
@ -61,6 +62,7 @@ def test_get_list_webhooks(webhook_internal_api_setup, make_user_auth_headers):
|
|||
"status_code": None,
|
||||
"request_trigger": "",
|
||||
"url": "",
|
||||
"event_data": "",
|
||||
},
|
||||
"trigger_template": None,
|
||||
"trigger_type": None,
|
||||
|
|
@ -102,6 +104,7 @@ def test_get_detail_webhook(webhook_internal_api_setup, make_user_auth_headers):
|
|||
"status_code": None,
|
||||
"request_trigger": "",
|
||||
"url": "",
|
||||
"event_data": "",
|
||||
},
|
||||
"trigger_template": None,
|
||||
"trigger_type": None,
|
||||
|
|
@ -148,6 +151,7 @@ def test_create_webhook(mocked_check_webhooks_2_enabled, webhook_internal_api_se
|
|||
"status_code": None,
|
||||
"request_trigger": "",
|
||||
"url": "",
|
||||
"event_data": "",
|
||||
},
|
||||
"trigger_template": None,
|
||||
"trigger_type_name": "Alert Group Created",
|
||||
|
|
@ -207,6 +211,7 @@ def test_create_valid_templated_field(
|
|||
"status_code": None,
|
||||
"request_trigger": "",
|
||||
"url": "",
|
||||
"event_data": "",
|
||||
},
|
||||
"trigger_template": None,
|
||||
"trigger_type_name": "Alert Group Created",
|
||||
|
|
@ -485,3 +490,59 @@ def test_webhook_from_other_team_without_flag(
|
|||
|
||||
response = client.get(url, format="json", **make_user_auth_headers(user, token))
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_get_webhook_responses(
|
||||
make_organization_and_user_with_plugin_token,
|
||||
make_team,
|
||||
make_user_auth_headers,
|
||||
make_custom_webhook,
|
||||
make_webhook_response,
|
||||
):
|
||||
organization, user, token = make_organization_and_user_with_plugin_token()
|
||||
team = make_team(organization)
|
||||
webhook = make_custom_webhook(
|
||||
organization=organization, team=team, trigger_type=Webhook.TRIGGER_ALERT_GROUP_CREATED
|
||||
)
|
||||
for i in range(0, RECENT_RESPONSE_LIMIT + 1):
|
||||
make_webhook_response(
|
||||
webhook=webhook,
|
||||
trigger_type=webhook.trigger_type,
|
||||
status_code=200,
|
||||
content=json.dumps({"id": "third-party-id"}),
|
||||
event_data=json.dumps({"test": f"{i}"}),
|
||||
)
|
||||
|
||||
client = APIClient()
|
||||
url = reverse("api-internal:webhooks-responses", kwargs={"pk": webhook.public_primary_key})
|
||||
response = client.get(url, format="json", **make_user_auth_headers(user, token))
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert len(response.data) == RECENT_RESPONSE_LIMIT
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.parametrize(
|
||||
"test_template, test_payload, expected_result",
|
||||
[
|
||||
("https://test.com", None, "https://test.com"),
|
||||
("https://test.com", "", "https://test.com"),
|
||||
("{{ name }}", {"name": "test_1"}, "test_1"),
|
||||
("{{ name }}", '{"name": "test_1"}', "test_1"),
|
||||
],
|
||||
)
|
||||
def test_webhook_preview_template(
|
||||
webhook_internal_api_setup, make_user_auth_headers, test_template, test_payload, expected_result
|
||||
):
|
||||
user, token, webhook = webhook_internal_api_setup
|
||||
client = APIClient()
|
||||
url = reverse("api-internal:webhooks-preview-template", kwargs={"pk": webhook.public_primary_key})
|
||||
data = {
|
||||
"template_name": WEBHOOK_URL,
|
||||
"template_body": test_template,
|
||||
"payload": test_payload,
|
||||
}
|
||||
|
||||
response = client.post(url, data, format="json", **make_user_auth_headers(user, token))
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.data["preview"] == expected_result
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
import json
|
||||
|
||||
from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
|
||||
from django_filters import rest_framework as filters
|
||||
from rest_framework import status
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.exceptions import NotFound
|
||||
from rest_framework.filters import SearchFilter
|
||||
|
|
@ -8,12 +11,23 @@ from rest_framework.response import Response
|
|||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from apps.api.permissions import RBACPermission
|
||||
from apps.api.serializers.webhook import WebhookSerializer
|
||||
from apps.api.serializers.webhook import WebhookResponseSerializer, WebhookSerializer
|
||||
from apps.auth_token.auth import PluginAuthentication
|
||||
from apps.webhooks.models import Webhook
|
||||
from apps.webhooks.utils import is_webhooks_enabled_for_organization
|
||||
from apps.webhooks.models import Webhook, WebhookResponse
|
||||
from apps.webhooks.utils import apply_jinja_template_for_json, is_webhooks_enabled_for_organization
|
||||
from common.api_helpers.exceptions import BadRequest
|
||||
from common.api_helpers.filters import ByTeamModelFieldFilterMixin, ModelFieldFilterMixin, TeamModelMultipleChoiceFilter
|
||||
from common.api_helpers.mixins import PublicPrimaryKeyMixin, TeamFilteringMixin
|
||||
from common.jinja_templater.apply_jinja_template import JinjaTemplateError, JinjaTemplateWarning
|
||||
|
||||
RECENT_RESPONSE_LIMIT = 20
|
||||
|
||||
WEBHOOK_URL = "url"
|
||||
WEBHOOK_HEADERS = "headers"
|
||||
WEBHOOK_TRIGGER_TEMPLATE = "trigger_template"
|
||||
WEBHOOK_TRIGGER_DATA = "data"
|
||||
|
||||
WEBHOOK_TEMPLATE_NAMES = [WEBHOOK_URL, WEBHOOK_HEADERS, WEBHOOK_TRIGGER_TEMPLATE, WEBHOOK_TRIGGER_DATA]
|
||||
|
||||
|
||||
class WebhooksFilter(ByTeamModelFieldFilterMixin, ModelFieldFilterMixin, filters.FilterSet):
|
||||
|
|
@ -33,6 +47,8 @@ class WebhooksView(TeamFilteringMixin, PublicPrimaryKeyMixin, ModelViewSet):
|
|||
"update": [RBACPermission.Permissions.OUTGOING_WEBHOOKS_WRITE],
|
||||
"partial_update": [RBACPermission.Permissions.OUTGOING_WEBHOOKS_WRITE],
|
||||
"destroy": [RBACPermission.Permissions.OUTGOING_WEBHOOKS_WRITE],
|
||||
"responses": [RBACPermission.Permissions.OUTGOING_WEBHOOKS_READ],
|
||||
"preview_template": [RBACPermission.Permissions.OUTGOING_WEBHOOKS_WRITE],
|
||||
}
|
||||
|
||||
model = Webhook
|
||||
|
|
@ -106,3 +122,44 @@ class WebhooksView(TeamFilteringMixin, PublicPrimaryKeyMixin, ModelViewSet):
|
|||
filter_options = list(filter(lambda f: filter_name in f["name"], filter_options))
|
||||
|
||||
return Response(filter_options)
|
||||
|
||||
@action(methods=["get"], detail=True)
|
||||
def responses(self, request, pk):
|
||||
webhook = self.get_object()
|
||||
queryset = WebhookResponse.objects.filter(webhook_id=webhook.id, trigger_type=webhook.trigger_type).order_by(
|
||||
"-timestamp"
|
||||
)[:RECENT_RESPONSE_LIMIT]
|
||||
response_serializer = WebhookResponseSerializer(queryset, many=True)
|
||||
return Response(response_serializer.data)
|
||||
|
||||
@action(methods=["post"], detail=True)
|
||||
def preview_template(self, request, pk):
|
||||
self.get_object() # Check webhook exists
|
||||
template_body = request.data.get("template_body", None)
|
||||
template_name = request.data.get("template_name", None)
|
||||
payload = request.data.get("payload", None)
|
||||
|
||||
if not payload:
|
||||
response = {"preview": template_body}
|
||||
return Response(response, status=status.HTTP_200_OK)
|
||||
|
||||
if isinstance(payload, str):
|
||||
try:
|
||||
payload = json.loads(payload)
|
||||
except json.JSONDecodeError:
|
||||
raise BadRequest(detail={"payload": "Could not parse json"})
|
||||
|
||||
if template_body is None or template_name is None:
|
||||
response = {"preview": None}
|
||||
return Response(response, status=status.HTTP_200_OK)
|
||||
|
||||
if template_name not in WEBHOOK_TEMPLATE_NAMES:
|
||||
raise BadRequest(detail={"template_name": "Unknown template name"})
|
||||
|
||||
try:
|
||||
result = apply_jinja_template_for_json(template_body, payload)
|
||||
except (JinjaTemplateError, JinjaTemplateWarning) as e:
|
||||
return Response({"preview": e.fallback_message}, status.HTTP_200_OK)
|
||||
|
||||
response = {"preview": result}
|
||||
return Response(response, status=status.HTTP_200_OK)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 3.2.19 on 2023-07-05 18:46
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('webhooks', '0006_auto_20230426_1631'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='webhookresponse',
|
||||
name='event_data',
|
||||
field=models.TextField(default=None, null=True),
|
||||
),
|
||||
]
|
||||
|
|
@ -293,6 +293,7 @@ class WebhookResponse(models.Model):
|
|||
url = models.TextField(null=True, default=None)
|
||||
status_code = models.IntegerField(default=None, null=True)
|
||||
content = models.TextField(null=True, default=None)
|
||||
event_data = models.TextField(null=True, default=None)
|
||||
|
||||
def json(self):
|
||||
if self.content:
|
||||
|
|
|
|||
|
|
@ -110,6 +110,7 @@ def make_request(webhook, alert_group, data):
|
|||
"status_code": None,
|
||||
"content": None,
|
||||
"webhook": webhook,
|
||||
"event_data": json.dumps(data),
|
||||
}
|
||||
|
||||
exception = error = None
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import Block from 'components/GBlock/Block';
|
|||
import MonacoEditor from 'components/MonacoEditor/MonacoEditor';
|
||||
import SourceCode from 'components/SourceCode/SourceCode';
|
||||
import Text from 'components/Text/Text';
|
||||
import TemplatePreview from 'containers/TemplatePreview/TemplatePreview';
|
||||
import TemplatePreview, { TEMPLATE_PAGE } from 'containers/TemplatePreview/TemplatePreview';
|
||||
import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip';
|
||||
import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_channel.types';
|
||||
import { Alert } from 'models/alertgroup/alertgroup.types';
|
||||
|
|
@ -132,13 +132,6 @@ const AlertTemplatesForm = (props: AlertTemplatesFormProps) => {
|
|||
}
|
||||
}, [activeGroup]);
|
||||
|
||||
const getTemplatePreviewEditClickHandler = (templateName: string) => {
|
||||
return () => {
|
||||
const template = templatesToRender.find((template) => template.name === templateName);
|
||||
setActiveTemplate(template);
|
||||
};
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!activeTemplate && filteredTemplatesToRender.length) {
|
||||
setActiveTemplate(filteredTemplatesToRender[0]);
|
||||
|
|
@ -261,11 +254,10 @@ const AlertTemplatesForm = (props: AlertTemplatesFormProps) => {
|
|||
<VerticalGroup style={{ width: '100%' }}>
|
||||
{groups[activeGroup].map((template) => (
|
||||
<TemplatePreview
|
||||
active={template.name === activeTemplate?.name}
|
||||
templatePage={TEMPLATE_PAGE.Integrations}
|
||||
key={template.name}
|
||||
templateName={template.name}
|
||||
templateBody={tempValues[template.name] ?? templates[template.name]}
|
||||
onEditClick={getTemplatePreviewEditClickHandler(template.name)}
|
||||
alertReceiveChannelId={alertReceiveChannelId}
|
||||
alertGroupId={alertGroupId}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ import cn from 'classnames/bind';
|
|||
|
||||
import Collapse from 'components/Collapse/Collapse';
|
||||
import { FormItem, FormItemType } from 'components/GForm/GForm.types';
|
||||
import MonacoEditor from 'components/MonacoEditor/MonacoEditor';
|
||||
import { MONACO_READONLY_CONFIG } from 'components/MonacoEditor/MonacoEditor.config';
|
||||
import GSelect from 'containers/GSelect/GSelect';
|
||||
import RemoteSelect from 'containers/RemoteSelect/RemoteSelect';
|
||||
|
||||
|
|
@ -17,6 +19,12 @@ interface GFormProps {
|
|||
form: { name: string; fields: FormItem[] };
|
||||
data: any;
|
||||
onSubmit: (data: any) => void;
|
||||
onFieldRender?: (
|
||||
formItem: FormItem,
|
||||
renderedControl: React.ReactElement,
|
||||
values: any,
|
||||
setValue: (value: string) => void
|
||||
) => React.ReactElement;
|
||||
}
|
||||
|
||||
const nullNormalizer = (value: string) => {
|
||||
|
|
@ -110,6 +118,28 @@ function renderFormControl(formItem: FormItem, register: any, control: any, onCh
|
|||
/>
|
||||
);
|
||||
|
||||
case FormItemType.Monaco:
|
||||
return (
|
||||
<InputControl
|
||||
control={control}
|
||||
name={formItem.name}
|
||||
render={({ field: { ...field } }) => {
|
||||
return (
|
||||
<MonacoEditor
|
||||
{...field}
|
||||
{...formItem.extra}
|
||||
showLineNumbers={false}
|
||||
monacoOptions={{
|
||||
...MONACO_READONLY_CONFIG,
|
||||
readOnly: formItem.isReadOnly,
|
||||
}}
|
||||
onChange={(value) => onChangeFn(field, value)}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
|
@ -117,7 +147,7 @@ function renderFormControl(formItem: FormItem, register: any, control: any, onCh
|
|||
|
||||
class GForm extends React.Component<GFormProps, {}> {
|
||||
render() {
|
||||
const { form, data } = this.props;
|
||||
const { form, data, onFieldRender } = this.props;
|
||||
|
||||
const openFields = form.fields.filter((field) => !field.collapsed);
|
||||
const collapsedfields = form.fields.filter((field) => field.collapsed);
|
||||
|
|
@ -131,6 +161,11 @@ class GForm extends React.Component<GFormProps, {}> {
|
|||
return null;
|
||||
}
|
||||
|
||||
const formControl = renderFormControl(formItem, register, control, (field, value) => {
|
||||
field?.onChange(value);
|
||||
this.forceUpdate();
|
||||
});
|
||||
|
||||
return (
|
||||
<Field
|
||||
key={formIndex}
|
||||
|
|
@ -140,10 +175,9 @@ class GForm extends React.Component<GFormProps, {}> {
|
|||
error={formItem.label ? `${formItem.label} is required` : `${capitalCase(formItem.name)} is required`}
|
||||
description={formItem.description}
|
||||
>
|
||||
{renderFormControl(formItem, register, control, (field, value) => {
|
||||
field?.onChange(value);
|
||||
this.forceUpdate();
|
||||
})}
|
||||
{onFieldRender
|
||||
? onFieldRender(formItem, formControl, getValues(), (value) => setValue(formItem.name, value))
|
||||
: formControl}
|
||||
</Field>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -7,12 +7,14 @@ export enum FormItemType {
|
|||
'GSelect' = 'gselect',
|
||||
'Switch' = 'switch',
|
||||
'RemoteSelect' = 'remoteselect',
|
||||
'Monaco' = 'monaco',
|
||||
}
|
||||
|
||||
export interface FormItem {
|
||||
name: string;
|
||||
label?: string;
|
||||
type: FormItemType;
|
||||
isReadOnly?: boolean;
|
||||
description?: string;
|
||||
normalize?: (value: any) => any;
|
||||
isVisible?: (data: any) => any;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
// Mostly used for input fields where we're hiding scrollbars
|
||||
export const MONACO_READONLY_CONFIG = {
|
||||
renderLineHighlight: false,
|
||||
readOnly: true,
|
||||
scrollbar: {
|
||||
vertical: 'hidden',
|
||||
horizontal: 'hidden',
|
||||
verticalScrollbarSize: 0,
|
||||
handleMouseWheel: false,
|
||||
},
|
||||
hideCursorInOverviewRuler: true,
|
||||
minimap: { enabled: false },
|
||||
cursorStyle: {
|
||||
display: 'none',
|
||||
},
|
||||
};
|
||||
|
||||
export const MONACO_EDITABLE_CONFIG = {
|
||||
renderLineHighlight: false,
|
||||
readOnly: false,
|
||||
hideCursorInOverviewRuler: true,
|
||||
minimap: { enabled: false },
|
||||
cursorStyle: {
|
||||
display: 'none',
|
||||
},
|
||||
};
|
||||
|
|
@ -11,7 +11,7 @@ declare const monaco: any;
|
|||
interface MonacoEditorProps {
|
||||
value: string;
|
||||
disabled?: boolean;
|
||||
height?: string;
|
||||
height?: string | number;
|
||||
focus?: boolean;
|
||||
data: any;
|
||||
showLineNumbers?: boolean;
|
||||
|
|
@ -20,6 +20,7 @@ interface MonacoEditorProps {
|
|||
onChange?: (value: string) => void;
|
||||
loading?: boolean;
|
||||
monacoOptions?: any;
|
||||
suggestionPrefix?: string;
|
||||
}
|
||||
|
||||
export enum MONACO_LANGUAGE {
|
||||
|
|
@ -48,15 +49,18 @@ const MonacoEditor: FC<MonacoEditorProps> = (props) => {
|
|||
monacoOptions,
|
||||
showLineNumbers = true,
|
||||
loading = false,
|
||||
suggestionPrefix = 'payload.',
|
||||
} = props;
|
||||
|
||||
const autoCompleteList = useCallback(
|
||||
() =>
|
||||
[...PREDEFINED_TERMS, ...getPaths(data?.payload_example).map((str) => `payload.${str}`)].map((str) => ({
|
||||
label: str,
|
||||
insertText: str,
|
||||
kind: CodeEditorSuggestionItemKind.Field,
|
||||
})),
|
||||
[...PREDEFINED_TERMS, ...getPaths(data?.payload_example).map((str) => `${suggestionPrefix}${str}`)].map(
|
||||
(str) => ({
|
||||
label: str,
|
||||
insertText: str,
|
||||
kind: CodeEditorSuggestionItemKind.Field,
|
||||
})
|
||||
),
|
||||
[data?.payload_example]
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import HamburgerMenu from 'components/HamburgerMenu/HamburgerMenu';
|
|||
import IntegrationBlock from 'components/Integrations/IntegrationBlock';
|
||||
import IntegrationBlockItem from 'components/Integrations/IntegrationBlockItem';
|
||||
import MonacoEditor from 'components/MonacoEditor/MonacoEditor';
|
||||
import { MONACO_READONLY_CONFIG } from 'components/MonacoEditor/MonacoEditor.config';
|
||||
import PluginLink from 'components/PluginLink/PluginLink';
|
||||
import Text from 'components/Text/Text';
|
||||
import TooltipBadge from 'components/TooltipBadge/TooltipBadge';
|
||||
|
|
@ -35,7 +36,7 @@ import { ChannelFilter } from 'models/channel_filter/channel_filter.types';
|
|||
import { EscalationChain } from 'models/escalation_chain/escalation_chain.types';
|
||||
import CommonIntegrationHelper from 'pages/integration/CommonIntegration.helper';
|
||||
import IntegrationHelper from 'pages/integration/Integration.helper';
|
||||
import { MONACO_INPUT_HEIGHT_SMALL, MONACO_OPTIONS } from 'pages/integration/IntegrationCommon.config';
|
||||
import { MONACO_INPUT_HEIGHT_SMALL } from 'pages/integration/IntegrationCommon.config';
|
||||
import { useStore } from 'state/useStore';
|
||||
import { openNotification } from 'utils';
|
||||
import { UserActions } from 'utils/authorization';
|
||||
|
|
@ -164,7 +165,7 @@ const ExpandedIntegrationRouteDisplay: React.FC<ExpandedIntegrationRouteDisplayP
|
|||
height={MONACO_INPUT_HEIGHT_SMALL}
|
||||
data={templates}
|
||||
showLineNumbers={false}
|
||||
monacoOptions={MONACO_OPTIONS}
|
||||
monacoOptions={MONACO_READONLY_CONFIG}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
|
|
|
|||
|
|
@ -6,13 +6,14 @@ import cn from 'classnames/bind';
|
|||
import IntegrationBlockItem from 'components/Integrations/IntegrationBlockItem';
|
||||
import IntegrationTemplateBlock from 'components/Integrations/IntegrationTemplateBlock';
|
||||
import MonacoEditor from 'components/MonacoEditor/MonacoEditor';
|
||||
import { MONACO_READONLY_CONFIG } from 'components/MonacoEditor/MonacoEditor.config';
|
||||
import Text from 'components/Text/Text';
|
||||
import { templatesToRender } from 'containers/IntegrationContainers/IntegrationTemplatesList.config';
|
||||
import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_channel.types';
|
||||
import { AlertTemplatesDTO } from 'models/alert_templates';
|
||||
import IntegrationHelper from 'pages/integration/Integration.helper';
|
||||
import styles from 'pages/integration/Integration.module.scss';
|
||||
import { MONACO_INPUT_HEIGHT_TALL, MONACO_OPTIONS } from 'pages/integration/IntegrationCommon.config';
|
||||
import { MONACO_INPUT_HEIGHT_TALL } from 'pages/integration/IntegrationCommon.config';
|
||||
import { useStore } from 'state/useStore';
|
||||
import { openErrorNotification, openNotification } from 'utils';
|
||||
|
||||
|
|
@ -108,7 +109,7 @@ const IntegrationTemplateList: React.FC<IntegrationTemplateListProps> = ({
|
|||
height={contents.height}
|
||||
data={templates}
|
||||
showLineNumbers={false}
|
||||
monacoOptions={MONACO_OPTIONS}
|
||||
monacoOptions={MONACO_READONLY_CONFIG}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React, { useCallback, useState, useEffect } from 'react';
|
||||
|
||||
import { Button, HorizontalGroup, Drawer, VerticalGroup, Icon } from '@grafana/ui';
|
||||
import { Button, HorizontalGroup, Drawer, VerticalGroup } from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
import { debounce } from 'lodash-es';
|
||||
import { observer } from 'mobx-react';
|
||||
|
|
@ -12,11 +12,10 @@ import {
|
|||
slackMessageTemplateCheatSheet,
|
||||
genericTemplateCheatSheet,
|
||||
} from 'components/CheatSheet/CheatSheet.config';
|
||||
import Block from 'components/GBlock/Block';
|
||||
import MonacoEditor from 'components/MonacoEditor/MonacoEditor';
|
||||
import Text from 'components/Text/Text';
|
||||
import TemplatePreview from 'containers/TemplatePreview/TemplatePreview';
|
||||
import TemplatesAlertGroupsList from 'containers/TemplatesAlertGroupsList/TemplatesAlertGroupsList';
|
||||
import TemplateResult from 'containers/TemplateResult/TemplateResult';
|
||||
import TemplatesAlertGroupsList, { TEMPLATE_PAGE } from 'containers/TemplatesAlertGroupsList/TemplatesAlertGroupsList';
|
||||
import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip';
|
||||
import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_channel.types';
|
||||
import { AlertTemplatesDTO } from 'models/alert_templates';
|
||||
|
|
@ -188,43 +187,15 @@ const IntegrationTemplate = observer((props: IntegrationTemplateProps) => {
|
|||
<div className={cx('container-wrapper')}>
|
||||
<div className={cx('container')} id={'content-container-id'}>
|
||||
<TemplatesAlertGroupsList
|
||||
templatePage={TEMPLATE_PAGE.Integrations}
|
||||
alertReceiveChannelId={id}
|
||||
onEditPayload={onEditPayload}
|
||||
onSelectAlertGroup={onSelectAlertGroup}
|
||||
templates={templates}
|
||||
onLoadAlertGroupsList={onLoadAlertGroupsList}
|
||||
/>
|
||||
{isCheatSheetVisible ? (
|
||||
<CheatSheet
|
||||
cheatSheetName={template.displayName}
|
||||
cheatSheetData={getCheatSheet(template.name)}
|
||||
onClose={onCloseCheatSheet}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<div className={cx('template-block-codeeditor')}>
|
||||
<div className={cx('template-editor-block-title')}>
|
||||
<HorizontalGroup justify="space-between" align="center" wrap>
|
||||
<Text>Template editor</Text>
|
||||
|
||||
<Button variant="secondary" fill="outline" onClick={onShowCheatSheet} icon="book" size="sm">
|
||||
Cheatsheet
|
||||
</Button>
|
||||
</HorizontalGroup>
|
||||
</div>
|
||||
<div className={cx('template-editor-block-content')}>
|
||||
<MonacoEditor
|
||||
value={changedTemplateBody}
|
||||
data={templates}
|
||||
showLineNumbers={true}
|
||||
height={editorHeight}
|
||||
onChange={getChangeHandler()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<Result
|
||||
{renderCheatSheet()}
|
||||
<TemplateResult
|
||||
alertReceiveChannelId={id}
|
||||
template={template}
|
||||
templateBody={changedTemplateBody}
|
||||
|
|
@ -238,88 +209,43 @@ const IntegrationTemplate = observer((props: IntegrationTemplateProps) => {
|
|||
</div>
|
||||
</Drawer>
|
||||
);
|
||||
|
||||
function renderCheatSheet() {
|
||||
if (isCheatSheetVisible) {
|
||||
return (
|
||||
<CheatSheet
|
||||
cheatSheetName={template.displayName}
|
||||
cheatSheetData={getCheatSheet(template.name)}
|
||||
onClose={onCloseCheatSheet}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={cx('template-block-codeeditor')}>
|
||||
<div className={cx('template-editor-block-title')}>
|
||||
<HorizontalGroup justify="space-between" align="center" wrap>
|
||||
<Text>Template editor</Text>
|
||||
|
||||
<Button variant="secondary" fill="outline" onClick={onShowCheatSheet} icon="book" size="sm">
|
||||
Cheatsheet
|
||||
</Button>
|
||||
</HorizontalGroup>
|
||||
</div>
|
||||
<div className={cx('template-editor-block-content')}>
|
||||
<MonacoEditor
|
||||
value={changedTemplateBody}
|
||||
data={templates}
|
||||
showLineNumbers={true}
|
||||
height={editorHeight}
|
||||
onChange={getChangeHandler()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
interface ResultProps {
|
||||
alertReceiveChannelId: AlertReceiveChannel['id'];
|
||||
// templateName: string;
|
||||
templateBody: string;
|
||||
template: TemplateForEdit;
|
||||
isAlertGroupExisting?: boolean;
|
||||
chatOpsPermalink?: string;
|
||||
payload?: JSON;
|
||||
error?: string;
|
||||
onSaveAndFollowLink?: (link: string) => void;
|
||||
templateIsRoute?: boolean;
|
||||
}
|
||||
|
||||
const Result = (props: ResultProps) => {
|
||||
const {
|
||||
alertReceiveChannelId,
|
||||
template,
|
||||
templateBody,
|
||||
chatOpsPermalink,
|
||||
payload,
|
||||
error,
|
||||
isAlertGroupExisting,
|
||||
onSaveAndFollowLink,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div className={cx('template-block-result')}>
|
||||
<div className={cx('template-block-title')}>
|
||||
<HorizontalGroup justify="space-between">
|
||||
<Text>Result</Text>
|
||||
</HorizontalGroup>
|
||||
</div>
|
||||
<div className={cx('result')}>
|
||||
{payload || error ? (
|
||||
<VerticalGroup spacing="lg">
|
||||
{error ? (
|
||||
<Block bordered fullWidth withBackground>
|
||||
<Text>{error}</Text>
|
||||
</Block>
|
||||
) : (
|
||||
<Block bordered fullWidth withBackground>
|
||||
<TemplatePreview
|
||||
key={template.name}
|
||||
templateName={template.name}
|
||||
templateBody={templateBody}
|
||||
templateType={template.type}
|
||||
templateIsRoute={template.isRoute}
|
||||
alertReceiveChannelId={alertReceiveChannelId}
|
||||
payload={payload}
|
||||
/>
|
||||
</Block>
|
||||
)}
|
||||
|
||||
{template?.additionalData?.additionalDescription && (
|
||||
<Text type="secondary">{template?.additionalData.additionalDescription}</Text>
|
||||
)}
|
||||
|
||||
{template?.additionalData?.chatOpsName && isAlertGroupExisting && (
|
||||
<VerticalGroup>
|
||||
<Button onClick={() => onSaveAndFollowLink(chatOpsPermalink)}>
|
||||
<HorizontalGroup spacing="xs" align="center">
|
||||
Save and open Alert Group in {template.additionalData.chatOpsDisplayName}{' '}
|
||||
<Icon name="external-link-alt" />
|
||||
</HorizontalGroup>
|
||||
</Button>
|
||||
|
||||
{template.additionalData.data && <Text type="secondary">{template.additionalData.data}</Text>}
|
||||
</VerticalGroup>
|
||||
)}
|
||||
</VerticalGroup>
|
||||
) : (
|
||||
<div>
|
||||
<Block bordered fullWidth className={cx('block-style')} withBackground>
|
||||
<Text>← Select alert group or "Use custom payload"</Text>
|
||||
</Block>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default IntegrationTemplate;
|
||||
|
|
|
|||
|
|
@ -141,14 +141,18 @@ export const form: { name: string; fields: FormItem[] } = {
|
|||
{
|
||||
name: 'url',
|
||||
label: 'Webhook URL',
|
||||
type: FormItemType.Input,
|
||||
type: FormItemType.Monaco,
|
||||
validation: { required: true },
|
||||
extra: {
|
||||
height: 30,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'headers',
|
||||
label: 'Webhook Headers',
|
||||
description: 'Request headers should be in JSON format.',
|
||||
type: FormItemType.TextArea,
|
||||
type: FormItemType.Monaco,
|
||||
isReadOnly: true,
|
||||
extra: {
|
||||
rows: 3,
|
||||
},
|
||||
|
|
@ -169,7 +173,8 @@ export const form: { name: string; fields: FormItem[] } = {
|
|||
},
|
||||
{
|
||||
name: 'trigger_template',
|
||||
type: FormItemType.TextArea,
|
||||
type: FormItemType.Monaco,
|
||||
isReadOnly: true,
|
||||
description:
|
||||
'Trigger template is used to conditionally execute the webhook based on incoming data. The trigger template must be empty or evaluate to true or 1 for the webhook to be sent',
|
||||
extra: {
|
||||
|
|
@ -185,12 +190,11 @@ export const form: { name: string; fields: FormItem[] } = {
|
|||
{
|
||||
name: 'data',
|
||||
getDisabled: (data) => Boolean(data?.forward_all),
|
||||
type: FormItemType.TextArea,
|
||||
type: FormItemType.Monaco,
|
||||
isReadOnly: true,
|
||||
description:
|
||||
'Available variables: {{ event }}, {{ user }}, {{ alert_group }}, {{ alert_group_id }}, {{ alert_payload }}, {{ integration }}, {{ notified_users }}, {{ users_to_be_notified }}, {{ responses }}',
|
||||
extra: {
|
||||
rows: 9,
|
||||
},
|
||||
extra: {},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -13,3 +13,18 @@
|
|||
.tabs__content {
|
||||
padding-top: 16px;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.form-field {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
/* TODO: figure out why this is not picked */
|
||||
.webhooks__drawerContent .cursor.monaco-mouse-cursor-text {
|
||||
display: none !important;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,8 +6,10 @@ import { observer } from 'mobx-react';
|
|||
import { useHistory } from 'react-router-dom';
|
||||
|
||||
import GForm from 'components/GForm/GForm';
|
||||
import { FormItem, FormItemType } from 'components/GForm/GForm.types';
|
||||
import Text from 'components/Text/Text';
|
||||
import OutgoingWebhook2Status from 'containers/OutgoingWebhook2Status/OutgoingWebhook2Status';
|
||||
import WebhooksTemplateEditor from 'containers/WebhooksTemplateEditor/WebhooksTemplateEditor';
|
||||
import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip';
|
||||
import { OutgoingWebhook2 } from 'models/outgoing_webhook_2/outgoing_webhook_2.types';
|
||||
import { WebhookFormActionType } from 'pages/outgoing_webhooks_2/OutgoingWebhooks2.types';
|
||||
|
|
@ -38,6 +40,8 @@ export const WebhookTabs = {
|
|||
const OutgoingWebhook2Form = observer((props: OutgoingWebhook2FormProps) => {
|
||||
const history = useHistory();
|
||||
const { id, action, onUpdate, onHide, onDelete } = props;
|
||||
const [onFormChangeFn, setOnFormChangeFn] = useState<{ fn: (value: string) => void }>(undefined);
|
||||
const [templateToEdit, setTemplateToEdit] = useState(undefined);
|
||||
const [activeTab, setActiveTab] = useState<string>(
|
||||
action === WebhookFormActionType.EDIT_SETTINGS ? WebhookTabs.Settings.key : WebhookTabs.LastRun.key
|
||||
);
|
||||
|
|
@ -56,6 +60,31 @@ const OutgoingWebhook2Form = observer((props: OutgoingWebhook2FormProps) => {
|
|||
[id]
|
||||
);
|
||||
|
||||
const getTemplateEditClickHandler = (formItem: FormItem, values, setFormFieldValue) => {
|
||||
return () => {
|
||||
const formValue = values[formItem.name];
|
||||
setTemplateToEdit({ value: formValue, displayName: undefined, description: undefined, name: formItem.name });
|
||||
setOnFormChangeFn({ fn: (value) => setFormFieldValue(value) });
|
||||
};
|
||||
};
|
||||
|
||||
const enrchField = (formItem: FormItem, renderedControl: React.ReactElement, values, setFormFieldValue) => {
|
||||
if (formItem.type === FormItemType.Monaco) {
|
||||
return (
|
||||
<div className={cx('form-row')}>
|
||||
<div className={cx('form-field')}>{renderedControl}</div>
|
||||
<Button
|
||||
icon="edit"
|
||||
variant="secondary"
|
||||
onClick={getTemplateEditClickHandler(formItem, values, setFormFieldValue)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return renderedControl;
|
||||
};
|
||||
|
||||
if (
|
||||
(action === WebhookFormActionType.EDIT_SETTINGS || action === WebhookFormActionType.VIEW_LAST_RUN) &&
|
||||
!outgoingWebhook2Store.items[id]
|
||||
|
|
@ -86,51 +115,79 @@ const OutgoingWebhook2Form = observer((props: OutgoingWebhook2FormProps) => {
|
|||
return null;
|
||||
}
|
||||
|
||||
const formElement = <GForm form={form} data={data} onSubmit={handleSubmit} onFieldRender={enrchField} />;
|
||||
|
||||
if (action === WebhookFormActionType.NEW || action === WebhookFormActionType.COPY) {
|
||||
// show just the creation form, not the tabs
|
||||
return (
|
||||
<Drawer scrollableContent title={'Create Outgoing Webhook'} onClose={onHide} closeOnMaskClick={false}>
|
||||
{renderWebhookForm()}
|
||||
</Drawer>
|
||||
<>
|
||||
<Drawer scrollableContent title={'Create Outgoing Webhook'} onClose={onHide} closeOnMaskClick={false}>
|
||||
<div className="webhooks__drawerContent">{renderWebhookForm()}</div>
|
||||
</Drawer>
|
||||
{templateToEdit && (
|
||||
<WebhooksTemplateEditor
|
||||
id={id}
|
||||
handleSubmit={(value) => onFormChangeFn?.fn(value)}
|
||||
onHide={() => setTemplateToEdit(undefined)}
|
||||
template={templateToEdit}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
// show tabbed drawer (edit/live_run)
|
||||
<Drawer scrollableContent title={'Outgoing webhook details'} onClose={onHide} closeOnMaskClick={false}>
|
||||
<TabsBar>
|
||||
<Tab
|
||||
key={WebhookTabs.Settings.key}
|
||||
onChangeTab={() => {
|
||||
setActiveTab(WebhookTabs.Settings.key);
|
||||
history.push(`${PLUGIN_ROOT}/outgoing_webhooks_2/edit/${id}`);
|
||||
}}
|
||||
active={activeTab === WebhookTabs.Settings.key}
|
||||
label={WebhookTabs.Settings.value}
|
||||
/>
|
||||
<>
|
||||
<Drawer scrollableContent title={'Outgoing webhook details'} onClose={onHide} closeOnMaskClick={false}>
|
||||
<div className={cx('webhooks__drawerContent')}>
|
||||
<TabsBar>
|
||||
<Tab
|
||||
key={WebhookTabs.Settings.key}
|
||||
onChangeTab={() => {
|
||||
setActiveTab(WebhookTabs.Settings.key);
|
||||
history.push(`${PLUGIN_ROOT}/outgoing_webhooks_2/edit/${id}`);
|
||||
}}
|
||||
active={activeTab === WebhookTabs.Settings.key}
|
||||
label={WebhookTabs.Settings.value}
|
||||
/>
|
||||
|
||||
<Tab
|
||||
key={WebhookTabs.LastRun.key}
|
||||
onChangeTab={() => {
|
||||
setActiveTab(WebhookTabs.LastRun.key);
|
||||
history.push(`${PLUGIN_ROOT}/outgoing_webhooks_2/last_run/${id}`);
|
||||
}}
|
||||
active={activeTab === WebhookTabs.LastRun.key}
|
||||
label={WebhookTabs.LastRun.value}
|
||||
/>
|
||||
</TabsBar>
|
||||
<Tab
|
||||
key={WebhookTabs.LastRun.key}
|
||||
onChangeTab={() => {
|
||||
setActiveTab(WebhookTabs.LastRun.key);
|
||||
history.push(`${PLUGIN_ROOT}/outgoing_webhooks_2/last_run/${id}`);
|
||||
}}
|
||||
active={activeTab === WebhookTabs.LastRun.key}
|
||||
label={WebhookTabs.LastRun.value}
|
||||
/>
|
||||
</TabsBar>
|
||||
|
||||
<WebhookTabsContent
|
||||
id={id}
|
||||
action={action}
|
||||
activeTab={activeTab}
|
||||
data={data}
|
||||
handleSubmit={handleSubmit}
|
||||
onDelete={onDelete}
|
||||
onHide={onHide}
|
||||
onUpdate={onUpdate}
|
||||
/>
|
||||
</Drawer>
|
||||
<WebhookTabsContent
|
||||
id={id}
|
||||
action={action}
|
||||
activeTab={activeTab}
|
||||
data={data}
|
||||
handleSubmit={handleSubmit}
|
||||
onDelete={onDelete}
|
||||
onHide={onHide}
|
||||
onUpdate={onUpdate}
|
||||
formElement={formElement}
|
||||
/>
|
||||
</div>
|
||||
</Drawer>
|
||||
{templateToEdit && (
|
||||
<WebhooksTemplateEditor
|
||||
id={id}
|
||||
handleSubmit={(value) => {
|
||||
onFormChangeFn?.fn(value);
|
||||
setTemplateToEdit(undefined);
|
||||
}}
|
||||
onHide={() => setTemplateToEdit(undefined)}
|
||||
template={templateToEdit}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
function renderWebhookForm() {
|
||||
|
|
@ -170,6 +227,7 @@ interface WebhookTabsProps {
|
|||
onUpdate: () => void;
|
||||
onDelete: () => void;
|
||||
handleSubmit: (data: Partial<OutgoingWebhook2>) => void;
|
||||
formElement: React.ReactElement;
|
||||
}
|
||||
|
||||
const WebhookTabsContent: React.FC<WebhookTabsProps> = ({
|
||||
|
|
@ -177,10 +235,10 @@ const WebhookTabsContent: React.FC<WebhookTabsProps> = ({
|
|||
action,
|
||||
activeTab,
|
||||
data,
|
||||
handleSubmit,
|
||||
onHide,
|
||||
onUpdate,
|
||||
onDelete,
|
||||
formElement,
|
||||
}) => {
|
||||
const [confirmationModal, setConfirmationModal] = useState<ConfirmModalProps>(undefined);
|
||||
|
||||
|
|
@ -193,7 +251,7 @@ const WebhookTabsContent: React.FC<WebhookTabsProps> = ({
|
|||
{activeTab === WebhookTabs.Settings.key && (
|
||||
<>
|
||||
<div className={cx('content')} data-testid="test__outgoingWebhook2EditForm">
|
||||
<GForm form={form} data={data} onSubmit={handleSubmit} />
|
||||
{formElement}
|
||||
<div className={cx('buttons')}>
|
||||
<HorizontalGroup justify={'flex-end'}>
|
||||
<Button variant="secondary" onClick={onHide}>
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { observer } from 'mobx-react';
|
|||
import Text from 'components/Text/Text';
|
||||
import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_channel.types';
|
||||
import { Alert } from 'models/alertgroup/alertgroup.types';
|
||||
import { OutgoingWebhook2 } from 'models/outgoing_webhook_2/outgoing_webhook_2.types';
|
||||
import { useStore } from 'state/useStore';
|
||||
import { openErrorNotification } from 'utils';
|
||||
import { useDebouncedCallback } from 'utils/hooks';
|
||||
|
|
@ -23,28 +24,43 @@ interface TemplatePreviewProps {
|
|||
templateIsRoute?: boolean;
|
||||
payload?: JSON;
|
||||
alertReceiveChannelId: AlertReceiveChannel['id'];
|
||||
onEditClick?: () => void;
|
||||
alertGroupId?: Alert['pk'];
|
||||
active?: boolean;
|
||||
onResult?: (result) => void;
|
||||
outgoingWebhookId?: OutgoingWebhook2['id'];
|
||||
templatePage: TEMPLATE_PAGE;
|
||||
}
|
||||
interface ConditionalResult {
|
||||
isResult?: boolean;
|
||||
value?: string;
|
||||
}
|
||||
|
||||
export enum TEMPLATE_PAGE {
|
||||
Integrations,
|
||||
Webhooks,
|
||||
}
|
||||
|
||||
const TemplatePreview = observer((props: TemplatePreviewProps) => {
|
||||
const { templateName, templateBody, templateType, payload, alertReceiveChannelId, alertGroupId, templateIsRoute } =
|
||||
props;
|
||||
const {
|
||||
templateName,
|
||||
templateBody,
|
||||
templateType,
|
||||
payload,
|
||||
alertReceiveChannelId,
|
||||
outgoingWebhookId,
|
||||
alertGroupId,
|
||||
templateIsRoute,
|
||||
templatePage,
|
||||
} = props;
|
||||
|
||||
const [result, setResult] = useState<{ preview: string | null } | undefined>(undefined);
|
||||
const [conditionalResult, setConditionalResult] = useState<ConditionalResult>({});
|
||||
|
||||
const store = useStore();
|
||||
const { alertReceiveChannelStore, alertGroupStore } = store;
|
||||
const { alertReceiveChannelStore, alertGroupStore, outgoingWebhook2Store } = store;
|
||||
|
||||
const handleTemplateBodyChange = useDebouncedCallback(() => {
|
||||
(alertGroupId
|
||||
(templatePage === TEMPLATE_PAGE.Webhooks
|
||||
? outgoingWebhook2Store.renderPreview(outgoingWebhookId, templateName, templateBody, payload)
|
||||
: alertGroupId
|
||||
? alertGroupStore.renderPreview(alertGroupId, templateName, templateBody)
|
||||
: alertReceiveChannelStore.renderPreview(alertReceiveChannelId, templateName, templateBody, payload)
|
||||
)
|
||||
|
|
|
|||
105
grafana-plugin/src/containers/TemplateResult/TemplateResult.tsx
Normal file
105
grafana-plugin/src/containers/TemplateResult/TemplateResult.tsx
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
import React from 'react';
|
||||
|
||||
import { Button, HorizontalGroup, Icon, VerticalGroup } from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
|
||||
import { TemplateForEdit } from 'components/AlertTemplates/CommonAlertTemplatesForm.config';
|
||||
import Block from 'components/GBlock/Block';
|
||||
import Text from 'components/Text/Text';
|
||||
import styles from 'containers/IntegrationTemplate/IntegrationTemplate.module.scss';
|
||||
import TemplatePreview, { TEMPLATE_PAGE } from 'containers/TemplatePreview/TemplatePreview';
|
||||
import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_channel.types';
|
||||
import { OutgoingWebhook2 } from 'models/outgoing_webhook_2/outgoing_webhook_2.types';
|
||||
|
||||
const cx = cn.bind(styles);
|
||||
|
||||
interface ResultProps {
|
||||
alertReceiveChannelId?: AlertReceiveChannel['id'];
|
||||
outgoingWebhookId?: OutgoingWebhook2['id'];
|
||||
templateBody: string;
|
||||
template: TemplateForEdit;
|
||||
isAlertGroupExisting?: boolean;
|
||||
chatOpsPermalink?: string;
|
||||
payload?: JSON;
|
||||
error?: string;
|
||||
onSaveAndFollowLink?: (link: string) => void;
|
||||
templateIsRoute?: boolean;
|
||||
templatePage?: TEMPLATE_PAGE;
|
||||
}
|
||||
|
||||
const TemplateResult = (props: ResultProps) => {
|
||||
const {
|
||||
alertReceiveChannelId,
|
||||
outgoingWebhookId,
|
||||
template,
|
||||
templateBody,
|
||||
chatOpsPermalink,
|
||||
payload,
|
||||
error,
|
||||
isAlertGroupExisting,
|
||||
onSaveAndFollowLink,
|
||||
templatePage = TEMPLATE_PAGE.Integrations,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div className={cx('template-block-result')}>
|
||||
<div className={cx('template-block-title')}>
|
||||
<HorizontalGroup justify="space-between">
|
||||
<Text>Result</Text>
|
||||
</HorizontalGroup>
|
||||
</div>
|
||||
<div className={cx('result')}>
|
||||
{payload || error ? (
|
||||
<VerticalGroup spacing="lg">
|
||||
{error ? (
|
||||
<Block bordered fullWidth withBackground>
|
||||
<Text>{error}</Text>
|
||||
</Block>
|
||||
) : (
|
||||
<Block bordered fullWidth withBackground>
|
||||
<TemplatePreview
|
||||
key={template.name}
|
||||
templatePage={templatePage}
|
||||
templateName={template.name}
|
||||
templateBody={templateBody}
|
||||
templateType={template.type}
|
||||
templateIsRoute={template.isRoute}
|
||||
alertReceiveChannelId={alertReceiveChannelId}
|
||||
outgoingWebhookId={outgoingWebhookId}
|
||||
payload={payload}
|
||||
/>
|
||||
</Block>
|
||||
)}
|
||||
|
||||
{template?.additionalData?.additionalDescription && (
|
||||
<Text type="secondary">{template?.additionalData.additionalDescription}</Text>
|
||||
)}
|
||||
|
||||
{template?.additionalData?.chatOpsName && isAlertGroupExisting && (
|
||||
<VerticalGroup>
|
||||
<Button onClick={() => onSaveAndFollowLink(chatOpsPermalink)}>
|
||||
<HorizontalGroup spacing="xs" align="center">
|
||||
Save and open Alert Group in {template.additionalData.chatOpsDisplayName}{' '}
|
||||
<Icon name="external-link-alt" />
|
||||
</HorizontalGroup>
|
||||
</Button>
|
||||
|
||||
{template.additionalData.data && <Text type="secondary">{template.additionalData.data}</Text>}
|
||||
</VerticalGroup>
|
||||
)}
|
||||
</VerticalGroup>
|
||||
) : (
|
||||
<div>
|
||||
<Block bordered fullWidth className={cx('block-style')} withBackground>
|
||||
<Text>
|
||||
← Select {templatePage === TEMPLATE_PAGE.Webhooks ? 'event' : 'alert group'} or "Use custom payload"
|
||||
</Text>
|
||||
</Block>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TemplateResult;
|
||||
|
|
@ -1,16 +1,17 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import { Button, HorizontalGroup, Tooltip, Icon, IconButton, Badge, LoadingPlaceholder } from '@grafana/ui';
|
||||
import { Button, HorizontalGroup, Icon, IconButton, Badge, LoadingPlaceholder } from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
import { debounce } from 'lodash-es';
|
||||
|
||||
import MonacoEditor, { MONACO_LANGUAGE } from 'components/MonacoEditor/MonacoEditor';
|
||||
import { MONACO_EDITABLE_CONFIG } from 'components/MonacoEditor/MonacoEditor.config';
|
||||
import Text from 'components/Text/Text';
|
||||
import TooltipBadge from 'components/TooltipBadge/TooltipBadge';
|
||||
import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_channel.types';
|
||||
import { AlertTemplatesDTO } from 'models/alert_templates';
|
||||
import { Alert } from 'models/alertgroup/alertgroup.types';
|
||||
import { MONACO_PAYLOAD_OPTIONS } from 'pages/integration/IntegrationCommon.config';
|
||||
import { OutgoingWebhook2, OutgoingWebhook2Response } from 'models/outgoing_webhook_2/outgoing_webhook_2.types';
|
||||
import { useStore } from 'state/useStore';
|
||||
|
||||
import styles from './TemplatesAlertGroupsList.module.css';
|
||||
|
|
@ -19,27 +20,55 @@ const cx = cn.bind(styles);
|
|||
const HEADER_OF_CONTAINER_HEIGHT = 59;
|
||||
const BADGE_WITH_PADDINGS_HEIGHT = 42;
|
||||
|
||||
export enum TEMPLATE_PAGE {
|
||||
Integrations,
|
||||
Webhooks,
|
||||
}
|
||||
|
||||
interface TemplatesAlertGroupsListProps {
|
||||
templatePage: TEMPLATE_PAGE;
|
||||
templates: AlertTemplatesDTO[];
|
||||
alertReceiveChannelId: AlertReceiveChannel['id'];
|
||||
alertReceiveChannelId?: AlertReceiveChannel['id'];
|
||||
outgoingwebhookId?: OutgoingWebhook2['id'];
|
||||
heading?: string;
|
||||
|
||||
onSelectAlertGroup?: (alertGroup: Alert) => void;
|
||||
|
||||
onEditPayload?: (payload: string) => void;
|
||||
onLoadAlertGroupsList?: (isRecentAlertExising: boolean) => void;
|
||||
}
|
||||
|
||||
const TemplatesAlertGroupsList = (props: TemplatesAlertGroupsListProps) => {
|
||||
const { alertReceiveChannelId, templates, onEditPayload, onSelectAlertGroup, onLoadAlertGroupsList } = props;
|
||||
const {
|
||||
templatePage,
|
||||
heading = 'Recent Alert groups',
|
||||
alertReceiveChannelId,
|
||||
outgoingwebhookId,
|
||||
templates,
|
||||
onEditPayload,
|
||||
onSelectAlertGroup,
|
||||
onLoadAlertGroupsList,
|
||||
} = props;
|
||||
const store = useStore();
|
||||
const [alertGroupsList, setAlertGroupsList] = useState(undefined);
|
||||
const [selectedAlertPayload, setSelectedAlertPayload] = useState<string>(undefined);
|
||||
const [selectedAlertName, setSelectedAlertName] = useState<string>(undefined);
|
||||
const [outgoingWebhookLastResponses, setOutgoingWebhookLastResponses] =
|
||||
useState<OutgoingWebhook2Response[]>(undefined);
|
||||
|
||||
const [selectedTitle, setSelectedTitle] = useState<string>(undefined);
|
||||
const [selectedPayload, setSelectedPayload] = useState<string>(undefined);
|
||||
const [isEditMode, setIsEditMode] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
store.alertGroupStore.getAlertGroupsForIntegration(alertReceiveChannelId).then((result) => {
|
||||
setAlertGroupsList(result.slice(0, 30));
|
||||
onLoadAlertGroupsList(result.length > 0);
|
||||
});
|
||||
if (templatePage === TEMPLATE_PAGE.Webhooks) {
|
||||
if (outgoingwebhookId !== 'new') {
|
||||
store.outgoingWebhook2Store.getLastResponses(outgoingwebhookId).then(setOutgoingWebhookLastResponses);
|
||||
}
|
||||
} else if (templatePage === TEMPLATE_PAGE.Integrations) {
|
||||
store.alertGroupStore.getAlertGroupsForIntegration(alertReceiveChannelId).then((result) => {
|
||||
setAlertGroupsList(result.slice(0, 30));
|
||||
onLoadAlertGroupsList(result.length > 0);
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
const getCodeEditorHeight = () => {
|
||||
|
|
@ -62,181 +91,240 @@ const TemplatesAlertGroupsList = (props: TemplatesAlertGroupsListProps) => {
|
|||
|
||||
const returnToListView = () => {
|
||||
setIsEditMode(false);
|
||||
setSelectedAlertPayload(undefined);
|
||||
setSelectedPayload(undefined);
|
||||
onEditPayload(null);
|
||||
};
|
||||
|
||||
// for Integrations
|
||||
|
||||
const getAlertGroupPayload = async (id) => {
|
||||
const groupedAlert = await store.alertGroupStore.getAlertsFromGroup(id);
|
||||
const currentIncidentRawResponse = await store.alertGroupStore.getPayloadForIncident(groupedAlert?.alerts[0]?.id);
|
||||
setSelectedAlertName(getAlertGroupName(groupedAlert));
|
||||
setSelectedAlertPayload(currentIncidentRawResponse?.raw_request_data);
|
||||
setSelectedTitle(getAlertGroupName(groupedAlert));
|
||||
setSelectedPayload(currentIncidentRawResponse?.raw_request_data);
|
||||
|
||||
// ?
|
||||
onSelectAlertGroup(groupedAlert);
|
||||
onEditPayload(JSON.stringify(currentIncidentRawResponse?.raw_request_data));
|
||||
};
|
||||
|
||||
const getAlertGroupName = (alertGroup: Alert) => {
|
||||
// Integrations page
|
||||
return alertGroup.inside_organization_number
|
||||
? `#${alertGroup.inside_organization_number} ${alertGroup.render_for_web.title}`
|
||||
: alertGroup.render_for_web.title;
|
||||
? `#${alertGroup.inside_organization_number} ${alertGroup.render_for_web?.title}`
|
||||
: alertGroup.render_for_web?.title;
|
||||
};
|
||||
|
||||
// for Outgoing webhooks
|
||||
|
||||
const handleOutgoingWebhookResponseSelect = (response: OutgoingWebhook2Response) => {
|
||||
setSelectedTitle(response.timestamp);
|
||||
|
||||
setSelectedPayload(JSON.parse(response.event_data));
|
||||
|
||||
onEditPayload(response.event_data);
|
||||
};
|
||||
|
||||
if (selectedPayload) {
|
||||
// IF selected we either display it as ReadOnly or in EditMode
|
||||
return (
|
||||
<div className={cx('template-block-list')} id="alerts-content-container-id">
|
||||
{isEditMode ? renderSelectedPayloadInEditMode() : renderSelectedPayloadInReadOnlyMode()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cx('template-block-list')} id="alerts-content-container-id">
|
||||
{selectedAlertPayload ? (
|
||||
{isEditMode ? (
|
||||
<>
|
||||
{isEditMode ? (
|
||||
<>
|
||||
<div className={cx('template-block-title-edit-mode')}>
|
||||
<HorizontalGroup justify="space-between">
|
||||
<Text>Edit custom payload</Text>
|
||||
<div className={cx('template-block-title-edit-mode')}>
|
||||
<HorizontalGroup justify="space-between">
|
||||
<Text>Edit custom payload</Text>
|
||||
|
||||
<HorizontalGroup>
|
||||
<IconButton name="times" onClick={() => returnToListView()} />
|
||||
</HorizontalGroup>
|
||||
</HorizontalGroup>
|
||||
</div>
|
||||
<div className={cx('alert-groups-editor')}>
|
||||
<MonacoEditor
|
||||
value={JSON.stringify(selectedAlertPayload, null, 4)}
|
||||
data={templates}
|
||||
height={getCodeEditorHeight()}
|
||||
onChange={getChangeHandler()}
|
||||
showLineNumbers
|
||||
useAutoCompleteList={false}
|
||||
language={MONACO_LANGUAGE.json}
|
||||
monacoOptions={MONACO_PAYLOAD_OPTIONS}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className={cx('template-block-title')}>
|
||||
<div className={cx('selected-alert-name-container')}>
|
||||
<div className={cx('selected-alert-name')}>
|
||||
<Text>{selectedAlertName}</Text>
|
||||
</div>
|
||||
<div className={cx('title-action-icons')}>
|
||||
<IconButton name="edit" onClick={() => setIsEditMode(true)} />
|
||||
<IconButton name="times" onClick={() => returnToListView()} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={cx('alert-groups-editor')}>
|
||||
<TooltipBadge
|
||||
borderType="primary"
|
||||
text="Last alert payload"
|
||||
tooltipTitle=""
|
||||
tooltipContent=""
|
||||
className={cx('alert-groups-last-payload-badge')}
|
||||
/>
|
||||
<div className={cx('alert-groups-editor-withBadge')}>
|
||||
{/* Editor used for Editing Given Payload */}
|
||||
<MonacoEditor
|
||||
value={JSON.stringify(selectedAlertPayload, null, 4)}
|
||||
data={undefined}
|
||||
disabled
|
||||
height={getCodeEditorHeightWithBadge()}
|
||||
onChange={getChangeHandler()}
|
||||
useAutoCompleteList={false}
|
||||
language={MONACO_LANGUAGE.json}
|
||||
monacoOptions={{
|
||||
...MONACO_PAYLOAD_OPTIONS,
|
||||
readOnly: true,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<HorizontalGroup>
|
||||
<IconButton name="times" onClick={returnToListView} />
|
||||
</HorizontalGroup>
|
||||
</HorizontalGroup>
|
||||
</div>
|
||||
<div className={cx('alert-groups-editor')}>
|
||||
<MonacoEditor
|
||||
value={null}
|
||||
disabled={true}
|
||||
useAutoCompleteList={false}
|
||||
language={MONACO_LANGUAGE.json}
|
||||
data={templates}
|
||||
monacoOptions={{
|
||||
...MONACO_EDITABLE_CONFIG,
|
||||
readOnly: false,
|
||||
}}
|
||||
height={getCodeEditorHeight()}
|
||||
onChange={getChangeHandler()}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{isEditMode ? (
|
||||
<>
|
||||
<div className={cx('template-block-title-edit-mode')}>
|
||||
<HorizontalGroup justify="space-between">
|
||||
<Text>Edit custom payload</Text>
|
||||
<div className={cx('template-block-title')}>
|
||||
<HorizontalGroup justify="space-between" wrap>
|
||||
<HorizontalGroup>
|
||||
<Text>{heading}</Text>
|
||||
{/* <Tooltip content="Here will be information about alert groups" placement="top">
|
||||
<Icon name="info-circle" />
|
||||
</Tooltip> */}
|
||||
</HorizontalGroup>
|
||||
|
||||
<HorizontalGroup>
|
||||
<IconButton name="times" onClick={() => returnToListView()} />
|
||||
</HorizontalGroup>
|
||||
</HorizontalGroup>
|
||||
</div>
|
||||
<div className={cx('alert-groups-editor')}>
|
||||
<MonacoEditor
|
||||
value={null}
|
||||
disabled={true}
|
||||
useAutoCompleteList={false}
|
||||
language={MONACO_LANGUAGE.json}
|
||||
data={templates}
|
||||
monacoOptions={{
|
||||
...MONACO_PAYLOAD_OPTIONS,
|
||||
readOnly: false,
|
||||
}}
|
||||
height={getCodeEditorHeight()}
|
||||
onChange={getChangeHandler()}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className={cx('template-block-title')}>
|
||||
<HorizontalGroup justify="space-between" wrap>
|
||||
<HorizontalGroup>
|
||||
<Text>Recent Alert groups</Text>
|
||||
<Tooltip content="Here will be information about alert groups" placement="top">
|
||||
<Icon name="info-circle" />
|
||||
</Tooltip>
|
||||
</HorizontalGroup>
|
||||
|
||||
<Button variant="secondary" fill="outline" onClick={() => setIsEditMode(true)} size="sm">
|
||||
Use custom payload
|
||||
</Button>
|
||||
</HorizontalGroup>
|
||||
</div>
|
||||
<div className={cx('alert-groups-list')}>
|
||||
{alertGroupsList ? (
|
||||
<>
|
||||
{alertGroupsList?.length > 0 ? (
|
||||
<>
|
||||
{alertGroupsList.map((alertGroup) => {
|
||||
return (
|
||||
<div
|
||||
key={alertGroup.pk}
|
||||
onClick={() => getAlertGroupPayload(alertGroup.pk)}
|
||||
className={cx('alert-groups-list-item')}
|
||||
>
|
||||
<Text type="link"> {getAlertGroupName(alertGroup)}</Text>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
) : (
|
||||
<Badge
|
||||
color="blue"
|
||||
text={
|
||||
<div className={cx('no-alert-groups-badge')}>
|
||||
<Icon name="info-circle" />
|
||||
<Text>
|
||||
This integration did not receive any alerts. Use custom payload example to preview
|
||||
results.
|
||||
</Text>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<LoadingPlaceholder text="Loading alert groups..." />
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<Button variant="secondary" fill="outline" onClick={() => setIsEditMode(true)} size="sm">
|
||||
Use custom payload
|
||||
</Button>
|
||||
</HorizontalGroup>
|
||||
</div>
|
||||
<div className={cx('alert-groups-list')}>
|
||||
{templatePage === TEMPLATE_PAGE.Webhooks ? renderOutgoingWebhookLastResponses() : renderAlertGroupList()}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
function renderOutgoingWebhookLastResponses() {
|
||||
if (!outgoingWebhookLastResponses) {
|
||||
return <LoadingPlaceholder text="Loading last events..." />;
|
||||
}
|
||||
|
||||
if (outgoingWebhookLastResponses.length) {
|
||||
return outgoingWebhookLastResponses
|
||||
.filter((response) => response.event_data)
|
||||
.map((response) => {
|
||||
return (
|
||||
<div
|
||||
key={response.timestamp}
|
||||
onClick={() => handleOutgoingWebhookResponseSelect(response)}
|
||||
className={cx('alert-groups-list-item')}
|
||||
>
|
||||
<Text type="link"> {response.timestamp}</Text>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
} else {
|
||||
return (
|
||||
<Badge
|
||||
color="blue"
|
||||
text={
|
||||
<div className={cx('no-alert-groups-badge')}>
|
||||
<Icon name="info-circle" />
|
||||
<Text>
|
||||
This outgoing webhook did not receive any events yet. Use custom payload example to preview results.
|
||||
</Text>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function renderAlertGroupList() {
|
||||
if (!alertGroupsList) {
|
||||
return <LoadingPlaceholder text="Loading alert groups..." />;
|
||||
}
|
||||
|
||||
if (alertGroupsList.length) {
|
||||
return alertGroupsList.map((alertGroup) => {
|
||||
return (
|
||||
<div
|
||||
key={alertGroup.pk}
|
||||
onClick={() => getAlertGroupPayload(alertGroup.pk)}
|
||||
className={cx('alert-groups-list-item')}
|
||||
>
|
||||
<Text type="link"> {getAlertGroupName(alertGroup)}</Text>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
} else {
|
||||
return (
|
||||
<Badge
|
||||
color="blue"
|
||||
text={
|
||||
<div className={cx('no-alert-groups-badge')}>
|
||||
<Icon name="info-circle" />
|
||||
<Text>This integration did not receive any alerts. Use custom payload example to preview results.</Text>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function renderSelectedPayloadInEditMode() {
|
||||
return (
|
||||
<>
|
||||
<div className={cx('template-block-title-edit-mode')}>
|
||||
<HorizontalGroup justify="space-between">
|
||||
<Text>Edit custom payload</Text>
|
||||
|
||||
<HorizontalGroup>
|
||||
<IconButton name="times" onClick={() => returnToListView()} />
|
||||
</HorizontalGroup>
|
||||
</HorizontalGroup>
|
||||
</div>
|
||||
<div className={cx('alert-groups-editor')}>
|
||||
<MonacoEditor
|
||||
value={JSON.stringify(selectedPayload, null, 4)}
|
||||
data={templates}
|
||||
height={getCodeEditorHeight()}
|
||||
onChange={getChangeHandler()}
|
||||
showLineNumbers
|
||||
useAutoCompleteList={false}
|
||||
language={MONACO_LANGUAGE.json}
|
||||
monacoOptions={MONACO_EDITABLE_CONFIG}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function renderSelectedPayloadInReadOnlyMode() {
|
||||
return (
|
||||
<>
|
||||
<div className={cx('template-block-title')}>
|
||||
<div className={cx('selected-alert-name-container')}>
|
||||
<div className={cx('selected-alert-name')}>
|
||||
<Text>{selectedTitle}</Text>
|
||||
</div>
|
||||
<div className={cx('title-action-icons')}>
|
||||
<IconButton name="edit" onClick={() => setIsEditMode(true)} />
|
||||
<IconButton name="times" onClick={() => returnToListView()} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={cx('alert-groups-editor')}>
|
||||
<TooltipBadge
|
||||
borderType="primary"
|
||||
text="Payload"
|
||||
tooltipTitle=""
|
||||
tooltipContent=""
|
||||
className={cx('alert-groups-last-payload-badge')}
|
||||
/>
|
||||
<div className={cx('alert-groups-editor-withBadge')}>
|
||||
{/* Editor used for Editing Given Payload */}
|
||||
<MonacoEditor
|
||||
value={JSON.stringify(selectedPayload, null, 4)}
|
||||
data={undefined}
|
||||
disabled
|
||||
height={getCodeEditorHeightWithBadge()}
|
||||
onChange={getChangeHandler()}
|
||||
useAutoCompleteList={false}
|
||||
language={MONACO_LANGUAGE.json}
|
||||
monacoOptions={{
|
||||
...MONACO_EDITABLE_CONFIG,
|
||||
readOnly: true,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default TemplatesAlertGroupsList;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,62 @@
|
|||
export const WebhooksDefaultAlertGroup = {
|
||||
pk: '0',
|
||||
event: {
|
||||
type: 'resolve',
|
||||
time: '2023-04-19T21:59:21.714058+00:00',
|
||||
},
|
||||
user: {
|
||||
id: 'UVMX6YI9VY9PV',
|
||||
username: 'admin',
|
||||
email: 'admin@localhost',
|
||||
},
|
||||
alert_group: {
|
||||
id: 'I6HNZGUFG4K11',
|
||||
integration_id: 'CZ7URAT4V3QF2',
|
||||
route_id: 'RKHXJKVZYYVST',
|
||||
alerts_count: 1,
|
||||
state: 'resolved',
|
||||
created_at: '2023-04-19T21:53:48.231148Z',
|
||||
resolved_at: '2023-04-19T21:59:21.714058Z',
|
||||
acknowledged_at: '2023-04-19T21:54:39.029347Z',
|
||||
title: 'Incident',
|
||||
permalinks: {
|
||||
slack: null,
|
||||
telegram: null,
|
||||
web: 'https://**********.grafana.net/a/grafana-oncall-app/alert-groups/I6HNZGUFG4K11',
|
||||
},
|
||||
},
|
||||
alert_group_id: 'I6HNZGUFG4K11',
|
||||
alert_payload: {
|
||||
endsAt: '0001-01-01T00:00:00Z',
|
||||
labels: {
|
||||
region: 'eu-1',
|
||||
alertname: 'TestAlert',
|
||||
},
|
||||
status: 'firing',
|
||||
startsAt: '2018-12-25T15:47:47.377363608Z',
|
||||
amixr_demo: true,
|
||||
annotations: {
|
||||
description: 'This alert was sent by user for the demonstration purposes',
|
||||
},
|
||||
generatorURL: '',
|
||||
},
|
||||
integration: {
|
||||
id: 'CZ7URAT4V3QF2',
|
||||
type: 'webhook',
|
||||
name: 'Main Integration - Webhook',
|
||||
team: 'Webhooks Demo',
|
||||
},
|
||||
notified_users: [],
|
||||
users_to_be_notified: [],
|
||||
responses: {
|
||||
WHP936BM1GPVHQ: {
|
||||
id: '7Qw7TbPmzppRnhLvK3AdkQ',
|
||||
created_at: '15:53:50',
|
||||
status: 'new',
|
||||
content: {
|
||||
message: 'Ticket created!',
|
||||
region: 'eu',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
@ -0,0 +1,176 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import { Button, Drawer, HorizontalGroup, VerticalGroup } from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
import { debounce } from 'lodash-es';
|
||||
|
||||
import CheatSheet from 'components/CheatSheet/CheatSheet';
|
||||
import MonacoEditor from 'components/MonacoEditor/MonacoEditor';
|
||||
import Text from 'components/Text/Text';
|
||||
import styles from 'containers/IntegrationTemplate/IntegrationTemplate.module.scss';
|
||||
import TemplateResult from 'containers/TemplateResult/TemplateResult';
|
||||
import TemplatesAlertGroupsList, { TEMPLATE_PAGE } from 'containers/TemplatesAlertGroupsList/TemplatesAlertGroupsList';
|
||||
import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip';
|
||||
import { OutgoingWebhook2 } from 'models/outgoing_webhook_2/outgoing_webhook_2.types';
|
||||
import { waitForElement } from 'utils/DOM';
|
||||
import { UserActions } from 'utils/authorization';
|
||||
|
||||
const cx = cn.bind(styles);
|
||||
|
||||
interface Template {
|
||||
value: string;
|
||||
displayName: string;
|
||||
description: string;
|
||||
name: undefined;
|
||||
}
|
||||
|
||||
interface WebhooksTemplateEditorProps {
|
||||
template: Template;
|
||||
id: OutgoingWebhook2['id'];
|
||||
onHide: () => void;
|
||||
handleSubmit: (template: string) => void;
|
||||
}
|
||||
|
||||
const WebhooksTemplateEditor: React.FC<WebhooksTemplateEditorProps> = ({ template, id, onHide, handleSubmit }) => {
|
||||
const [isCheatSheetVisible] = useState(false);
|
||||
const [changedTemplateBody, setChangedTemplateBody] = useState<string>(template.value);
|
||||
const [editorHeight, setEditorHeight] = useState<string>(undefined);
|
||||
const [selectedPayload, setSelectedPayload] = useState(undefined);
|
||||
const [resultError, setResultError] = useState<string>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
waitForElement('#content-container-id').then(() => {
|
||||
const mainDiv = document.getElementById('content-container-id');
|
||||
const height = mainDiv?.getBoundingClientRect().height - 59;
|
||||
setEditorHeight(`${height}px`);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const getChangeHandler = () => {
|
||||
return debounce((value: string) => {
|
||||
setChangedTemplateBody(value);
|
||||
}, 500);
|
||||
};
|
||||
|
||||
const onEditPayload = (alertPayload: string) => {
|
||||
if (alertPayload !== null) {
|
||||
try {
|
||||
const jsonPayload = JSON.parse(alertPayload);
|
||||
if (typeof jsonPayload === 'object') {
|
||||
setResultError(undefined);
|
||||
setSelectedPayload(JSON.parse(alertPayload));
|
||||
} else {
|
||||
setResultError('Please check your JSON format');
|
||||
}
|
||||
} catch (e) {
|
||||
setResultError(e.message);
|
||||
}
|
||||
} else {
|
||||
setResultError(undefined);
|
||||
setSelectedPayload(undefined);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
title={
|
||||
<div className={cx('title-container')}>
|
||||
<HorizontalGroup justify="space-between" align="flex-start">
|
||||
<VerticalGroup>
|
||||
<Text.Title level={3}>Edit {template.displayName} template</Text.Title>
|
||||
{template.description && <Text type="secondary">{template.description}</Text>}
|
||||
</VerticalGroup>
|
||||
|
||||
<HorizontalGroup>
|
||||
<WithPermissionControlTooltip userAction={UserActions.OutgoingWebhooksWrite}>
|
||||
<Button variant="secondary" onClick={onHide}>
|
||||
Cancel
|
||||
</Button>
|
||||
</WithPermissionControlTooltip>
|
||||
<WithPermissionControlTooltip userAction={UserActions.OutgoingWebhooksWrite}>
|
||||
<Button variant="primary" onClick={() => handleSubmit(changedTemplateBody)}>
|
||||
Save
|
||||
</Button>
|
||||
</WithPermissionControlTooltip>
|
||||
</HorizontalGroup>
|
||||
</HorizontalGroup>
|
||||
</div>
|
||||
}
|
||||
onClose={onHide}
|
||||
closeOnMaskClick={false}
|
||||
width="95%"
|
||||
>
|
||||
<div className={cx('container-wrapper')}>
|
||||
<div className={cx('container')} id={'content-container-id'}>
|
||||
<TemplatesAlertGroupsList
|
||||
heading="Last events"
|
||||
templatePage={TEMPLATE_PAGE.Webhooks}
|
||||
outgoingwebhookId={id}
|
||||
onEditPayload={onEditPayload}
|
||||
templates={
|
||||
{
|
||||
// TODO: this is just some dummy data, this will need replaced with an actual Webhook Template
|
||||
acknowledge_condition_template: null,
|
||||
acknowledge_condition_template_is_default: true,
|
||||
} as any
|
||||
}
|
||||
onLoadAlertGroupsList={(_isRecentAlertExisting: boolean) => {}}
|
||||
/>
|
||||
|
||||
{isCheatSheetVisible ? (
|
||||
<CheatSheet
|
||||
cheatSheetName={template.displayName}
|
||||
cheatSheetData={getCheatSheet(template.name)}
|
||||
onClose={onCloseCheatSheet}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<div className={cx('template-block-codeeditor')}>
|
||||
<div className={cx('template-editor-block-title')}>
|
||||
<HorizontalGroup justify="space-between" align="center" wrap>
|
||||
<Text>Template editor</Text>
|
||||
|
||||
{/* <Button variant="secondary" fill="outline" onClick={onShowCheatSheet} icon="book" size="sm">
|
||||
Cheatsheet
|
||||
</Button> */}
|
||||
</HorizontalGroup>
|
||||
</div>
|
||||
<div className={cx('template-editor-block-content')}>
|
||||
<MonacoEditor
|
||||
value={template.value}
|
||||
data={{ payload_example: selectedPayload }}
|
||||
showLineNumbers={true}
|
||||
height={editorHeight}
|
||||
onChange={getChangeHandler()}
|
||||
suggestionPrefix=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<TemplateResult
|
||||
templatePage={TEMPLATE_PAGE.Webhooks}
|
||||
outgoingWebhookId={id}
|
||||
template={template}
|
||||
templateBody={changedTemplateBody}
|
||||
isAlertGroupExisting={false}
|
||||
chatOpsPermalink={undefined}
|
||||
payload={selectedPayload}
|
||||
error={resultError}
|
||||
onSaveAndFollowLink={undefined}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Drawer>
|
||||
);
|
||||
|
||||
// function onShowCheatSheet() {}
|
||||
|
||||
function onCloseCheatSheet() {}
|
||||
|
||||
function getCheatSheet(_templateName: string) {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
export default WebhooksTemplateEditor;
|
||||
|
|
@ -94,4 +94,17 @@ export class OutgoingWebhook2Store extends BaseStore {
|
|||
|
||||
return this.searchResult[query].map((outgoingWebhook2Id: OutgoingWebhook2['id']) => this.items[outgoingWebhook2Id]);
|
||||
}
|
||||
|
||||
async getLastResponses(id: OutgoingWebhook2['id']) {
|
||||
const result = await makeRequest(`${this.path}${id}/responses`, {});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async renderPreview(id: OutgoingWebhook2['id'], template_name: string, template_body: string, payload) {
|
||||
return await makeRequest(`${this.path}${id}/preview_template/`, {
|
||||
method: 'POST',
|
||||
data: { template_name, template_body, payload },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,4 +28,5 @@ export interface OutgoingWebhook2Response {
|
|||
request_data: string;
|
||||
status_code: string;
|
||||
content: string;
|
||||
event_data: string;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ import IntegrationInputField from 'components/IntegrationInputField/IntegrationI
|
|||
import IntegrationLogo from 'components/IntegrationLogo/IntegrationLogo';
|
||||
import IntegrationBlock from 'components/Integrations/IntegrationBlock';
|
||||
import MonacoEditor, { MONACO_LANGUAGE } from 'components/MonacoEditor/MonacoEditor';
|
||||
import { MONACO_EDITABLE_CONFIG } from 'components/MonacoEditor/MonacoEditor.config';
|
||||
import PageErrorHandlingWrapper, { PageBaseState } from 'components/PageErrorHandlingWrapper/PageErrorHandlingWrapper';
|
||||
import { initErrorDataState } from 'components/PageErrorHandlingWrapper/PageErrorHandlingWrapper.helpers';
|
||||
import PluginLink from 'components/PluginLink/PluginLink';
|
||||
|
|
@ -71,8 +72,6 @@ import { UserActions } from 'utils/authorization';
|
|||
import { PLUGIN_ROOT } from 'utils/consts';
|
||||
import sanitize from 'utils/sanitize';
|
||||
|
||||
import { MONACO_PAYLOAD_OPTIONS } from './IntegrationCommon.config';
|
||||
|
||||
const cx = cn.bind(styles);
|
||||
|
||||
interface IntegrationProps extends WithStoreProps, PageProps, RouteComponentProps<{ id: string }> {}
|
||||
|
|
@ -673,7 +672,7 @@ const IntegrationSendDemoPayloadModal: React.FC<IntegrationSendDemoPayloadModalP
|
|||
useAutoCompleteList={false}
|
||||
language={MONACO_LANGUAGE.json}
|
||||
data={undefined}
|
||||
monacoOptions={MONACO_PAYLOAD_OPTIONS}
|
||||
monacoOptions={MONACO_EDITABLE_CONFIG}
|
||||
showLineNumbers={false}
|
||||
onChange={onPayloadChangeDebounced}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -3,33 +3,6 @@ import { KeyValuePair } from 'utils';
|
|||
export const TEXTAREA_ROWS_COUNT = 4;
|
||||
export const MAX_CHARACTERS_COUNT = 50;
|
||||
|
||||
// Mostly used for input fields where we're hiding scrollbars
|
||||
export const MONACO_OPTIONS = {
|
||||
renderLineHighlight: false,
|
||||
readOnly: true,
|
||||
scrollbar: {
|
||||
vertical: 'hidden',
|
||||
horizontal: 'hidden',
|
||||
verticalScrollbarSize: 0,
|
||||
handleMouseWheel: false,
|
||||
},
|
||||
hideCursorInOverviewRuler: true,
|
||||
minimap: { enabled: false },
|
||||
cursorStyle: {
|
||||
display: 'none',
|
||||
},
|
||||
};
|
||||
|
||||
export const MONACO_PAYLOAD_OPTIONS = {
|
||||
renderLineHighlight: false,
|
||||
readOnly: false,
|
||||
hideCursorInOverviewRuler: true,
|
||||
minimap: { enabled: false },
|
||||
cursorStyle: {
|
||||
display: 'none',
|
||||
},
|
||||
};
|
||||
|
||||
export const MONACO_INPUT_HEIGHT_SMALL = '32px';
|
||||
export const MONACO_INPUT_HEIGHT_TALL = '120px';
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue