Unhide direct paging integration (#2483)
# What this PR does Fixes https://github.com/grafana/oncall/issues/2442 https://github.com/grafana/oncall/assets/2262529/08bb8e5f-acc6-4f2d-9e38-717c9f37e3da ## Which issue(s) this PR fixes ## Checklist - [ ] Unit, integration, and e2e (if applicable) tests updated - [ ] Documentation added (or `pr:no public docs` PR label added if not required) - [ ] `CHANGELOG.md` updated (or `pr:no changelog` PR label added if not required)
This commit is contained in:
parent
d24dc4b630
commit
0b28815d46
20 changed files with 353 additions and 186 deletions
|
|
@ -34,6 +34,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
- Add debounce on Select UI components to avoid making API search requests on each key-down event by
|
||||
@maskin25 ([#2466](https://github.com/grafana/oncall/pull/2466))
|
||||
- Make Direct paging integration configurable ([2483](https://github.com/grafana/oncall/pull/2483))
|
||||
|
||||
## v1.3.8 (2023-07-11)
|
||||
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ def _trigger_alert(
|
|||
deleted_at=None,
|
||||
defaults={
|
||||
"author": from_user,
|
||||
"verbal_name": f"Direct paging ({team.name if team else 'General'} team)",
|
||||
"verbal_name": "Direct paging",
|
||||
},
|
||||
)
|
||||
if alert_receive_channel.default_channel_filter is None:
|
||||
|
|
@ -149,8 +149,6 @@ def direct_paging(
|
|||
Otherwise, create a new alert using given title and message.
|
||||
|
||||
"""
|
||||
if not users and not schedules and not escalation_chain:
|
||||
return
|
||||
|
||||
if users is None:
|
||||
users = []
|
||||
|
|
|
|||
|
|
@ -154,20 +154,6 @@ def test_check_user_availability_on_call(
|
|||
assert warnings == []
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_direct_paging_no_one(make_organization, make_user_for_organization):
|
||||
organization = make_organization()
|
||||
from_user = make_user_for_organization(organization)
|
||||
|
||||
with patch("apps.alerts.paging.notify_user_task") as notify_task:
|
||||
direct_paging(organization, None, from_user)
|
||||
|
||||
# no alert group
|
||||
assert AlertGroup.all_objects.count() == 0
|
||||
# no notifications
|
||||
assert not notify_task.apply_async.called
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_direct_paging_user(make_organization, make_user_for_organization):
|
||||
organization = make_organization()
|
||||
|
|
|
|||
|
|
@ -117,8 +117,15 @@ class AlertReceiveChannelSerializer(EagerLoadingMixin, serializers.ModelSerializ
|
|||
connection_error = GrafanaAlertingSyncManager.check_for_connection_errors(organization)
|
||||
if connection_error:
|
||||
raise BadRequest(detail=connection_error)
|
||||
for _integration in AlertReceiveChannel._config:
|
||||
if _integration.slug == integration:
|
||||
is_able_to_autoresolve = _integration.is_able_to_autoresolve
|
||||
|
||||
instance = AlertReceiveChannel.create(
|
||||
**validated_data, organization=organization, author=self.context["request"].user
|
||||
**validated_data,
|
||||
organization=organization,
|
||||
author=self.context["request"].user,
|
||||
allow_source_based_resolving=is_able_to_autoresolve,
|
||||
)
|
||||
|
||||
return instance
|
||||
|
|
@ -172,8 +179,11 @@ class AlertReceiveChannelSerializer(EagerLoadingMixin, serializers.ModelSerializ
|
|||
return obj.channel_filters.count()
|
||||
|
||||
def get_connected_escalations_chains_count(self, obj) -> int:
|
||||
return len(
|
||||
set(ChannelFilter.objects.filter(alert_receive_channel=obj).values_list("escalation_chain", flat=True))
|
||||
return (
|
||||
ChannelFilter.objects.filter(alert_receive_channel=obj, escalation_chain__isnull=False)
|
||||
.values("escalation_chain")
|
||||
.distinct()
|
||||
.count()
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -192,8 +202,7 @@ class FastAlertReceiveChannelSerializer(serializers.ModelSerializer):
|
|||
fields = ["id", "integration", "verbal_name", "deleted"]
|
||||
|
||||
def get_deleted(self, obj):
|
||||
# Treat direct paging integrations as deleted, so integration settings are disabled on the frontend
|
||||
return obj.deleted_at is not None or obj.integration == AlertReceiveChannel.INTEGRATION_DIRECT_PAGING
|
||||
return obj.deleted_at is not None
|
||||
|
||||
|
||||
class FilterAlertReceiveChannelSerializer(serializers.ModelSerializer):
|
||||
|
|
|
|||
|
|
@ -56,17 +56,12 @@ class DirectPagingSerializer(serializers.Serializer):
|
|||
def validate(self, attrs):
|
||||
organization = self.context["organization"]
|
||||
|
||||
users = attrs["users"]
|
||||
schedules = attrs["schedules"]
|
||||
escalation_chain_id = attrs["escalation_chain_id"]
|
||||
|
||||
alert_group_id = attrs["alert_group_id"]
|
||||
title = attrs["title"]
|
||||
message = attrs["message"]
|
||||
|
||||
if len(users) == 0 and len(schedules) == 0 and not escalation_chain_id:
|
||||
raise serializers.ValidationError("Provide users, schedules, or an escalation chain")
|
||||
|
||||
if alert_group_id and (title or message):
|
||||
raise serializers.ValidationError("alert_group_id and (title, message) are mutually exclusive")
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ from rest_framework import status
|
|||
from rest_framework.response import Response
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from apps.alerts.models import AlertGroup, AlertGroupLogRecord, AlertReceiveChannel
|
||||
from apps.alerts.models import AlertGroup, AlertGroupLogRecord
|
||||
from apps.api.errors import AlertGroupAPIError
|
||||
from apps.api.permissions import LegacyAccessControlRole
|
||||
from apps.base.models import UserNotificationPolicyLogRecord
|
||||
|
|
@ -1813,28 +1813,6 @@ def test_alert_group_paged_users(
|
|||
assert response.json()["paged_users"] == [user2.short()]
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_direct_paging_integration_treated_as_deleted(
|
||||
make_organization_and_user_with_plugin_token,
|
||||
make_alert_receive_channel,
|
||||
alert_group_internal_api_setup,
|
||||
make_channel_filter,
|
||||
make_alert_group,
|
||||
make_user_auth_headers,
|
||||
):
|
||||
organization, user, token = make_organization_and_user_with_plugin_token()
|
||||
alert_receive_channel = make_alert_receive_channel(
|
||||
organization, integration=AlertReceiveChannel.INTEGRATION_DIRECT_PAGING
|
||||
)
|
||||
alert_group = make_alert_group(alert_receive_channel)
|
||||
|
||||
client = APIClient()
|
||||
url = reverse("api-internal:alertgroup-detail", kwargs={"pk": alert_group.public_primary_key})
|
||||
|
||||
response = client.get(url, format="json", **make_user_auth_headers(user, token))
|
||||
assert response.json()["alert_receive_channel"]["deleted"] is True
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_alert_group_resolve_resolution_note(
|
||||
make_organization_and_user_with_plugin_token,
|
||||
|
|
|
|||
|
|
@ -675,23 +675,6 @@ def test_alert_receive_channel_counters_per_integration_permissions(
|
|||
assert response.status_code == expected_status
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_get_alert_receive_channels_direct_paging_hidden_from_list(
|
||||
make_organization_and_user_with_plugin_token, make_alert_receive_channel, make_user_auth_headers
|
||||
):
|
||||
organization, user, token = make_organization_and_user_with_plugin_token()
|
||||
make_alert_receive_channel(user.organization, integration=AlertReceiveChannel.INTEGRATION_DIRECT_PAGING)
|
||||
|
||||
client = APIClient()
|
||||
url = reverse("api-internal:alert_receive_channel-list")
|
||||
response = client.get(url, format="json", **make_user_auth_headers(user, token))
|
||||
|
||||
# Check no direct paging integrations in the response
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.json()["count"] == 0
|
||||
assert len(response.json()["results"]) == 0
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_get_alert_receive_channels_direct_paging_present_for_filters(
|
||||
make_organization_and_user_with_plugin_token, make_alert_receive_channel, make_user_auth_headers
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ from apps.api.serializers.alert_receive_channel import (
|
|||
)
|
||||
from apps.api.throttlers import DemoAlertThrottler
|
||||
from apps.auth_token.auth import PluginAuthentication
|
||||
from apps.user_management.models.team import Team
|
||||
from common.api_helpers.exceptions import BadRequest
|
||||
from common.api_helpers.filters import ByTeamModelFieldFilterMixin, TeamModelMultipleChoiceFilter
|
||||
from common.api_helpers.mixins import (
|
||||
|
|
@ -104,10 +105,39 @@ class AlertReceiveChannelView(
|
|||
}
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
if request.data["integration"] is not None and (
|
||||
request.data["integration"] in AlertReceiveChannel.WEB_INTEGRATION_CHOICES
|
||||
):
|
||||
return super().create(request, *args, **kwargs)
|
||||
organization = request.auth.organization
|
||||
user = request.user
|
||||
team_lookup = {}
|
||||
if "team" in request.data:
|
||||
team_public_pk = request.data.get("team", None)
|
||||
if team_public_pk is not None:
|
||||
try:
|
||||
team = user.available_teams.get(public_primary_key=team_public_pk)
|
||||
team_lookup = {"team": team}
|
||||
except Team.DoesNotExist:
|
||||
return Response(data="invalid team", status=status.HTTP_400_BAD_REQUEST)
|
||||
else:
|
||||
team_lookup = {"team__isnull": True}
|
||||
|
||||
if request.data["integration"] is not None:
|
||||
if request.data["integration"] in AlertReceiveChannel.WEB_INTEGRATION_CHOICES:
|
||||
# Don't allow multiple Direct Paging integrations
|
||||
if request.data["integration"] == AlertReceiveChannel.INTEGRATION_DIRECT_PAGING:
|
||||
try:
|
||||
AlertReceiveChannel.objects.get(
|
||||
organization=organization,
|
||||
integration=AlertReceiveChannel.INTEGRATION_DIRECT_PAGING,
|
||||
deleted_at=None,
|
||||
**team_lookup,
|
||||
)
|
||||
return Response(
|
||||
data="Direct paging integration already exists for this team",
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
except AlertReceiveChannel.DoesNotExist:
|
||||
pass
|
||||
return super().create(request, *args, **kwargs)
|
||||
|
||||
return Response(data="invalid integration", status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def perform_update(self, serializer):
|
||||
|
|
@ -147,10 +177,6 @@ class AlertReceiveChannelView(
|
|||
if not ignore_filtering_by_available_teams:
|
||||
queryset = queryset.filter(*self.available_teams_lookup_args).distinct()
|
||||
|
||||
# Hide direct paging integrations from the list view, but not from the filters
|
||||
if not is_filters_request:
|
||||
queryset = queryset.exclude(integration=AlertReceiveChannel.INTEGRATION_DIRECT_PAGING)
|
||||
|
||||
return queryset
|
||||
|
||||
@action(detail=True, methods=["post"], throttle_classes=[DemoAlertThrottler])
|
||||
|
|
|
|||
|
|
@ -726,25 +726,6 @@ def test_set_default_messaging_backend_template(
|
|||
assert response.data == expected_response
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_get_list_integrations_direct_paging_hidden(
|
||||
make_organization_and_user_with_token,
|
||||
make_alert_receive_channel,
|
||||
make_channel_filter,
|
||||
make_integration_heartbeat,
|
||||
):
|
||||
organization, user, token = make_organization_and_user_with_token()
|
||||
make_alert_receive_channel(organization, integration=AlertReceiveChannel.INTEGRATION_DIRECT_PAGING)
|
||||
|
||||
client = APIClient()
|
||||
url = reverse("api-public:integrations-list")
|
||||
response = client.get(url, format="json", HTTP_AUTHORIZATION=f"{token}")
|
||||
|
||||
# Check no direct paging integrations in the response
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.json()["results"] == []
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_get_list_integrations_link_and_inbound_email(
|
||||
make_organization_and_user_with_token,
|
||||
|
|
|
|||
|
|
@ -48,9 +48,6 @@ class IntegrationView(
|
|||
queryset = self.serializer_class.setup_eager_loading(queryset)
|
||||
queryset = queryset.annotate(alert_groups_count_annotated=Count("alert_groups", distinct=True))
|
||||
|
||||
# Hide direct paging integrations
|
||||
queryset = queryset.exclude(integration=AlertReceiveChannel.INTEGRATION_DIRECT_PAGING)
|
||||
|
||||
return queryset
|
||||
|
||||
def get_object(self):
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ title = "Direct paging"
|
|||
slug = "direct_paging"
|
||||
short_description = None
|
||||
description = None
|
||||
is_displayed_on_web = False
|
||||
is_displayed_on_web = True
|
||||
is_featured = False
|
||||
is_able_to_autoresolve = False
|
||||
is_demo_alert_enabled = False
|
||||
|
|
|
|||
|
|
@ -15,19 +15,5 @@ export const manualAlertFormConfig: { name: string; fields: FormItem[] } = {
|
|||
label: 'Description',
|
||||
validation: { required: true },
|
||||
},
|
||||
{
|
||||
name: 'team',
|
||||
label: 'Assign to team',
|
||||
description:
|
||||
'Assigning to the teams allows you to filter Alert Groups and configure their visibility. Go to OnCall -> Settings -> Team and Access Settings for more details',
|
||||
type: FormItemType.GSelect,
|
||||
extra: {
|
||||
modelName: 'grafanaTeamStore',
|
||||
displayField: 'name',
|
||||
valueField: 'id',
|
||||
showSearch: true,
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -6,3 +6,23 @@
|
|||
background: var(--secondary-background);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.responders-list {
|
||||
list-style-type: none;
|
||||
margin-bottom: 20px;
|
||||
width: 100%;
|
||||
background: var(--background-secondary);
|
||||
|
||||
& > li .hover-button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
& > li:hover .hover-button {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
& > li {
|
||||
padding: 10px 12px;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,35 @@
|
|||
import React, { FC, useCallback, useState } from 'react';
|
||||
|
||||
import { Button, Drawer, HorizontalGroup, Icon, VerticalGroup } from '@grafana/ui';
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Drawer,
|
||||
Field,
|
||||
HorizontalGroup,
|
||||
Icon,
|
||||
IconButton,
|
||||
IconName,
|
||||
Label,
|
||||
LoadingPlaceholder,
|
||||
Tooltip,
|
||||
VerticalGroup,
|
||||
} from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
|
||||
import Block from 'components/GBlock/Block';
|
||||
import GForm from 'components/GForm/GForm';
|
||||
import PluginLink from 'components/PluginLink/PluginLink';
|
||||
import Text from 'components/Text/Text';
|
||||
import EscalationVariants from 'containers/EscalationVariants/EscalationVariants';
|
||||
import { prepareForUpdate } from 'containers/EscalationVariants/EscalationVariants.helpers';
|
||||
import { Alert } from 'models/alertgroup/alertgroup.types';
|
||||
import GrafanaTeamSelect from 'containers/GrafanaTeamSelect/GrafanaTeamSelect';
|
||||
import TeamName from 'containers/TeamName/TeamName';
|
||||
import { AlertReceiveChannelStore } from 'models/alert_receive_channel/alert_receive_channel';
|
||||
import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_channel.types';
|
||||
import { Alert as AlertType } from 'models/alertgroup/alertgroup.types';
|
||||
import { GrafanaTeam } from 'models/grafana_team/grafana_team.types';
|
||||
import IntegrationHelper from 'pages/integration/Integration.helper';
|
||||
import { useStore } from 'state/useStore';
|
||||
import { openWarningNotification } from 'utils';
|
||||
|
||||
import { manualAlertFormConfig } from './ManualAlertGroup.config';
|
||||
|
||||
|
|
@ -17,7 +37,8 @@ import styles from './ManualAlertGroup.module.css';
|
|||
|
||||
interface ManualAlertGroupProps {
|
||||
onHide: () => void;
|
||||
onCreate: (id: Alert['pk']) => void;
|
||||
onCreate: (id: AlertType['pk']) => void;
|
||||
alertReceiveChannelStore: AlertReceiveChannelStore;
|
||||
}
|
||||
|
||||
const cx = cn.bind(styles);
|
||||
|
|
@ -26,13 +47,24 @@ const ManualAlertGroup: FC<ManualAlertGroupProps> = (props) => {
|
|||
const store = useStore();
|
||||
const [userResponders, setUserResponders] = useState([]);
|
||||
const [scheduleResponders, setScheduleResponders] = useState([]);
|
||||
const { onHide, onCreate } = props;
|
||||
const data = { team: store.userStore.currentUser?.current_team };
|
||||
const { onHide, onCreate, alertReceiveChannelStore } = props;
|
||||
|
||||
const [selectedTeamId, setSelectedTeam] = useState<GrafanaTeam['id']>();
|
||||
const [selectedTeamDirectPaging, setSelectedTeamDirectPaging] = useState<AlertReceiveChannel>();
|
||||
const [directPagingLoading, setdirectPagingLoading] = useState<boolean>();
|
||||
|
||||
const [chatOpsAvailableChannels, setChatopsAvailableChannels] = useState<any>();
|
||||
|
||||
const data = {};
|
||||
|
||||
const handleFormSubmit = async (data) => {
|
||||
if (selectedTeamId === undefined) {
|
||||
openWarningNotification('Select team first');
|
||||
return;
|
||||
}
|
||||
store.directPagingStore
|
||||
.createManualAlertRule(prepareForUpdate(userResponders, scheduleResponders, data))
|
||||
.then(({ alert_group_id: id }: { alert_group_id: Alert['pk'] }) => {
|
||||
.createManualAlertRule(prepareForUpdate(userResponders, scheduleResponders, { team: selectedTeamId, ...data }))
|
||||
.then(({ alert_group_id: id }: { alert_group_id: AlertType['pk'] }) => {
|
||||
onCreate(id);
|
||||
})
|
||||
.finally(() => {
|
||||
|
|
@ -40,48 +72,174 @@ const ManualAlertGroup: FC<ManualAlertGroupProps> = (props) => {
|
|||
});
|
||||
};
|
||||
|
||||
const onUpdateSelectedTeam = async (selectedTeamId: GrafanaTeam['id']) => {
|
||||
setdirectPagingLoading(true);
|
||||
setSelectedTeamDirectPaging(null);
|
||||
setSelectedTeam(selectedTeamId);
|
||||
await alertReceiveChannelStore.updateItems({ team: selectedTeamId, integration: 'direct_paging' });
|
||||
const directPagingAlertReceiveChannel =
|
||||
alertReceiveChannelStore.getSearchResult() && alertReceiveChannelStore.getSearchResult()[0];
|
||||
if (directPagingAlertReceiveChannel) {
|
||||
setSelectedTeamDirectPaging(directPagingAlertReceiveChannel);
|
||||
await alertReceiveChannelStore.updateChannelFilters(directPagingAlertReceiveChannel.id);
|
||||
await store.slackChannelStore.updateItems();
|
||||
|
||||
// The code below is used to get the unique available chotops channels for all routes in integraion
|
||||
// This is the workaround for IntegrationHelper.getChatOpsChannels, it should be moved to the helper
|
||||
const filterIds = alertReceiveChannelStore.channelFilterIds[directPagingAlertReceiveChannel.id];
|
||||
let availableChannels = [];
|
||||
let channelKeys = new Set();
|
||||
filterIds.map((channelFilterId) => {
|
||||
IntegrationHelper.getChatOpsChannels(alertReceiveChannelStore.channelFilters[channelFilterId], store)
|
||||
.filter((channel) => channel)
|
||||
.map((channel) => {
|
||||
if (!channelKeys.has(channel.name + channel.icon)) {
|
||||
availableChannels.push(channel);
|
||||
channelKeys.add(channel.name + channel.icon);
|
||||
}
|
||||
});
|
||||
});
|
||||
setChatopsAvailableChannels(Array.from(availableChannels));
|
||||
}
|
||||
setdirectPagingLoading(false);
|
||||
};
|
||||
|
||||
const onUpdateEscalationVariants = useCallback(
|
||||
(value) => {
|
||||
setUserResponders(value.userResponders);
|
||||
|
||||
setScheduleResponders(value.scheduleResponders);
|
||||
},
|
||||
[userResponders, scheduleResponders]
|
||||
);
|
||||
|
||||
const DirectPagingIntegrationVariants = ({ selectedTeamId, selectedTeamDirectPaging, chatOpsAvailableChannels }) => {
|
||||
const escalationChainsExist = selectedTeamDirectPaging?.connected_escalations_chains_count === 0;
|
||||
|
||||
return (
|
||||
<VerticalGroup>
|
||||
{selectedTeamId &&
|
||||
(directPagingLoading ? (
|
||||
<LoadingPlaceholder text="Loading..." />
|
||||
) : selectedTeamDirectPaging ? (
|
||||
<VerticalGroup>
|
||||
<Label>Team will be notified according to the integration settings:</Label>
|
||||
<ul className={cx('responders-list')}>
|
||||
<li>
|
||||
<HorizontalGroup justify="space-between">
|
||||
<HorizontalGroup>
|
||||
{escalationChainsExist && (
|
||||
<Tooltip content="Integration doesn't have connected escalation policies">
|
||||
<Icon name="exclamation-triangle" style={{ color: 'var(--warning-text-color)' }} />
|
||||
</Tooltip>
|
||||
)}
|
||||
<Text>{selectedTeamDirectPaging.verbal_name}</Text>
|
||||
</HorizontalGroup>
|
||||
<HorizontalGroup>
|
||||
<Text type="secondary">Team:</Text>
|
||||
<TeamName team={store.grafanaTeamStore.items[selectedTeamId]} />
|
||||
</HorizontalGroup>
|
||||
<HorizontalGroup>
|
||||
{chatOpsAvailableChannels && (
|
||||
<>
|
||||
<Text type="secondary">ChatOps:</Text>{' '}
|
||||
{chatOpsAvailableChannels.map(
|
||||
(chatOpsChannel: { name: string; icon: IconName }, chatOpsIndex) => (
|
||||
<div
|
||||
key={`${chatOpsChannel.name}-${chatOpsIndex}`}
|
||||
className={cx({
|
||||
'u-margin-right-xs': chatOpsIndex !== chatOpsAvailableChannels.length,
|
||||
})}
|
||||
>
|
||||
{chatOpsChannel.icon && <Icon name={chatOpsChannel.icon} className={cx('icon')} />}
|
||||
<Text type="primary">{chatOpsChannel.name || ''}</Text>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</HorizontalGroup>
|
||||
<HorizontalGroup>
|
||||
<PluginLink target="_blank" query={{ page: 'integrations', id: selectedTeamDirectPaging.id }}>
|
||||
<IconButton
|
||||
tooltip="Open integration in new tab"
|
||||
style={{ color: 'var(--always-gray)' }}
|
||||
name="external-link-alt"
|
||||
/>
|
||||
</PluginLink>
|
||||
</HorizontalGroup>
|
||||
</HorizontalGroup>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
{(escalationChainsExist || !chatOpsAvailableChannels) && (
|
||||
<Alert severity="warning" title="Possible notification miss">
|
||||
<VerticalGroup>
|
||||
{escalationChainsExist && (
|
||||
<Text>
|
||||
Integration doesn't have connected escalation policies. Consider adding responders manually by
|
||||
user or by email
|
||||
</Text>
|
||||
)}
|
||||
{!chatOpsAvailableChannels && (
|
||||
<Text>Integration doesn't have connected ChatOps channels in messengers.</Text>
|
||||
)}
|
||||
</VerticalGroup>
|
||||
</Alert>
|
||||
)}
|
||||
</VerticalGroup>
|
||||
) : (
|
||||
<Alert severity="warning" title={"This team doesn't have the the Direct Paging integration yet"}>
|
||||
<HorizontalGroup>
|
||||
<Text>
|
||||
Empty integration for this team will be created automatically. Consider selecting responders by
|
||||
schedule or user below
|
||||
</Text>
|
||||
</HorizontalGroup>
|
||||
</Alert>
|
||||
))}
|
||||
</VerticalGroup>
|
||||
);
|
||||
};
|
||||
|
||||
const submitButtonDisabled = !(
|
||||
selectedTeamId &&
|
||||
(selectedTeamDirectPaging || userResponders.length || scheduleResponders.length)
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Drawer scrollableContent title="Create manual alert group" onClose={onHide} closeOnMaskClick={false}>
|
||||
<VerticalGroup spacing="lg">
|
||||
<EscalationVariants
|
||||
value={{ userResponders, scheduleResponders }}
|
||||
onUpdateEscalationVariants={onUpdateEscalationVariants}
|
||||
/>
|
||||
<GForm form={manualAlertFormConfig} data={data} onSubmit={handleFormSubmit} />
|
||||
{store.teamStore.currentTeam.slack_team_identity && (
|
||||
<Block className={cx('info-block')}>
|
||||
<Icon name="info-circle" />{' '}
|
||||
<Text type="secondary">
|
||||
The alert group will also be posted to #{store.teamStore.currentTeam?.slack_channel?.display_name} Slack
|
||||
channel.
|
||||
</Text>
|
||||
</Block>
|
||||
)}
|
||||
<HorizontalGroup justify="flex-end">
|
||||
<Button variant="secondary" onClick={onHide}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
form={manualAlertFormConfig.name}
|
||||
disabled={!userResponders.length && !scheduleResponders.length}
|
||||
>
|
||||
Create
|
||||
</Button>
|
||||
</HorizontalGroup>
|
||||
</VerticalGroup>
|
||||
</Drawer>
|
||||
</>
|
||||
<Drawer
|
||||
scrollableContent
|
||||
title="Create manual alert group (Direct Paging)"
|
||||
onClose={onHide}
|
||||
closeOnMaskClick={false}
|
||||
width="70%"
|
||||
>
|
||||
<VerticalGroup>
|
||||
<GForm form={manualAlertFormConfig} data={data} onSubmit={handleFormSubmit} />
|
||||
<Field label="Select team you want to notify">
|
||||
<GrafanaTeamSelect withoutModal onSelect={onUpdateSelectedTeam} />
|
||||
</Field>
|
||||
<DirectPagingIntegrationVariants
|
||||
selectedTeamId={selectedTeamId}
|
||||
selectedTeamDirectPaging={selectedTeamDirectPaging}
|
||||
chatOpsAvailableChannels={chatOpsAvailableChannels}
|
||||
/>
|
||||
<EscalationVariants
|
||||
value={{ userResponders, scheduleResponders }}
|
||||
onUpdateEscalationVariants={onUpdateEscalationVariants}
|
||||
variant={'secondary'}
|
||||
withLabels={true}
|
||||
/>
|
||||
<HorizontalGroup justify="flex-end">
|
||||
<Button variant="secondary" onClick={onHide}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" form={manualAlertFormConfig.name} disabled={submitButtonDisabled}>
|
||||
Create
|
||||
</Button>
|
||||
</HorizontalGroup>
|
||||
</VerticalGroup>
|
||||
</Drawer>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ const CreateAlertReceiveChannelContainer = observer((props: CreateAlertReceiveCh
|
|||
label="Assign to team"
|
||||
description="OnCall teams allow you to organize integrations so you can filter and set up access. "
|
||||
>
|
||||
<GrafanaTeamSelect withoutModal onSelect={setSelectedTeam} />
|
||||
<GrafanaTeamSelect withoutModal onSelect={setSelectedTeam} defaultValue={user.current_team} />
|
||||
</Field>
|
||||
</div>
|
||||
<hr />
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ export interface EscalationVariantsProps {
|
|||
variant?: 'secondary' | 'primary';
|
||||
hideSelected?: boolean;
|
||||
disabled?: boolean;
|
||||
withLabels?: boolean;
|
||||
}
|
||||
|
||||
const EscalationVariants = observer(
|
||||
|
|
@ -35,6 +36,7 @@ const EscalationVariants = observer(
|
|||
variant = 'primary',
|
||||
hideSelected = false,
|
||||
disabled,
|
||||
withLabels = false,
|
||||
}: EscalationVariantsProps) => {
|
||||
const [showEscalationVariants, setShowEscalationVariants] = useState(false);
|
||||
|
||||
|
|
@ -103,7 +105,7 @@ const EscalationVariants = observer(
|
|||
<div className={cx('body')}>
|
||||
{!hideSelected && Boolean(value.userResponders.length || value.scheduleResponders.length) && (
|
||||
<>
|
||||
<Label>Responders:</Label>
|
||||
<Label>Additional Responders will be notified immediately:</Label>
|
||||
<ul className={cx('responders-list')}>
|
||||
{value.userResponders.map((responder, index) => (
|
||||
<UserResponder
|
||||
|
|
@ -125,6 +127,7 @@ const EscalationVariants = observer(
|
|||
</>
|
||||
)}
|
||||
<div className={cx('assign-responders-button')}>
|
||||
{withLabels && <Label>Assign additional responders from other teams (by user or by schedule)</Label>}
|
||||
<WithPermissionControlTooltip userAction={UserActions.AlertGroupsWrite}>
|
||||
<Button
|
||||
icon="users-alt"
|
||||
|
|
@ -134,7 +137,7 @@ const EscalationVariants = observer(
|
|||
setShowEscalationVariants(true);
|
||||
}}
|
||||
>
|
||||
Add responders
|
||||
Invite additional responders
|
||||
</Button>
|
||||
</WithPermissionControlTooltip>
|
||||
</div>
|
||||
|
|
@ -230,11 +233,11 @@ const UserResponder = ({ important, data, onImportantChange, handleDelete }) =>
|
|||
}}
|
||||
onChange={onImportantChange}
|
||||
/>
|
||||
<Text type="secondary">notification chain</Text>
|
||||
<Text type="secondary">notification policies</Text>
|
||||
</HorizontalGroup>
|
||||
) : (
|
||||
<HorizontalGroup>
|
||||
<Tooltip content="User doesn't have configured notification chains">
|
||||
<Tooltip content="User doesn't have configured notification policies">
|
||||
<Icon name="exclamation-triangle" style={{ color: 'var(--error-text-color)' }} />
|
||||
</Tooltip>
|
||||
</HorizontalGroup>
|
||||
|
|
|
|||
|
|
@ -18,15 +18,16 @@ interface GrafanaTeamSelectProps {
|
|||
onSelect: (id: GrafanaTeam['id']) => void;
|
||||
onHide?: () => void;
|
||||
withoutModal?: boolean;
|
||||
defaultValue?: GrafanaTeam['id'];
|
||||
}
|
||||
|
||||
const GrafanaTeamSelect = observer(({ onSelect, onHide, withoutModal }: GrafanaTeamSelectProps) => {
|
||||
const GrafanaTeamSelect = observer(({ onSelect, onHide, withoutModal, defaultValue }: GrafanaTeamSelectProps) => {
|
||||
const store = useStore();
|
||||
|
||||
const { userStore, grafanaTeamStore } = store;
|
||||
const user = userStore.currentUser;
|
||||
|
||||
const [selectedTeam, setSelectedTeam] = useState<GrafanaTeam['id']>(user.current_team);
|
||||
const [selectedTeam, setSelectedTeam] = useState<GrafanaTeam['id']>(defaultValue);
|
||||
|
||||
const grafanaTeams = grafanaTeamStore.getSearchResult();
|
||||
|
||||
|
|
|
|||
|
|
@ -62,8 +62,12 @@ const IntegrationForm = observer((props: IntegrationFormProps) => {
|
|||
.then((response) => {
|
||||
history.push(`${PLUGIN_ROOT}/integrations/${response.id}`);
|
||||
})
|
||||
.catch(() => {
|
||||
openErrorNotification('Something went wrong, please try again later.');
|
||||
.catch((err) => {
|
||||
if (err.response?.data?.length > 0) {
|
||||
openErrorNotification(err.response.data);
|
||||
} else {
|
||||
openErrorNotification('Something went wrong, please try again later.');
|
||||
}
|
||||
})
|
||||
: alertReceiveChannelStore.update(id, data)
|
||||
).then(() => {
|
||||
|
|
|
|||
|
|
@ -111,7 +111,7 @@ class Incidents extends React.Component<IncidentsPageProps, IncidentsPageState>
|
|||
const { showAddAlertGroupForm } = this.state;
|
||||
const {
|
||||
store,
|
||||
store: { alertGroupStore },
|
||||
store: { alertGroupStore, alertReceiveChannelStore },
|
||||
} = this.props;
|
||||
|
||||
if (!alertGroupStore.irmPlan && !store.isOpenSource()) {
|
||||
|
|
@ -126,7 +126,7 @@ class Incidents extends React.Component<IncidentsPageProps, IncidentsPageState>
|
|||
<Text.Title level={3}>Alert Groups</Text.Title>
|
||||
<WithPermissionControlTooltip userAction={UserActions.AlertGroupsWrite}>
|
||||
<Button icon="plus" onClick={this.handleOnClickEscalateTo}>
|
||||
New alert group
|
||||
New manual alert group
|
||||
</Button>
|
||||
</WithPermissionControlTooltip>
|
||||
</HorizontalGroup>
|
||||
|
|
@ -142,6 +142,7 @@ class Incidents extends React.Component<IncidentsPageProps, IncidentsPageState>
|
|||
onCreate={(id: Alert['pk']) => {
|
||||
history.push(`${PLUGIN_ROOT}/alert-groups/${id}`);
|
||||
}}
|
||||
alertReceiveChannelStore={alertReceiveChannelStore}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -326,7 +326,7 @@ class Integration extends React.Component<IntegrationProps, IntegrationState> {
|
|||
Autoresolve:
|
||||
</Text>
|
||||
<Text type="primary">
|
||||
{IntegrationHelper.truncateLine(templates['resolve_condition_template'] || '')}
|
||||
{IntegrationHelper.truncateLine(templates['resolve_condition_template'] || 'disabled')}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
|
|
@ -964,6 +964,17 @@ const HowToConnectComponent: React.FC<{ id: AlertReceiveChannel['id'] }> = ({ id
|
|||
const item = alertReceiveChannelStore.items[id];
|
||||
const url = item?.integration_url || item?.inbound_email;
|
||||
|
||||
const howToConnectTagName = (integration: string) => {
|
||||
switch (integration) {
|
||||
case 'direct_paging':
|
||||
return 'Manual';
|
||||
case 'email':
|
||||
return 'Inbound Email';
|
||||
default:
|
||||
return 'HTTP Endpoint';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<IntegrationBlock
|
||||
noContent={hasAlerts}
|
||||
|
|
@ -976,29 +987,50 @@ const HowToConnectComponent: React.FC<{ id: AlertReceiveChannel['id'] }> = ({ id
|
|||
className={cx('how-to-connect__tag')}
|
||||
>
|
||||
<Text type="primary" size="small" className={cx('radius')}>
|
||||
{item?.inbound_email ? 'Inbound Email' : 'HTTP Endpoint'}
|
||||
{howToConnectTagName(item?.integration)}
|
||||
</Text>
|
||||
</Tag>
|
||||
{url && (
|
||||
<IntegrationInputField
|
||||
value={url}
|
||||
className={cx('integration__input-field')}
|
||||
showExternal={!!item?.integration_url}
|
||||
/>
|
||||
{item?.integration === 'direct_paging' ? (
|
||||
<>
|
||||
<Text type="secondary">Alert Groups raised manually via Web or ChatOps</Text>
|
||||
<a
|
||||
href="https://grafana.com/docs/oncall/latest/integrations/manual"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className={cx('u-pull-right')}
|
||||
>
|
||||
<Text type="link" size="small">
|
||||
<HorizontalGroup>
|
||||
How it works
|
||||
<Icon name="external-link-alt" />
|
||||
</HorizontalGroup>
|
||||
</Text>
|
||||
</a>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{url && (
|
||||
<IntegrationInputField
|
||||
value={url}
|
||||
className={cx('integration__input-field')}
|
||||
showExternal={!!item?.integration_url}
|
||||
/>
|
||||
)}
|
||||
<a
|
||||
href="https://grafana.com/docs/oncall/latest/integrations/"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className={cx('u-pull-right')}
|
||||
>
|
||||
<Text type="link" size="small">
|
||||
<HorizontalGroup>
|
||||
How to connect
|
||||
<Icon name="external-link-alt" />
|
||||
</HorizontalGroup>
|
||||
</Text>
|
||||
</a>
|
||||
</>
|
||||
)}
|
||||
<a
|
||||
href="https://grafana.com/docs/oncall/latest/integrations/"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className={cx('u-pull-right')}
|
||||
>
|
||||
<Text type="link" size="small">
|
||||
<HorizontalGroup>
|
||||
How to connect
|
||||
<Icon name="external-link-alt" />
|
||||
</HorizontalGroup>
|
||||
</Text>
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
content={hasAlerts ? null : renderContent()}
|
||||
|
|
@ -1006,12 +1038,20 @@ const HowToConnectComponent: React.FC<{ id: AlertReceiveChannel['id'] }> = ({ id
|
|||
);
|
||||
|
||||
function renderContent() {
|
||||
const callToAction = () => {
|
||||
if (item?.integration === 'direct_paging') {
|
||||
return <Text type={'primary'}>try to raise a demo alert group via Web or Chatops</Text>;
|
||||
} else {
|
||||
return item.demo_alert_enabled && <Text type={'primary'}>; try to send a demo alert</Text>;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<VerticalGroup justify={'flex-start'} spacing={'xs'}>
|
||||
{!hasAlerts && (
|
||||
<HorizontalGroup spacing={'xs'}>
|
||||
<Icon name="fa fa-spinner" size="md" className={cx('loadingPlaceholder')} />
|
||||
<Text type={'primary'}>No alerts yet; try to send a demo alert</Text>
|
||||
<Text type={'primary'}>No alerts yet;</Text> {callToAction()}
|
||||
</HorizontalGroup>
|
||||
)}
|
||||
</VerticalGroup>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue