Merge pull request #2072 from grafana/dev

Merge dev to main
This commit is contained in:
Michael Derynck 2023-05-31 14:48:07 -06:00 committed by GitHub
commit 61c44741fd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
35 changed files with 386 additions and 166 deletions

View file

@ -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

View file

@ -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">

View file

@ -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">

View file

@ -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...

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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"]:

View file

@ -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'),
),
]

View file

@ -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)

View file

@ -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",

View file

@ -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

View file

@ -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

View file

@ -6,6 +6,12 @@
min-width: min-content;
}
.cheatsheet-container > div {
height: 100%;
max-height: 100%;
overflow-y: scroll;
}
.cheatsheet-item {
margin-bottom: 24px;
}

View file

@ -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>
);

View file

@ -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>

View file

@ -292,6 +292,7 @@ class AlertRules extends React.Component<AlertRulesProps, AlertRulesState> {
)}
</div>
</Block>
{alertReceiveChannel.description && (
<div className={cx('description-style')}>
<Alert

View file

@ -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>

View file

@ -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);
}
};

View file

@ -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 youll need to do next:
<ul className={cx('integration-info-list')}>

View file

@ -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;

View file

@ -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>

View file

@ -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>

View file

@ -13,3 +13,8 @@
.message code {
white-space: break-spaces;
}
.image-result img {
max-width: 100%;
max-height: 100%;
}

View file

@ -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 />') || ''),
}}
/>
)}
</>
)}
</>
) : (

View file

@ -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%;
}

View file

@ -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>
);
})}

View file

@ -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;
}

View file

@ -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,

View file

@ -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();

View file

@ -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 {

View file

@ -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;
}

View file

@ -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>

View file

@ -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

View file

@ -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';