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:
Ildar Iskhakov 2023-07-13 13:41:31 +08:00 committed by GitHub
parent d24dc4b630
commit 0b28815d46
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 353 additions and 186 deletions

View file

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

View file

@ -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 = []

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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(() => {

View file

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

View file

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