commit
61c44741fd
35 changed files with 386 additions and 166 deletions
11
CHANGELOG.md
11
CHANGELOG.md
|
|
@ -5,6 +5,17 @@ All notable changes to this project will be documented in this file.
|
|||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## v1.2.34 (2023-05-31)
|
||||
|
||||
### Added
|
||||
|
||||
- Add description to "Default channel for Slack notifications" UI dropdown by @joeyorlando ([2051](https://github.com/grafana/oncall/pull/2051))
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix templates when slack or telegram is disabled ([#2064](https://github.com/grafana/oncall/pull/2064))
|
||||
- Reduce number of alert groups returned by `Attach To` in slack to avoid event trigger timeout @mderynck ([#2049](https://github.com/grafana/oncall/pull/2049))
|
||||
|
||||
## v1.2.33 (2023-05-30)
|
||||
|
||||
### Fixed
|
||||
|
|
|
|||
|
|
@ -13,6 +13,9 @@ weight: 500
|
|||
# On-call status and shifts
|
||||
|
||||
On the **Feed** page, your avatar on top of the screen indicates whether you are oncall, will be on call soon, or not.
|
||||
Tap on it to open the **upcoming shifts** view. This view presents your current, and next upcoming shifts (if any), up to 1 month into the future.
|
||||
Tap it to open the **upcoming shifts** view. This view displays:
|
||||
|
||||
<img src="/static/img/oncall/mobile-app-shifts.png" width="300px">
|
||||
- your current shift (if any), per schedule.
|
||||
- your next upcoming shift (if any), per schedule. This only looks up to 31 days into the future.
|
||||
|
||||
<img src="/static/img/oncall/mobile-app-v1-android-shifts.png" width="300px">
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ To receive push notifications from the Grafana OnCall mobile app, you must add t
|
|||
In the **Settings** tab of the mobile app, tap on **Notification policies** to review, reorder, remove, add or change steps.
|
||||
Alternatively, you can do the same on desktop. From Grafana OnCall, navigate to the **Users** page, click **View my profile** and navigate to the **User Info** tab.
|
||||
|
||||
<img src="/static/img/oncall/mobile-app-notification-policies.png" width="300px">
|
||||
<img src="/static/img/oncall/mobile-app-v1-android-notification-policies.png" width="300px">
|
||||
|
||||
## Configuration
|
||||
|
||||
|
|
@ -33,20 +33,31 @@ Use the **Push notifications** section in the **Settings** tab to configure push
|
|||
|
||||
You can always confirm how a notification is presented by going to Grafana OnCall on your desktop,
|
||||
navigate to the **Users** page, click **View my profile** and navigate to the **Mobile App connection** tab.
|
||||
Here you can send a test notification of default or important priority.
|
||||
Here you can send a test notification of default or important priority. We recommend doing this to try out
|
||||
correct configuration of **Do Not Disturb** and **Volume** overrides.
|
||||
|
||||
### Android
|
||||
|
||||
On Android, we leverage the "Notification channels" system feature.
|
||||
Each type of notification (**important**, **default**, and **on-call shifts**) registers a channel.
|
||||
In this channel, you may configure the sound style, optional Do Not Disturb override, vibration, and so on.
|
||||
In this channel, you may configure the sound style, vibration, and so on.
|
||||
**Customize notifications** takes you to this system menu, while hitting the **back** button or swiping left (if enabled) takes you back to the application.
|
||||
|
||||
>**Note**: You can explore included sounds and recommendations via the **Sound Library** button, but to change the sound, go to **Customize notifications**.
|
||||
|
||||
#### Override Do Not Disturb
|
||||
|
||||
- On most Android versions, the **Override Do Not Disturb** option is available in the channel options described above.
|
||||
- On some Samsung devices, you can add the Grafana Oncall app under (System) Settings > Notifications > Do not disturb > App notifications.
|
||||
- If your device does something different, you may need to search for this setting for notifications via the (System) Settings app.
|
||||
Do not confuse this with the **Override Do Not Disturb** application permission, needed for **Volume Overrides** (see below).
|
||||
|
||||
#### Override Volume
|
||||
|
||||
**Volume Override** can optionally be configured in the mobile app itself.
|
||||
Confusingly, this requires you to provide the **Override Do Not Disturb** permission to the application, in the system configuration.
|
||||
The app will prompt for this if applicable.
|
||||
|
||||
>**Note**: You can explore included sounds and recommendations via the **Sound Library** button, but to change the sound, go to **Customize notifications**.
|
||||
The app will prompt for this if applicable. Note that this is a different setting than the **Do Not Disturb** override needed for
|
||||
notifications triggered by the application, which is described above.
|
||||
|
||||
<img src="/static/img/oncall/mobile-app-v1-android-settings.png" width="300px">
|
||||
<img src="/static/img/oncall/mobile-app-v1-android-push-notifications-prompt.png" width="300px">
|
||||
|
|
|
|||
|
|
@ -49,6 +49,11 @@ class IntegrationOptionsMixin:
|
|||
integration_config.slug: integration_config.short_description for integration_config in _config
|
||||
}
|
||||
INTEGRATION_FEATURED = [integration_config.slug for integration_config in _config if integration_config.is_featured]
|
||||
INTEGRATION_FEATURED_TAG_NAME = {
|
||||
integration_config.slug: integration_config.featured_tag_name
|
||||
for integration_config in _config
|
||||
if hasattr(integration_config, "featured_tag_name")
|
||||
}
|
||||
|
||||
# The following attributes dynamically generated and used by apps.alerts.incident_appearance.renderers, templaters
|
||||
# e.g. INTEGRATION_TO_DEFAULT_SLACK_TITLE_TEMPLATE, INTEGRATION_TO_DEFAULT_SLACK_MESSAGE_TEMPLATE, etc...
|
||||
|
|
|
|||
|
|
@ -255,6 +255,7 @@ def notify_ical_schedule_shift(schedule_pk):
|
|||
|
||||
for prev_ical_file, current_ical_file in prev_and_current_ical_files:
|
||||
if prev_ical_file and (not current_ical_file or not is_icals_equal(current_ical_file, prev_ical_file)):
|
||||
task_logger.info(f"ical files are different")
|
||||
# If icals are not equal then compare current_events from them
|
||||
is_prev_ical_diff = True
|
||||
prev_calendar = icalendar.Calendar.from_ical(prev_ical_file)
|
||||
|
|
@ -295,6 +296,7 @@ def notify_ical_schedule_shift(schedule_pk):
|
|||
shift_changed, diff_uids = calculate_shift_diff(current_shifts, prev_shifts)
|
||||
|
||||
if shift_changed:
|
||||
task_logger.info(f"shifts_changed: {diff_uids}")
|
||||
# Get only new/changed shifts to send a reminder message.
|
||||
new_shifts = []
|
||||
for uid in diff_uids:
|
||||
|
|
@ -361,6 +363,7 @@ def notify_ical_schedule_shift(schedule_pk):
|
|||
schedule.save(update_fields=["current_shifts", "empty_oncall"])
|
||||
|
||||
if len(new_shifts) > 0 or empty_oncall:
|
||||
task_logger.info(f"new_shifts: {new_shifts}")
|
||||
slack_client = SlackClientWithErrorHandling(schedule.organization.slack_team_identity.bot_access_token)
|
||||
step = scenario_step.ScenarioStep.get_step("schedules", "EditScheduleShiftNotifyStep")
|
||||
report_blocks = step.get_report_blocks_ical(new_shifts, upcoming_shifts, schedule, empty_oncall)
|
||||
|
|
|
|||
|
|
@ -216,23 +216,6 @@ class FilterAlertReceiveChannelSerializer(serializers.ModelSerializer):
|
|||
|
||||
class AlertReceiveChannelTemplatesSerializer(EagerLoadingMixin, serializers.ModelSerializer):
|
||||
id = serializers.CharField(read_only=True, source="public_primary_key")
|
||||
CORE_TEMPLATE_NAMES = [
|
||||
"slack_title_template",
|
||||
"slack_message_template",
|
||||
"slack_image_url_template",
|
||||
"web_title_template",
|
||||
"web_message_template",
|
||||
"web_image_url_template",
|
||||
"telegram_title_template",
|
||||
"telegram_message_template",
|
||||
"telegram_image_url_template",
|
||||
"sms_title_template",
|
||||
"phone_call_title_template",
|
||||
"source_link_template",
|
||||
"grouping_id_template",
|
||||
"resolve_condition_template",
|
||||
"acknowledge_condition_template",
|
||||
]
|
||||
|
||||
payload_example = SerializerMethodField()
|
||||
is_based_on_alertmanager = SerializerMethodField()
|
||||
|
|
@ -325,9 +308,7 @@ class AlertReceiveChannelTemplatesSerializer(EagerLoadingMixin, serializers.Mode
|
|||
"""Update core templates if needed."""
|
||||
errors = {}
|
||||
|
||||
core_template_names = self.CORE_TEMPLATE_NAMES
|
||||
|
||||
for field_name in core_template_names:
|
||||
for field_name in self.core_templates_names:
|
||||
value = data.get(field_name)
|
||||
validator = jinja_template_env.from_string
|
||||
if value is not None:
|
||||
|
|
@ -343,7 +324,6 @@ class AlertReceiveChannelTemplatesSerializer(EagerLoadingMixin, serializers.Mode
|
|||
|
||||
def to_representation(self, obj):
|
||||
ret = super().to_representation(obj)
|
||||
ret = self._get_templates_to_show(ret)
|
||||
|
||||
core_templates = self._get_core_templates(obj)
|
||||
ret.update(core_templates)
|
||||
|
|
@ -354,29 +334,6 @@ class AlertReceiveChannelTemplatesSerializer(EagerLoadingMixin, serializers.Mode
|
|||
|
||||
return ret
|
||||
|
||||
def _get_templates_to_show(self, response_data):
|
||||
"""
|
||||
For On-prem installations with disabled features it is needed to disable corresponding templates
|
||||
"""
|
||||
slack_integration_required_templates = [
|
||||
"slack_title_template",
|
||||
"slack_message_template",
|
||||
"slack_image_url_template",
|
||||
]
|
||||
telegram_integration_required_templates = [
|
||||
"telegram_title_template",
|
||||
"telegram_message_template",
|
||||
"telegram_image_url_template",
|
||||
]
|
||||
if not settings.FEATURE_SLACK_INTEGRATION_ENABLED:
|
||||
for st in slack_integration_required_templates:
|
||||
response_data.pop(st)
|
||||
if not settings.FEATURE_TELEGRAM_INTEGRATION_ENABLED:
|
||||
for tt in telegram_integration_required_templates:
|
||||
response_data.pop(tt)
|
||||
|
||||
return response_data
|
||||
|
||||
def _get_messaging_backend_templates(self, obj):
|
||||
"""Return additional messaging backend templates if any."""
|
||||
templates = {}
|
||||
|
|
@ -399,8 +356,7 @@ class AlertReceiveChannelTemplatesSerializer(EagerLoadingMixin, serializers.Mode
|
|||
def _get_core_templates(self, obj):
|
||||
core_templates = {}
|
||||
|
||||
core_template_names = self.CORE_TEMPLATE_NAMES
|
||||
for template_name in core_template_names:
|
||||
for template_name in self.core_templates_names:
|
||||
template_value = getattr(obj, template_name)
|
||||
defaults = getattr(obj, f"INTEGRATION_TO_DEFAULT_{template_name.upper()}", {})
|
||||
default_template_value = defaults.get(obj.integration)
|
||||
|
|
@ -408,3 +364,40 @@ class AlertReceiveChannelTemplatesSerializer(EagerLoadingMixin, serializers.Mode
|
|||
core_templates[f"{template_name}_is_default"] = not bool(template_value)
|
||||
|
||||
return core_templates
|
||||
|
||||
@property
|
||||
def core_templates_names(self):
|
||||
"""
|
||||
core_templates_names returns names of templates introduced before messaging backends system with respect to
|
||||
enabled integrations.
|
||||
"""
|
||||
core_templates = [
|
||||
"web_title_template",
|
||||
"web_message_template",
|
||||
"web_image_url_template",
|
||||
"sms_title_template",
|
||||
"phone_call_title_template",
|
||||
"source_link_template",
|
||||
"grouping_id_template",
|
||||
"resolve_condition_template",
|
||||
"acknowledge_condition_template",
|
||||
]
|
||||
|
||||
slack_integration_required_templates = [
|
||||
"slack_title_template",
|
||||
"slack_message_template",
|
||||
"slack_image_url_template",
|
||||
]
|
||||
telegram_integration_required_templates = [
|
||||
"telegram_title_template",
|
||||
"telegram_message_template",
|
||||
"telegram_image_url_template",
|
||||
]
|
||||
|
||||
apppend = []
|
||||
|
||||
if settings.FEATURE_SLACK_INTEGRATION_ENABLED:
|
||||
core_templates += slack_integration_required_templates
|
||||
if settings.FEATURE_TELEGRAM_INTEGRATION_ENABLED:
|
||||
core_templates += telegram_integration_required_templates
|
||||
return apppend + core_templates
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from django.test.utils import override_settings
|
||||
from django.urls import reverse
|
||||
from rest_framework import status
|
||||
from rest_framework.response import Response
|
||||
|
|
@ -381,3 +382,44 @@ def test_update_alert_receive_channel_templates(
|
|||
assert updated_templates_data[template_name] is False
|
||||
else:
|
||||
assert updated_templates_data[template_name] == template_update_func(prev_template_value)
|
||||
|
||||
|
||||
@override_settings(FEATURE_TELEGRAM_INTEGRATION_ENABLED=False)
|
||||
@override_settings(FEATURE_SLACK_INTEGRATION_ENABLED=False)
|
||||
@pytest.mark.django_db
|
||||
def test_update_alert_receive_channel_backend_template_hide_disabled_integration_templates(
|
||||
make_organization_and_user_with_plugin_token,
|
||||
make_user_auth_headers,
|
||||
make_alert_receive_channel,
|
||||
):
|
||||
slack_integration_required_templates = [
|
||||
"slack_title_template",
|
||||
"slack_message_template",
|
||||
"slack_image_url_template",
|
||||
]
|
||||
telegram_integration_required_templates = [
|
||||
"telegram_title_template",
|
||||
"telegram_message_template",
|
||||
"telegram_image_url_template",
|
||||
]
|
||||
|
||||
organization, user, token = make_organization_and_user_with_plugin_token()
|
||||
alert_receive_channel = make_alert_receive_channel(
|
||||
organization,
|
||||
messaging_backends_templates={"TESTONLY": {"title": "the-title", "message": "the-message", "image_url": "url"}},
|
||||
)
|
||||
client = APIClient()
|
||||
|
||||
url = reverse(
|
||||
"api-internal:alert_receive_channel_template-detail", kwargs={"pk": alert_receive_channel.public_primary_key}
|
||||
)
|
||||
|
||||
response = client.get(url, format="json", **make_user_auth_headers(user, token))
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
templates_data = response.json()
|
||||
|
||||
for st in slack_integration_required_templates:
|
||||
assert st not in templates_data
|
||||
|
||||
for tt in telegram_integration_required_templates:
|
||||
assert tt not in templates_data
|
||||
|
|
|
|||
|
|
@ -198,6 +198,9 @@ class AlertReceiveChannelView(
|
|||
"display_name": integration_title,
|
||||
"short_description": AlertReceiveChannel.INTEGRATION_SHORT_DESCRIPTION[integration_id],
|
||||
"featured": integration_id in AlertReceiveChannel.INTEGRATION_FEATURED,
|
||||
"featured_tag_name": AlertReceiveChannel.INTEGRATION_FEATURED_TAG_NAME[integration_id]
|
||||
if integration_id in AlertReceiveChannel.INTEGRATION_FEATURED_TAG_NAME
|
||||
else None,
|
||||
}
|
||||
# if integration is featured we show it in the beginning
|
||||
if choice["featured"]:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
# Generated by Django 3.2.19 on 2023-05-30 16:45
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('user_management', '0011_auto_20230411_1358'),
|
||||
('email', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='emailmessage',
|
||||
name='receiver',
|
||||
field=models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='user_management.user'),
|
||||
),
|
||||
]
|
||||
|
|
@ -16,5 +16,5 @@ class EmailMessage(models.Model):
|
|||
"base.UserNotificationPolicy", on_delete=models.SET_NULL, null=True, default=None
|
||||
)
|
||||
|
||||
receiver = models.ForeignKey("user_management.User", on_delete=models.PROTECT, null=True, default=None)
|
||||
receiver = models.ForeignKey("user_management.User", on_delete=models.CASCADE, null=True, default=None)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
|
|
|||
|
|
@ -37,6 +37,8 @@ from common.utils import clean_markup, is_string_with_visible_characters
|
|||
|
||||
from .step_mixins import CheckAlertIsUnarchivedMixin, IncidentActionsAccessControlMixin
|
||||
|
||||
ATTACH_TO_ALERT_GROUPS_LIMIT = 20
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
|
|
@ -398,7 +400,7 @@ class SelectAttachGroupStep(
|
|||
.order_by("-pk")
|
||||
)
|
||||
|
||||
for alert_group_to_attach in alert_groups_queryset[:60]:
|
||||
for alert_group_to_attach in alert_groups_queryset[:ATTACH_TO_ALERT_GROUPS_LIMIT]:
|
||||
# long_verbose_name_without_formatting was removed from here because it increases queries count due to
|
||||
# alerts.first().
|
||||
# alert_group_to_attach.alerts.exists() and alerts.all()[0] don't make additional queries to db due to
|
||||
|
|
@ -435,7 +437,7 @@ class SelectAttachGroupStep(
|
|||
"text": "Attach to...",
|
||||
},
|
||||
"action_id": AttachGroupStep.routing_uid(),
|
||||
"options": collected_options[:60],
|
||||
"options": collected_options[:ATTACH_TO_ALERT_GROUPS_LIMIT],
|
||||
},
|
||||
"label": {
|
||||
"type": "plain_text",
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ short_description = (
|
|||
description = None
|
||||
is_displayed_on_web = True
|
||||
is_featured = True
|
||||
featured_tag_name = "Quick Connect"
|
||||
is_able_to_autoresolve = True
|
||||
is_demo_alert_enabled = True
|
||||
|
||||
|
|
|
|||
|
|
@ -2,9 +2,10 @@
|
|||
enabled = True
|
||||
title = "Webhook"
|
||||
slug = "webhook"
|
||||
short_description = None
|
||||
short_description = "If your monitoring system isn't listed, choose Webhook for generic templates, and feel free to modify them as needed."
|
||||
description = None
|
||||
is_featured = False
|
||||
is_featured = True
|
||||
featured_tag_name = "Generic"
|
||||
is_displayed_on_web = True
|
||||
is_able_to_autoresolve = True
|
||||
is_demo_alert_enabled = True
|
||||
|
|
|
|||
|
|
@ -6,6 +6,12 @@
|
|||
min-width: min-content;
|
||||
}
|
||||
|
||||
.cheatsheet-container > div {
|
||||
height: 100%;
|
||||
max-height: 100%;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.cheatsheet-item {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,8 +11,8 @@ const cx = cn.bind(styles);
|
|||
export interface IntegrationCollapsibleItem {
|
||||
customIcon?: IconName;
|
||||
canHoverIcon: boolean;
|
||||
expandedView: React.ReactNode;
|
||||
collapsedView: React.ReactNode;
|
||||
collapsedView: (toggle?: () => void) => React.ReactNode; // needs toggle param for toggling on click
|
||||
expandedView: () => React.ReactNode; // for consistency, this is also a function
|
||||
isCollapsible: boolean;
|
||||
isExpanded?: boolean;
|
||||
onStateChange?(): void;
|
||||
|
|
@ -116,10 +116,10 @@ const IntegrationCollapsibleTreeItem: React.FC<{
|
|||
)}
|
||||
</div>
|
||||
<div className={cx('integrationTree__element', { 'integrationTree__element--visible': isExpanded })}>
|
||||
{item.expandedView}
|
||||
{item.expandedView?.()}
|
||||
</div>
|
||||
<div className={cx('integrationTree__element', { 'integrationTree__element--visible': !isExpanded })}>
|
||||
{item.collapsedView}
|
||||
{item.collapsedView?.(onClick)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import React from 'react';
|
||||
|
||||
import cn from 'classnames/bind';
|
||||
import { noop } from 'lodash-es';
|
||||
|
||||
import Block from 'components/GBlock/Block';
|
||||
|
||||
|
|
@ -13,13 +14,20 @@ interface IntegrationBlockProps {
|
|||
hasCollapsedBorder: boolean;
|
||||
heading: React.ReactNode;
|
||||
content: React.ReactNode;
|
||||
toggle?: () => void;
|
||||
}
|
||||
|
||||
const IntegrationBlock: React.FC<IntegrationBlockProps> = ({ heading, content, hasCollapsedBorder, className }) => {
|
||||
const IntegrationBlock: React.FC<IntegrationBlockProps> = ({
|
||||
heading,
|
||||
content,
|
||||
hasCollapsedBorder,
|
||||
className,
|
||||
toggle = noop,
|
||||
}) => {
|
||||
return (
|
||||
<div className={cx('integrationBlock', className)}>
|
||||
{heading && (
|
||||
<Block bordered shadowed className={cx('integrationBlock__heading')}>
|
||||
<Block bordered shadowed className={cx('integrationBlock__heading')} onClick={toggle}>
|
||||
{heading}
|
||||
</Block>
|
||||
)}
|
||||
|
|
@ -28,6 +36,7 @@ const IntegrationBlock: React.FC<IntegrationBlockProps> = ({ heading, content, h
|
|||
className={cx('integrationBlock__content', {
|
||||
'integrationBlock__content--collapsedBorder': hasCollapsedBorder,
|
||||
})}
|
||||
onClick={toggle}
|
||||
>
|
||||
{content}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -292,6 +292,7 @@ class AlertRules extends React.Component<AlertRulesProps, AlertRulesState> {
|
|||
)}
|
||||
</div>
|
||||
</Block>
|
||||
|
||||
{alertReceiveChannel.description && (
|
||||
<div className={cx('description-style')}>
|
||||
<Alert
|
||||
|
|
|
|||
|
|
@ -21,10 +21,11 @@ interface CollapsedIntegrationRouteDisplayProps {
|
|||
alertReceiveChannelId: AlertReceiveChannel['id'];
|
||||
channelFilterId: ChannelFilter['id'];
|
||||
routeIndex: number;
|
||||
toggle: () => void;
|
||||
}
|
||||
|
||||
const CollapsedIntegrationRouteDisplay: React.FC<CollapsedIntegrationRouteDisplayProps> = observer(
|
||||
({ channelFilterId, alertReceiveChannelId, routeIndex }) => {
|
||||
({ channelFilterId, alertReceiveChannelId, routeIndex, toggle }) => {
|
||||
const { escalationChainStore, alertReceiveChannelStore } = useStore();
|
||||
const [routeIdForDeletion, setRouteIdForDeletion] = useState<ChannelFilter['id']>(undefined);
|
||||
|
||||
|
|
@ -44,6 +45,7 @@ const CollapsedIntegrationRouteDisplay: React.FC<CollapsedIntegrationRouteDispla
|
|||
<IntegrationBlock
|
||||
hasCollapsedBorder={false}
|
||||
key={channelFilterId}
|
||||
toggle={toggle}
|
||||
heading={
|
||||
<div className={cx('heading-container')}>
|
||||
<div className={cx('heading-container__item', 'heading-container__item--large')}>
|
||||
|
|
@ -53,7 +55,10 @@ const CollapsedIntegrationRouteDisplay: React.FC<CollapsedIntegrationRouteDispla
|
|||
alertReceiveChannelStore.channelFilterIds[alertReceiveChannelId],
|
||||
routeIndex
|
||||
)}
|
||||
tooltipTitle={undefined}
|
||||
tooltipTitle={IntegrationHelper.getRouteConditionTooltipWording(
|
||||
alertReceiveChannelStore.channelFilterIds[alertReceiveChannelId],
|
||||
routeIndex
|
||||
)}
|
||||
tooltipContent={undefined}
|
||||
/>
|
||||
{routeWording === 'Default' && (
|
||||
|
|
@ -91,7 +96,7 @@ const CollapsedIntegrationRouteDisplay: React.FC<CollapsedIntegrationRouteDispla
|
|||
|
||||
<HorizontalGroup>
|
||||
<Icon name="list-ui-alt" />
|
||||
<Text type="secondary">Escalate to</Text>
|
||||
<Text type="secondary">Trigger escalation chain:</Text>
|
||||
|
||||
{escalationChain?.name && (
|
||||
<PluginLink
|
||||
|
|
@ -110,9 +115,7 @@ const CollapsedIntegrationRouteDisplay: React.FC<CollapsedIntegrationRouteDispla
|
|||
<div className={cx('icon-exclamation')}>
|
||||
<Icon name="exclamation-triangle" />
|
||||
</div>
|
||||
<Text type="primary" strong>
|
||||
No Escalation chain
|
||||
</Text>
|
||||
<Text type="primary">No Escalation chain selected</Text>
|
||||
</HorizontalGroup>
|
||||
)}
|
||||
</HorizontalGroup>
|
||||
|
|
|
|||
|
|
@ -32,7 +32,6 @@ import { ChannelFilter } from 'models/channel_filter/channel_filter.types';
|
|||
import { EscalationChain } from 'models/escalation_chain/escalation_chain.types';
|
||||
import { MONACO_INPUT_HEIGHT_SMALL, MONACO_OPTIONS } from 'pages/integration_2/Integration2.config';
|
||||
import IntegrationHelper from 'pages/integration_2/Integration2.helper';
|
||||
import { AppFeature } from 'state/features';
|
||||
import { useStore } from 'state/useStore';
|
||||
import { UserActions } from 'utils/authorization';
|
||||
|
||||
|
|
@ -58,7 +57,6 @@ const ExpandedIntegrationRouteDisplay: React.FC<ExpandedIntegrationRouteDisplayP
|
|||
const store = useStore();
|
||||
const {
|
||||
telegramChannelStore,
|
||||
teamStore,
|
||||
escalationPolicyStore,
|
||||
escalationChainStore,
|
||||
alertReceiveChannelStore,
|
||||
|
|
@ -66,9 +64,6 @@ const ExpandedIntegrationRouteDisplay: React.FC<ExpandedIntegrationRouteDisplayP
|
|||
} = store;
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const isSlackInstalled = Boolean(teamStore.currentTeam?.slack_team_identity);
|
||||
const isTelegramInstalled =
|
||||
store.hasFeature(AppFeature.Telegram) && telegramChannelStore.currentTeamToTelegramChannel?.length > 0;
|
||||
|
||||
const [{ isEscalationCollapsed, isRefreshingEscalationChains, routeIdForDeletion }, setState] = useReducer(
|
||||
(state: ExpandedIntegrationRouteDisplayState, newState: Partial<ExpandedIntegrationRouteDisplayState>) => ({
|
||||
|
|
@ -118,7 +113,7 @@ const ExpandedIntegrationRouteDisplay: React.FC<ExpandedIntegrationRouteDisplayP
|
|||
<TooltipBadge
|
||||
borderType="success"
|
||||
text={IntegrationHelper.getRouteConditionWording(channelFilterIds, routeIndex)}
|
||||
tooltipTitle={undefined}
|
||||
tooltipTitle={IntegrationHelper.getRouteConditionTooltipWording(channelFilterIds, routeIndex)}
|
||||
tooltipContent={undefined}
|
||||
/>
|
||||
</HorizontalGroup>
|
||||
|
|
@ -134,6 +129,16 @@ const ExpandedIntegrationRouteDisplay: React.FC<ExpandedIntegrationRouteDisplayP
|
|||
}
|
||||
content={
|
||||
<VerticalGroup spacing="xs">
|
||||
{routeIndex !== channelFiltersTotal.length - 1 && (
|
||||
<IntegrationBlockItem>
|
||||
<VerticalGroup>
|
||||
<Text type="secondary">
|
||||
If the Routing Template is True, group the alerts using the Grouping Template, publish them to
|
||||
messengers, and trigger the escalation chain.
|
||||
</Text>
|
||||
</VerticalGroup>
|
||||
</IntegrationBlockItem>
|
||||
)}
|
||||
{/* Show Routing Template only for If/Else Routes, not for Default */}
|
||||
{!isDefault && (
|
||||
<IntegrationBlockItem>
|
||||
|
|
@ -170,14 +175,12 @@ const ExpandedIntegrationRouteDisplay: React.FC<ExpandedIntegrationRouteDisplayP
|
|||
</IntegrationBlockItem>
|
||||
)}
|
||||
|
||||
{(isSlackInstalled || isTelegramInstalled) && (
|
||||
<IntegrationBlockItem>
|
||||
<VerticalGroup spacing="md">
|
||||
<Text type="primary">Publish to ChatOps</Text>
|
||||
<ChatOpsConnectors channelFilterId={channelFilterId} showLineNumber={false} />
|
||||
</VerticalGroup>
|
||||
</IntegrationBlockItem>
|
||||
)}
|
||||
<IntegrationBlockItem>
|
||||
<VerticalGroup spacing="md">
|
||||
<Text type="primary">Publish to ChatOps</Text>
|
||||
<ChatOpsConnectors channelFilterId={channelFilterId} showLineNumber={false} />
|
||||
</VerticalGroup>
|
||||
</IntegrationBlockItem>
|
||||
|
||||
<IntegrationBlockItem>
|
||||
<VerticalGroup>
|
||||
|
|
@ -213,9 +216,13 @@ const ExpandedIntegrationRouteDisplay: React.FC<ExpandedIntegrationRouteDisplayP
|
|||
></Select>
|
||||
</WithPermissionControlTooltip>
|
||||
|
||||
<Tooltip content={'Reload escalation chains list'} placement={'top'}>
|
||||
<Button variant={'secondary'} icon={'sync'} size={'md'} onClick={onEscalationChainsRefresh} />
|
||||
</Tooltip>
|
||||
<Button
|
||||
variant={'secondary'}
|
||||
tooltip={'Refresh Escalation Chains'}
|
||||
icon={'sync'}
|
||||
size={'md'}
|
||||
onClick={onEscalationChainsRefresh}
|
||||
/>
|
||||
|
||||
<PluginLink className={cx('hover-button')} target="_blank" query={escalationChainRedirectObj}>
|
||||
<Tooltip
|
||||
|
|
@ -336,18 +343,25 @@ export const RouteButtonsDisplay: React.FC<RouteButtonsDisplayProps> = ({
|
|||
{!channelFilter.is_default && (
|
||||
<WithPermissionControlTooltip userAction={UserActions.IntegrationsWrite}>
|
||||
<Tooltip placement="top" content={'Delete'}>
|
||||
<Button variant={'secondary'} icon={'trash-alt'} size={'sm'} onClick={setRouteIdForDeletion} />
|
||||
<Button variant={'secondary'} icon={'trash-alt'} size={'sm'} onClick={onDelete} />
|
||||
</Tooltip>
|
||||
</WithPermissionControlTooltip>
|
||||
)}
|
||||
</HorizontalGroup>
|
||||
);
|
||||
|
||||
function onRouteMoveDown() {
|
||||
function onDelete(e: React.SyntheticEvent) {
|
||||
e.stopPropagation();
|
||||
setRouteIdForDeletion();
|
||||
}
|
||||
|
||||
function onRouteMoveDown(e: React.SyntheticEvent) {
|
||||
e.stopPropagation();
|
||||
alertReceiveChannelStore.moveChannelFilterToPosition(alertReceiveChannelId, routeIndex, routeIndex + 1);
|
||||
}
|
||||
|
||||
function onRouteMoveUp() {
|
||||
function onRouteMoveUp(e: React.SyntheticEvent) {
|
||||
e.stopPropagation();
|
||||
alertReceiveChannelStore.moveChannelFilterToPosition(alertReceiveChannelId, routeIndex, routeIndex - 1);
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -100,6 +100,10 @@ const IntegrationForm2 = observer((props: IntegrationFormProps) => {
|
|||
<Drawer scrollableContent title="New Integration" onClose={onHide} closeOnMaskClick={false} width="640px">
|
||||
<div className={cx('content')}>
|
||||
<VerticalGroup>
|
||||
<Text type="secondary">
|
||||
Integration receives alerts on an unique API URL, interprets them using set of templates tailored for
|
||||
monitoring system and starts escalations.
|
||||
</Text>
|
||||
<div className={cx('search-integration')}>
|
||||
<Input
|
||||
autoFocus
|
||||
|
|
@ -129,7 +133,9 @@ const IntegrationForm2 = observer((props: IntegrationFormProps) => {
|
|||
<Text strong data-testid="integration-display-name">
|
||||
{alertReceiveChannelChoice.display_name}
|
||||
</Text>
|
||||
{alertReceiveChannelChoice.featured && <Tag name="Quick connect" colorIndex={5} />}
|
||||
{alertReceiveChannelChoice.featured && alertReceiveChannelChoice.featured_tag_name && (
|
||||
<Tag name={alertReceiveChannelChoice.featured_tag_name} colorIndex={5} />
|
||||
)}
|
||||
</HorizontalGroup>
|
||||
<Text type="secondary" size="small">
|
||||
{alertReceiveChannelChoice.short_description}
|
||||
|
|
@ -152,7 +158,7 @@ const IntegrationForm2 = observer((props: IntegrationFormProps) => {
|
|||
<div className={cx('content')}>
|
||||
<VerticalGroup>
|
||||
<GForm form={form} data={data} onSubmit={handleSubmit} />
|
||||
{isTableView && (
|
||||
{isTableView && selectedOption && (
|
||||
<Collapse
|
||||
headerWithBackground
|
||||
className={cx('collapse')}
|
||||
|
|
@ -165,10 +171,10 @@ const IntegrationForm2 = observer((props: IntegrationFormProps) => {
|
|||
<ul className={cx('integration-info-list')}>
|
||||
<li className={cx('integration-info-item')}>Unique URL endpoint for receiving alerts </li>
|
||||
<li className={cx('integration-info-item')}>
|
||||
Templates to interpret alerts, tailored for Grafana Alerting{' '}
|
||||
Templates to interpret alerts, tailored for {selectedOption.display_name}{' '}
|
||||
</li>
|
||||
<li className={cx('integration-info-item')}>Grafana Alerting contact point </li>
|
||||
<li className={cx('integration-info-item')}>Grafana Alerting notification</li>
|
||||
<li className={cx('integration-info-item')}>{selectedOption.display_name} contact point </li>
|
||||
<li className={cx('integration-info-item')}>{selectedOption.display_name} notification</li>
|
||||
</ul>
|
||||
What you’ll need to do next:
|
||||
<ul className={cx('integration-info-list')}>
|
||||
|
|
|
|||
|
|
@ -4,10 +4,14 @@
|
|||
|
||||
.container-wrapper {
|
||||
padding: 8px;
|
||||
height: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
max-height: 100%;
|
||||
width: 100%;
|
||||
border: var(--border-strong);
|
||||
padding: 0 16px;
|
||||
|
|
|
|||
|
|
@ -55,6 +55,12 @@ const IntegrationTemplate = observer((props: IntegrationTemplateProps) => {
|
|||
LocationHelper.update(locationParams, 'partial');
|
||||
}, []);
|
||||
|
||||
const getCodeEditorHeight = () => {
|
||||
const mainDiv = document.getElementById('content-container-id');
|
||||
const height = mainDiv?.getBoundingClientRect().height - 59;
|
||||
return `${height}px`;
|
||||
};
|
||||
|
||||
const onShowCheatSheet = useCallback(() => {
|
||||
setIsCheatSheetVisible(true);
|
||||
}, []);
|
||||
|
|
@ -166,7 +172,7 @@ const IntegrationTemplate = observer((props: IntegrationTemplateProps) => {
|
|||
width={'95%'}
|
||||
>
|
||||
<div className={cx('container-wrapper')}>
|
||||
<div className={cx('container')}>
|
||||
<div className={cx('container')} id={'content-container-id'}>
|
||||
<TemplatesAlertGroupsList
|
||||
alertReceiveChannelId={id}
|
||||
onEditPayload={onEditPayload}
|
||||
|
|
@ -183,7 +189,7 @@ const IntegrationTemplate = observer((props: IntegrationTemplateProps) => {
|
|||
<>
|
||||
<div className={cx('template-block-codeeditor')}>
|
||||
<div className={cx('template-editor-block-title')}>
|
||||
<HorizontalGroup justify="space-between">
|
||||
<HorizontalGroup justify="space-between" wrap>
|
||||
<Text>Template editor</Text>
|
||||
|
||||
<Button variant="secondary" fill="outline" onClick={onShowCheatSheet} icon="book" size="sm">
|
||||
|
|
@ -196,7 +202,7 @@ const IntegrationTemplate = observer((props: IntegrationTemplateProps) => {
|
|||
value={changedTemplateBody}
|
||||
data={templates}
|
||||
showLineNumbers={true}
|
||||
height={'85vh'}
|
||||
height={getCodeEditorHeight()}
|
||||
onChange={getChangeHandler()}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -215,7 +215,7 @@ const MobileAppConnection = observer(({ userPk }: Props) => {
|
|||
onClick={() => onSendTestNotification(true)}
|
||||
disabled={isAttemptingTestNotification}
|
||||
>
|
||||
Send Critical Test Push notification
|
||||
Send Important Test Push notification
|
||||
</Button>
|
||||
</HorizontalGroup>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -13,3 +13,8 @@
|
|||
.message code {
|
||||
white-space: break-spaces;
|
||||
}
|
||||
|
||||
.image-result img {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -60,7 +60,6 @@ const TemplatePreview = observer((props: TemplatePreviewProps) => {
|
|||
}, 1000);
|
||||
|
||||
useEffect(handleTemplateBodyChange, [templateBody, payload]);
|
||||
// onResult(result);
|
||||
|
||||
return result ? (
|
||||
<>
|
||||
|
|
@ -93,12 +92,20 @@ const TemplatePreview = observer((props: TemplatePreviewProps) => {
|
|||
)}
|
||||
</Text>
|
||||
) : (
|
||||
<div
|
||||
className={cx('message')}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: sanitize(result.preview || ''),
|
||||
}}
|
||||
/>
|
||||
<>
|
||||
{templateName.includes('image') ? (
|
||||
<div className={cx('image-result')}>
|
||||
<img src={result.preview} />
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className={cx('message')}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: sanitize(result.preview.replace(/\n/g, '<br />') || ''),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -27,6 +27,15 @@
|
|||
padding-left: 0;
|
||||
}
|
||||
|
||||
.alert-groups-list-item {
|
||||
cursor: pointer;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.alert-groups-list-item:hover {
|
||||
background-color: var(--background-secondary);
|
||||
}
|
||||
|
||||
.alert-groups-editor {
|
||||
width: 100%;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,6 +37,12 @@ const TemplatesAlertGroupsList = (props: TemplatesAlertGroupsListProps) => {
|
|||
.then((result) => setAlertGroupsList(result.slice(0, 30)));
|
||||
}, []);
|
||||
|
||||
const getCodeEditorHeight = () => {
|
||||
const mainDiv = document.getElementById('content-container-id');
|
||||
const height = mainDiv?.getBoundingClientRect().height - 59;
|
||||
return `${height}px`;
|
||||
};
|
||||
|
||||
const getChangeHandler = () => {
|
||||
return debounce((value: string) => {
|
||||
onEditPayload(value);
|
||||
|
|
@ -65,7 +71,7 @@ const TemplatesAlertGroupsList = (props: TemplatesAlertGroupsListProps) => {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className={cx('template-block-list')}>
|
||||
<div className={cx('template-block-list')} id="content-container-id">
|
||||
{selectedAlertPayload ? (
|
||||
<>
|
||||
{isEditMode ? (
|
||||
|
|
@ -83,7 +89,7 @@ const TemplatesAlertGroupsList = (props: TemplatesAlertGroupsListProps) => {
|
|||
<MonacoEditor
|
||||
value={JSON.stringify(selectedAlertPayload, null, 4)}
|
||||
data={templates}
|
||||
height={'85vh'}
|
||||
height={getCodeEditorHeight()}
|
||||
onChange={getChangeHandler()}
|
||||
showLineNumbers
|
||||
useAutoCompleteList={false}
|
||||
|
|
@ -110,7 +116,7 @@ const TemplatesAlertGroupsList = (props: TemplatesAlertGroupsListProps) => {
|
|||
value={JSON.stringify(selectedAlertPayload, null, 4)}
|
||||
data={undefined}
|
||||
disabled
|
||||
height={'85vh'}
|
||||
height={getCodeEditorHeight()}
|
||||
onChange={getChangeHandler()}
|
||||
showLineNumbers
|
||||
useAutoCompleteList={false}
|
||||
|
|
@ -147,7 +153,7 @@ const TemplatesAlertGroupsList = (props: TemplatesAlertGroupsListProps) => {
|
|||
data={templates}
|
||||
monacoOptions={MONACO_PAYLOAD_OPTIONS}
|
||||
showLineNumbers={false}
|
||||
height={'85vh'}
|
||||
height={getCodeEditorHeight()}
|
||||
onChange={getChangeHandler()}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -175,10 +181,12 @@ const TemplatesAlertGroupsList = (props: TemplatesAlertGroupsListProps) => {
|
|||
<>
|
||||
{alertGroupsList.map((alertGroup) => {
|
||||
return (
|
||||
<div key={alertGroup.pk}>
|
||||
<Button fill="text" onClick={() => getAlertGroupPayload(alertGroup.pk)}>
|
||||
{getAlertGroupName(alertGroup)}
|
||||
</Button>
|
||||
<div
|
||||
key={alertGroup.pk}
|
||||
onClick={() => getAlertGroupPayload(alertGroup.pk)}
|
||||
className={cx('alert-groups-list-item')}
|
||||
>
|
||||
<Text type="link"> {getAlertGroupName(alertGroup)}</Text>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ export interface AlertReceiveChannelOption {
|
|||
value: number;
|
||||
featured: boolean;
|
||||
short_description: string;
|
||||
featured_tag_name: string;
|
||||
}
|
||||
|
||||
export interface AlertReceiveChannelCounters {
|
||||
|
|
@ -43,6 +44,7 @@ export interface AlertReceiveChannel {
|
|||
heartbeat: Heartbeat | null;
|
||||
is_available_for_integration_heartbeat: boolean;
|
||||
routes_count: number;
|
||||
connected_escalations_chains_count: number;
|
||||
allow_delete: boolean;
|
||||
deleted?: boolean;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,8 @@
|
|||
/*
|
||||
[oncall-private]
|
||||
Any change to this file needs to be done in the oncall-private also
|
||||
*/
|
||||
|
||||
import { KeyValuePair } from 'utils';
|
||||
|
||||
export const TEXTAREA_ROWS_COUNT = 4;
|
||||
|
|
@ -45,10 +50,6 @@ const TemplateOptions = {
|
|||
TelegramTitle: new KeyValuePair('telegram_title_template', 'Title'),
|
||||
TelegramMessage: new KeyValuePair('telegram_message_template', 'Message'),
|
||||
TelegramImage: new KeyValuePair('telegram_image_url_template', 'Image'),
|
||||
/*Should it be in Oncallprivate repo? (All MsTeams)*/
|
||||
MSTeamsTitle: new KeyValuePair('MSTeams Title', 'Title'),
|
||||
MSTeamsMessage: new KeyValuePair('MSTeams Message', 'Message'),
|
||||
MSTeamsImage: new KeyValuePair('MSTeams Image', 'Image'),
|
||||
|
||||
Email: new KeyValuePair('Email', 'Email'),
|
||||
Slack: new KeyValuePair('Slack', 'Slack'),
|
||||
|
|
@ -105,24 +106,6 @@ export const INTEGRATION_TEMPLATES_LIST = [
|
|||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: TemplateOptions.MSTeams.value,
|
||||
value: TemplateOptions.MSTeams.key,
|
||||
children: [
|
||||
{
|
||||
label: TemplateOptions.MSTeamsTitle.value,
|
||||
value: TemplateOptions.MSTeamsTitle.key,
|
||||
},
|
||||
{
|
||||
label: TemplateOptions.MSTeamsMessage.value,
|
||||
value: TemplateOptions.MSTeamsMessage.key,
|
||||
},
|
||||
{
|
||||
label: TemplateOptions.MSTeamsImage.value,
|
||||
value: TemplateOptions.MSTeamsImage.key,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: TemplateOptions.Telegram.value,
|
||||
value: TemplateOptions.Telegram.key,
|
||||
|
|
|
|||
|
|
@ -46,6 +46,15 @@ const IntegrationHelper = {
|
|||
return routeIndex ? 'Else' : 'If';
|
||||
},
|
||||
|
||||
getRouteConditionTooltipWording(channelFilters: Array<ChannelFilter['id']>, routeIndex: number) {
|
||||
const totalCount = Object.keys(channelFilters).length;
|
||||
|
||||
if (routeIndex === totalCount - 1) {
|
||||
return 'If the alert payload does not match to the previous routes, it will be directed to this default route.';
|
||||
}
|
||||
return 'If the alert payload evaluates the route template as True, it will be directed to this route. It will not be evaluated against the subsequent routes.';
|
||||
},
|
||||
|
||||
getMaintenanceText(maintenanceUntill: number, mode: number = undefined) {
|
||||
const date = dayjs(new Date(maintenanceUntill * 1000));
|
||||
const now = dayjs();
|
||||
|
|
|
|||
|
|
@ -104,7 +104,14 @@ $LARGE-MARGIN: 24px;
|
|||
.loadingPlaceholder {
|
||||
margin-bottom: 0;
|
||||
margin-right: 4px;
|
||||
animation: none;
|
||||
}
|
||||
|
||||
.integration__description-alert {
|
||||
padding-top: 24px;
|
||||
|
||||
a {
|
||||
color: var(--primary-text-link);
|
||||
}
|
||||
}
|
||||
|
||||
.customise-button button {
|
||||
|
|
|
|||
|
|
@ -12,9 +12,10 @@ import {
|
|||
IconButton,
|
||||
ConfirmModal,
|
||||
Drawer,
|
||||
Alert,
|
||||
} from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
import { get } from 'lodash-es';
|
||||
import { get, noop } from 'lodash-es';
|
||||
import { observer } from 'mobx-react';
|
||||
import CopyToClipboard from 'react-copy-to-clipboard';
|
||||
import Emoji from 'react-emoji-render';
|
||||
|
|
@ -64,7 +65,8 @@ import { openNotification, openErrorNotification } from 'utils';
|
|||
import { getVar } from 'utils/DOM';
|
||||
import LocationHelper from 'utils/LocationHelper';
|
||||
import { UserActions } from 'utils/authorization';
|
||||
import { DATASOURCE_ALERTING, PLUGIN_ROOT } from 'utils/consts';
|
||||
import { DATASOURCE_GRAFANA, PLUGIN_ROOT } from 'utils/consts';
|
||||
import sanitize from 'utils/sanitize';
|
||||
|
||||
const cx = cn.bind(styles);
|
||||
|
||||
|
|
@ -205,10 +207,20 @@ class Integration2 extends React.Component<Integration2Props, Integration2State>
|
|||
<IntegrationHeader
|
||||
alertReceiveChannel={alertReceiveChannel}
|
||||
alertReceiveChannelCounter={alertReceiveChannelCounter}
|
||||
channelFilterIds={channelFilterIds}
|
||||
integration={integration}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{alertReceiveChannel.description && (
|
||||
<div className={cx('integration__description-alert')}>
|
||||
<Alert
|
||||
style={{ marginBottom: '0' }}
|
||||
// @ts-ignore
|
||||
title={<div dangerouslySetInnerHTML={{ __html: sanitize(alertReceiveChannel.description) }}></div>}
|
||||
severity="info"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<IntegrationCollapsibleTreeView
|
||||
|
|
@ -218,14 +230,14 @@ class Integration2 extends React.Component<Integration2Props, Integration2State>
|
|||
customIcon: 'plug',
|
||||
canHoverIcon: false,
|
||||
collapsedView: null,
|
||||
expandedView: <HowToConnectComponent id={id} />,
|
||||
expandedView: () => <HowToConnectComponent id={id} />,
|
||||
},
|
||||
{
|
||||
customIcon: 'layer-group',
|
||||
isExpanded: false,
|
||||
isCollapsible: false,
|
||||
canHoverIcon: false,
|
||||
expandedView: (
|
||||
expandedView: () => (
|
||||
<IntegrationBlock
|
||||
hasCollapsedBorder
|
||||
heading={
|
||||
|
|
@ -242,7 +254,10 @@ class Integration2 extends React.Component<Integration2Props, Integration2State>
|
|||
|
||||
<div className={cx('templates__content')}>
|
||||
<div className={cx('templates__container')}>
|
||||
<div className={cx('templates__item', 'templates__item--large')}>
|
||||
<div
|
||||
className={cx('templates__item', 'templates__item--large')}
|
||||
onClick={() => this.setState({ isTemplateSettingsOpen: true })}
|
||||
>
|
||||
<Text type="secondary" className={cx('templates__item-text')}>
|
||||
Grouping:
|
||||
</Text>
|
||||
|
|
@ -251,7 +266,10 @@ class Integration2 extends React.Component<Integration2Props, Integration2State>
|
|||
</Text>
|
||||
</div>
|
||||
|
||||
<div className={cx('templates__item', 'templates__item--large')}>
|
||||
<div
|
||||
className={cx('templates__item', 'templates__item--large')}
|
||||
onClick={() => this.setState({ isTemplateSettingsOpen: true })}
|
||||
>
|
||||
<Text type="secondary" className={cx('templates__item-text')}>
|
||||
Autoresolve:
|
||||
</Text>
|
||||
|
|
@ -260,7 +278,10 @@ class Integration2 extends React.Component<Integration2Props, Integration2State>
|
|||
</Text>
|
||||
</div>
|
||||
|
||||
<div className={cx('templates__item', 'templates__item--small')}>
|
||||
<div
|
||||
className={cx('templates__item', 'templates__item--small')}
|
||||
onClick={() => this.setState({ isTemplateSettingsOpen: true })}
|
||||
>
|
||||
<Text type="secondary" className={cx('templates__item-text')}>
|
||||
Visualisation:
|
||||
</Text>
|
||||
|
|
@ -290,7 +311,7 @@ class Integration2 extends React.Component<Integration2Props, Integration2State>
|
|||
isCollapsible: false,
|
||||
collapsedView: null,
|
||||
canHoverIcon: false,
|
||||
expandedView: (
|
||||
expandedView: () => (
|
||||
<div className={cx('routesSection')}>
|
||||
<VerticalGroup spacing="md">
|
||||
<Text type={'primary'} className={cx('routesSection__heading')}>
|
||||
|
|
@ -420,14 +441,15 @@ class Integration2 extends React.Component<Integration2Props, Integration2State>
|
|||
this.setState((prevState) => ({ newRoutes: prevState.newRoutes.filter((r) => r !== channelFilterId) }));
|
||||
}
|
||||
},
|
||||
collapsedView: (
|
||||
collapsedView: (toggle) => (
|
||||
<CollapsedIntegrationRouteDisplay
|
||||
alertReceiveChannelId={id}
|
||||
channelFilterId={channelFilterId}
|
||||
routeIndex={routeIndex}
|
||||
toggle={toggle}
|
||||
/>
|
||||
),
|
||||
expandedView: (
|
||||
expandedView: () => (
|
||||
<ExpandedIntegrationRouteDisplay
|
||||
alertReceiveChannelId={id}
|
||||
channelFilterId={channelFilterId}
|
||||
|
|
@ -884,12 +906,13 @@ const HowToConnectComponent: React.FC<{ id: AlertReceiveChannel['id'] }> = ({ id
|
|||
const { alertReceiveChannelStore } = useStore();
|
||||
const alertReceiveChannelCounter = alertReceiveChannelStore.counters[id];
|
||||
const alertReceiveChannel = alertReceiveChannelStore.items[id];
|
||||
const isAlertManager = alertReceiveChannel.integration === DATASOURCE_ALERTING;
|
||||
const isGrafanaDatasource = alertReceiveChannel.integration === DATASOURCE_GRAFANA;
|
||||
const hasAlerts = !!alertReceiveChannelCounter?.alerts_count;
|
||||
|
||||
return (
|
||||
<IntegrationBlock
|
||||
hasCollapsedBorder={false}
|
||||
toggle={noop}
|
||||
heading={
|
||||
<div className={cx('how-to-connect__container')}>
|
||||
<Tag
|
||||
|
|
@ -915,7 +938,7 @@ const HowToConnectComponent: React.FC<{ id: AlertReceiveChannel['id'] }> = ({ id
|
|||
</a>
|
||||
</div>
|
||||
}
|
||||
content={isAlertManager || !hasAlerts ? renderContent() : null}
|
||||
content={isGrafanaDatasource || !hasAlerts ? renderContent() : null}
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
@ -930,14 +953,14 @@ const HowToConnectComponent: React.FC<{ id: AlertReceiveChannel['id'] }> = ({ id
|
|||
</HorizontalGroup>
|
||||
)}
|
||||
|
||||
{isAlertManager && (
|
||||
{isGrafanaDatasource && (
|
||||
<HorizontalGroup spacing={'xs'}>
|
||||
<Icon name="list-ui-alt" size="md" />
|
||||
<a href="/alerting/notifications" target="_blank">
|
||||
<a href={`/alerting/notifications?alertmanager=grafana`} target="_blank" rel="noreferrer">
|
||||
<Text type={'link'}>Contact Point</Text>
|
||||
</a>
|
||||
<Text type={'secondary'}>and</Text>
|
||||
<a href="/alerting/routes" target="_blank">
|
||||
<a href="/alerting/routes?alertmanager=grafana" target="_blank">
|
||||
<Text type={'link'}>Notification Policy</Text>
|
||||
</a>
|
||||
<Text type={'secondary'}>created in Grafana Alerting</Text>
|
||||
|
|
@ -953,14 +976,12 @@ interface IntegrationHeaderProps {
|
|||
alertReceiveChannelCounter: AlertReceiveChannelCounters;
|
||||
alertReceiveChannel: AlertReceiveChannel;
|
||||
integration: SelectOption;
|
||||
channelFilterIds: string[];
|
||||
}
|
||||
|
||||
const IntegrationHeader: React.FC<IntegrationHeaderProps> = ({
|
||||
integration,
|
||||
alertReceiveChannelCounter,
|
||||
alertReceiveChannel,
|
||||
channelFilterIds,
|
||||
}) => {
|
||||
const { grafanaTeamStore, heartbeatStore, alertReceiveChannelStore } = useStore();
|
||||
|
||||
|
|
@ -984,9 +1005,17 @@ const IntegrationHeader: React.FC<IntegrationHeaderProps> = ({
|
|||
<TooltipBadge
|
||||
borderType="success"
|
||||
icon="link"
|
||||
text={channelFilterIds.length}
|
||||
tooltipTitle={`${channelFilterIds.length} Routes`}
|
||||
tooltipContent={undefined}
|
||||
text={`${alertReceiveChannel.connected_escalations_chains_count}/${alertReceiveChannel.routes_count}`}
|
||||
tooltipTitle=""
|
||||
tooltipContent={
|
||||
alertReceiveChannel.connected_escalations_chains_count +
|
||||
' connected escalation chain' +
|
||||
(alertReceiveChannel.connected_escalations_chains_count === 1 ? '' : 's') +
|
||||
' in ' +
|
||||
alertReceiveChannel.routes_count +
|
||||
' route' +
|
||||
(alertReceiveChannel.routes_count === 1 ? '' : 's')
|
||||
}
|
||||
/>
|
||||
|
||||
{alertReceiveChannel.maintenance_till && (
|
||||
|
|
@ -1044,7 +1073,7 @@ const IntegrationHeader: React.FC<IntegrationHeaderProps> = ({
|
|||
|
||||
if (
|
||||
!alertReceiveChannel.is_available_for_integration_heartbeat ||
|
||||
alertReceiveChannel.heartbeat?.last_heartbeat_time_verbal === null
|
||||
!alertReceiveChannel.heartbeat?.last_heartbeat_time_verbal
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React from 'react';
|
||||
|
||||
import { HorizontalGroup, Button, IconButton } from '@grafana/ui';
|
||||
import { HorizontalGroup, Button, IconButton, VerticalGroup } from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
import { debounce } from 'lodash-es';
|
||||
import { observer } from 'mobx-react';
|
||||
|
|
@ -165,7 +165,12 @@ class Integrations extends React.Component<IntegrationsProps, IntegrationsState>
|
|||
<div className={cx('root')}>
|
||||
<div className={cx('title')}>
|
||||
<HorizontalGroup justify="space-between">
|
||||
<Text.Title level={3}>Integrations 2</Text.Title>
|
||||
<VerticalGroup>
|
||||
<Text.Title level={3}>Integrations 2</Text.Title>
|
||||
<Text type="secondary">
|
||||
Receive alerts, group and interpret using templates and route to escalations
|
||||
</Text>
|
||||
</VerticalGroup>
|
||||
<WithPermissionControlTooltip userAction={UserActions.IntegrationsWrite}>
|
||||
<Button
|
||||
onClick={() => {
|
||||
|
|
@ -257,6 +262,7 @@ class Integrations extends React.Component<IntegrationsProps, IntegrationsState>
|
|||
renderIntegrationStatus(item: AlertReceiveChannel, alertReceiveChannelStore) {
|
||||
const alertReceiveChannelCounter = alertReceiveChannelStore.counters[item.id];
|
||||
let routesCounter = item.routes_count;
|
||||
let connectedEscalationsChainsCount = item.connected_escalations_chains_count;
|
||||
|
||||
return (
|
||||
<HorizontalGroup spacing="xs">
|
||||
|
|
@ -282,9 +288,17 @@ class Integrations extends React.Component<IntegrationsProps, IntegrationsState>
|
|||
<TooltipBadge
|
||||
borderType="success"
|
||||
icon="link"
|
||||
text={routesCounter}
|
||||
text={`${connectedEscalationsChainsCount}/${routesCounter}`}
|
||||
tooltipTitle=""
|
||||
tooltipContent={`${routesCounter} routes`}
|
||||
tooltipContent={
|
||||
connectedEscalationsChainsCount +
|
||||
' connected escalation chain' +
|
||||
(connectedEscalationsChainsCount === 1 ? '' : 's') +
|
||||
' in ' +
|
||||
routesCounter +
|
||||
' route' +
|
||||
(routesCounter === 1 ? '' : 's')
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</HorizontalGroup>
|
||||
|
|
|
|||
|
|
@ -109,7 +109,10 @@ class SlackSettings extends Component<SlackProps, SlackState> {
|
|||
<Text>{store.teamStore.currentTeam.slack_team_identity?.cached_name}</Text>
|
||||
</div>
|
||||
</Field>
|
||||
<Field label="Default channel for Slack notifications">
|
||||
<Field
|
||||
label="Default channel for Slack notifications"
|
||||
description="The selected channel will be used as a fallback in the event that a schedule or integration does not have a configured channel"
|
||||
>
|
||||
<WithPermissionControlTooltip userAction={UserActions.ChatOpsUpdateSettings}>
|
||||
<GSelect
|
||||
showSearch
|
||||
|
|
|
|||
|
|
@ -34,4 +34,4 @@ export const DOCS_TELEGRAM_SETUP = 'https://grafana.com/docs/oncall/latest/chat-
|
|||
// Make sure if you chage max-width here you also change it in responsive.css
|
||||
export const TABLE_COLUMN_MAX_WIDTH = 1500;
|
||||
|
||||
export const DATASOURCE_ALERTING = 'alertmanager';
|
||||
export const DATASOURCE_GRAFANA = 'grafana';
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue