remove organization maintenance mode + fix integration maintenance mode (#2511)
This commit is contained in:
parent
939590fe4c
commit
d24dc4b630
34 changed files with 371 additions and 764 deletions
14
CHANGELOG.md
14
CHANGELOG.md
|
|
@ -15,18 +15,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
### Changed
|
||||
|
||||
- Deprecated `/maintenance` web UI page. Maintenance is now handled at the integration level and can be performed
|
||||
within a single integration's page. by @Ukochka ([2497](https://github.com/grafana/oncall/issues/2497))
|
||||
|
||||
within a single integration's page. by @Ukochka ([#2497](https://github.com/grafana/oncall/issues/2497))
|
||||
|
||||
### Fixed
|
||||
|
||||
- Schedules: Long popup does not fit screen & buttons unreachable & objects outside of the popup [1002](https://github.com/grafana/oncall/issues/1002)
|
||||
- New schedules white theme issues [2356](https://github.com/grafana/oncall/issues/2356)
|
||||
- Fixed a bug in the integration maintenance mode workflow where a user could not start/stop an integration's
|
||||
maintenance mode by @joeyorlando ([#2511](https://github.com/grafana/oncall/issues/2511))
|
||||
- Schedules: Long popup does not fit screen & buttons unreachable & objects outside of the popup [#1002](https://github.com/grafana/oncall/issues/1002)
|
||||
- New schedules white theme issues [#2356](https://github.com/grafana/oncall/issues/2356)
|
||||
|
||||
## v1.3.9 (2023-07-12)
|
||||
|
||||
### Added
|
||||
|
||||
- Bring new Jinja editor to webhooks ([2344](https://github.com/grafana/oncall/issues/2344))
|
||||
- Bring new Jinja editor to webhooks ([#2344](https://github.com/grafana/oncall/issues/2344))
|
||||
|
||||
### Fixed
|
||||
|
||||
|
|
@ -83,7 +85,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
### Changed
|
||||
|
||||
- UI drawer updates for webhooks2 ([#2419](https://github.com/grafana/oncall/pull/2419))
|
||||
- Removed url from sms notification, changed format ([2317](https://github.com/grafana/oncall/pull/2317))
|
||||
- Removed url from sms notification, changed format ([#2317](https://github.com/grafana/oncall/pull/2317))
|
||||
|
||||
## v1.3.3 (2023-06-29)
|
||||
|
||||
|
|
|
|||
|
|
@ -231,9 +231,7 @@ class EscalationSnapshotMixin:
|
|||
"""
|
||||
AlertGroup = apps.get_model("alerts", "AlertGroup")
|
||||
|
||||
is_on_maintenace_or_debug_mode = (
|
||||
self.channel.maintenance_mode is not None or self.channel.organization.maintenance_mode is not None
|
||||
)
|
||||
is_on_maintenace_or_debug_mode = self.channel.maintenance_mode is not None
|
||||
|
||||
if (
|
||||
self.is_restricted
|
||||
|
|
|
|||
|
|
@ -128,10 +128,8 @@ class Alert(models.Model):
|
|||
if group_created:
|
||||
# all code below related to maintenance mode
|
||||
maintenance_uuid = None
|
||||
if alert_receive_channel.organization.maintenance_mode == AlertReceiveChannel.MAINTENANCE:
|
||||
maintenance_uuid = alert_receive_channel.organization.maintenance_uuid
|
||||
|
||||
elif alert_receive_channel.maintenance_mode == AlertReceiveChannel.MAINTENANCE:
|
||||
if alert_receive_channel.maintenance_mode == AlertReceiveChannel.MAINTENANCE:
|
||||
maintenance_uuid = alert_receive_channel.maintenance_uuid
|
||||
|
||||
if maintenance_uuid is not None:
|
||||
|
|
|
|||
|
|
@ -426,7 +426,6 @@ class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models.
|
|||
return self.maintenance_uuid is not None
|
||||
|
||||
def stop_maintenance(self, user: User) -> None:
|
||||
Organization = apps.get_model("user_management", "Organization")
|
||||
AlertReceiveChannel = apps.get_model("alerts", "AlertReceiveChannel")
|
||||
|
||||
try:
|
||||
|
|
@ -436,13 +435,6 @@ class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models.
|
|||
except AlertReceiveChannel.DoesNotExist:
|
||||
pass
|
||||
|
||||
try:
|
||||
organization_on_maintenance = Organization.objects.get(maintenance_uuid=self.maintenance_uuid)
|
||||
organization_on_maintenance.force_disable_maintenance(user)
|
||||
return
|
||||
except Organization.DoesNotExist:
|
||||
pass
|
||||
|
||||
self.resolve_by_disable_maintenance()
|
||||
|
||||
@property
|
||||
|
|
|
|||
|
|
@ -46,11 +46,8 @@ def send_alert_create_signal(alert_id):
|
|||
|
||||
task_logger.debug(f"Started send_alert_create_signal task for alert {alert_id}")
|
||||
alert = Alert.objects.get(pk=alert_id)
|
||||
is_on_maintenace_mode = (
|
||||
alert.group.channel.maintenance_mode == AlertReceiveChannel.MAINTENANCE
|
||||
or alert.group.channel.organization.maintenance_mode == AlertReceiveChannel.MAINTENANCE
|
||||
)
|
||||
if not is_on_maintenace_mode:
|
||||
|
||||
if alert.group.channel.maintenance_mode != AlertReceiveChannel.MAINTENANCE:
|
||||
alert_create_signal.send(
|
||||
sender=send_alert_create_signal,
|
||||
alert=alert_id,
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ from .task_logger import task_logger
|
|||
def disable_maintenance(*args, **kwargs):
|
||||
AlertGroup = apps.get_model("alerts", "AlertGroup")
|
||||
User = apps.get_model("user_management", "User")
|
||||
Organization = apps.get_model("user_management", "Organization")
|
||||
user = None
|
||||
object_under_maintenance = None
|
||||
user_id = kwargs.get("user_id")
|
||||
|
|
@ -37,12 +36,6 @@ def disable_maintenance(*args, **kwargs):
|
|||
task_logger.info(
|
||||
f"AlertReceiveChannel for disable_maintenance does not exists. Id: {alert_receive_channel_id}"
|
||||
)
|
||||
elif "organization_id" in kwargs:
|
||||
organization_id = kwargs["organization_id"]
|
||||
try:
|
||||
object_under_maintenance = Organization.objects.select_for_update().get(pk=organization_id)
|
||||
except Organization.DoesNotExist:
|
||||
task_logger.info(f"Organization for disable_maintenance does not exists. Id: {organization_id}")
|
||||
|
||||
else:
|
||||
task_logger.info(f"Invalid instance id passed in disable_maintenance. Got: {kwargs}")
|
||||
|
|
@ -90,7 +83,6 @@ def disable_maintenance(*args, **kwargs):
|
|||
)
|
||||
def check_maintenance_finished(*args, **kwargs):
|
||||
AlertReceiveChannel = apps.get_model("alerts", "AlertReceiveChannel")
|
||||
Organization = apps.get_model("user_management", "Organization")
|
||||
now = timezone.now()
|
||||
maintenance_finish_at = ExpressionWrapper(
|
||||
(F("maintenance_started_at") + F("maintenance_duration")), output_field=fields.DateTimeField()
|
||||
|
|
@ -107,15 +99,3 @@ def check_maintenance_finished(*args, **kwargs):
|
|||
args=(),
|
||||
kwargs={"alert_receive_channel_id": id, "force": True},
|
||||
)
|
||||
|
||||
organization_with_expired_maintenance_ids = (
|
||||
Organization.objects.filter(maintenance_started_at__isnull=False)
|
||||
.annotate(maintenance_finish_at=maintenance_finish_at)
|
||||
.filter(maintenance_finish_at__lt=now)
|
||||
.values_list("pk", flat=True)
|
||||
)
|
||||
for id in organization_with_expired_maintenance_ids:
|
||||
disable_maintenance.apply_async(
|
||||
args=(),
|
||||
kwargs={"organization_id": id, "force": True},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -21,11 +21,7 @@ def send_update_log_report_signal(log_record_pk=None, alert_group_pk=None):
|
|||
)
|
||||
return
|
||||
|
||||
is_on_maintenace_mode = (
|
||||
alert_group.channel.maintenance_mode == AlertReceiveChannel.MAINTENANCE
|
||||
or alert_group.channel.organization.maintenance_mode == AlertReceiveChannel.MAINTENANCE
|
||||
)
|
||||
if is_on_maintenace_mode:
|
||||
if alert_group.channel.maintenance_mode == AlertReceiveChannel.MAINTENANCE:
|
||||
task_logger.debug(
|
||||
f'send_update_log_report_signal: alert_group={alert_group_pk} msg="skip alert_group_update_log_report_signal due to maintenace"'
|
||||
)
|
||||
|
|
|
|||
|
|
@ -86,40 +86,6 @@ def test_maintenance_integration_will_not_start_twice(
|
|||
assert alert_receive_channel.maintenance_author == user
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_start_maintenance_team(maintenance_test_setup, mock_start_disable_maintenance_task):
|
||||
organization, user = maintenance_test_setup
|
||||
|
||||
mode = AlertReceiveChannel.MAINTENANCE
|
||||
duration = AlertReceiveChannel.DURATION_ONE_HOUR.seconds
|
||||
|
||||
organization.start_maintenance(mode, duration, user)
|
||||
|
||||
assert organization.maintenance_mode == mode
|
||||
assert organization.maintenance_duration == AlertReceiveChannel.DURATION_ONE_HOUR
|
||||
assert organization.maintenance_uuid is not None
|
||||
assert organization.maintenance_started_at is not None
|
||||
assert organization.maintenance_author == user
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_maintenance_team_will_not_start_twice(maintenance_test_setup, mock_start_disable_maintenance_task):
|
||||
organization, user = maintenance_test_setup
|
||||
|
||||
mode = AlertReceiveChannel.MAINTENANCE
|
||||
duration = AlertReceiveChannel.DURATION_ONE_HOUR.seconds
|
||||
|
||||
organization.start_maintenance(mode, duration, user)
|
||||
with pytest.raises(MaintenanceCouldNotBeStartedError):
|
||||
organization.start_maintenance(mode, duration, user)
|
||||
|
||||
assert organization.maintenance_mode == mode
|
||||
assert organization.maintenance_duration == AlertReceiveChannel.DURATION_ONE_HOUR
|
||||
assert organization.maintenance_uuid is not None
|
||||
assert organization.maintenance_started_at is not None
|
||||
assert organization.maintenance_author == user
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_alert_attached_to_maintenance_incident_integration(
|
||||
maintenance_test_setup,
|
||||
|
|
@ -151,38 +117,6 @@ def test_alert_attached_to_maintenance_incident_integration(
|
|||
assert alert.group.root_alert_group == maintenance_incident
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_alert_attached_to_maintenance_incident_team(
|
||||
maintenance_test_setup,
|
||||
make_alert_receive_channel,
|
||||
make_alert_with_custom_create_method,
|
||||
mock_start_disable_maintenance_task,
|
||||
):
|
||||
organization, user = maintenance_test_setup
|
||||
|
||||
alert_receive_channel = make_alert_receive_channel(
|
||||
organization, integration=AlertReceiveChannel.INTEGRATION_GRAFANA
|
||||
)
|
||||
|
||||
mode = AlertReceiveChannel.MAINTENANCE
|
||||
duration = AlertReceiveChannel.DURATION_ONE_HOUR.seconds
|
||||
|
||||
organization.start_maintenance(mode, duration, user)
|
||||
maintenance_incident = AlertGroup.all_objects.get(maintenance_uuid=organization.maintenance_uuid)
|
||||
|
||||
alert = make_alert_with_custom_create_method(
|
||||
title="test_title",
|
||||
message="test_message",
|
||||
image_url="test_img_url",
|
||||
link_to_upstream_details=None,
|
||||
alert_receive_channel=alert_receive_channel,
|
||||
raw_request_data={"message": "test"},
|
||||
integration_unique_data={},
|
||||
)
|
||||
|
||||
assert alert.group.root_alert_group == maintenance_incident
|
||||
|
||||
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
def test_stop_maintenance(
|
||||
maintenance_test_setup,
|
||||
|
|
@ -214,9 +148,3 @@ def test_stop_maintenance(
|
|||
alert.refresh_from_db()
|
||||
assert maintenance_incident.resolved_by == AlertGroup.DISABLE_MAINTENANCE
|
||||
assert alert.group.resolved_by == AlertGroup.DISABLE_MAINTENANCE
|
||||
|
||||
assert organization.maintenance_mode is None
|
||||
assert organization.maintenance_duration is None
|
||||
assert organization.maintenance_uuid is None
|
||||
assert organization.maintenance_started_at is None
|
||||
assert organization.maintenance_author is None
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ class OrganizationSerializer(EagerLoadingMixin, serializers.ModelSerializer):
|
|||
slack_team_identity = FastSlackTeamIdentitySerializer(read_only=True)
|
||||
|
||||
name = serializers.CharField(required=False, allow_null=True, allow_blank=True, source="org_title")
|
||||
maintenance_till = serializers.ReadOnlyField(source="till_maintenance_timestamp")
|
||||
slack_channel = serializers.SerializerMethodField()
|
||||
|
||||
SELECT_RELATED = ["slack_team_identity"]
|
||||
|
|
@ -32,14 +31,10 @@ class OrganizationSerializer(EagerLoadingMixin, serializers.ModelSerializer):
|
|||
"pk",
|
||||
"name",
|
||||
"slack_team_identity",
|
||||
"maintenance_mode",
|
||||
"maintenance_till",
|
||||
"slack_channel",
|
||||
]
|
||||
read_only_fields = [
|
||||
"slack_team_identity",
|
||||
"maintenance_mode",
|
||||
"maintenance_till",
|
||||
]
|
||||
|
||||
def get_slack_channel(self, obj):
|
||||
|
|
|
|||
|
|
@ -96,7 +96,9 @@ def test_update_notify_multiple_users_step(escalation_policy_internal_api_setup,
|
|||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.json()["step"] == EscalationPolicy.STEP_NOTIFY_MULTIPLE_USERS
|
||||
assert response.json()["notify_to_users_queue"] == [first_user.public_primary_key, second_user.public_primary_key]
|
||||
assert sorted(response.json()["notify_to_users_queue"]) == sorted(
|
||||
[first_user.public_primary_key, second_user.public_primary_key]
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
|
|
|
|||
|
|
@ -1,272 +0,0 @@
|
|||
import pytest
|
||||
from django.urls import reverse
|
||||
from rest_framework import status
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from apps.alerts.models import AlertReceiveChannel
|
||||
from apps.user_management.models import Organization
|
||||
|
||||
# TODO: should probably modify these tests to take into account new rbac permissions
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def maintenance_internal_api_setup(
|
||||
make_organization_and_user_with_plugin_token,
|
||||
make_escalation_chain,
|
||||
make_alert_receive_channel,
|
||||
):
|
||||
organization, user, token = make_organization_and_user_with_plugin_token()
|
||||
make_escalation_chain(organization)
|
||||
alert_receive_channel = make_alert_receive_channel(organization)
|
||||
return token, organization, user, alert_receive_channel
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_start_maintenance_integration(
|
||||
maintenance_internal_api_setup, mock_start_disable_maintenance_task, make_user_auth_headers
|
||||
):
|
||||
token, _, user, alert_receive_channel = maintenance_internal_api_setup
|
||||
client = APIClient()
|
||||
|
||||
url = reverse("api-internal:start_maintenance")
|
||||
data = {
|
||||
"mode": AlertReceiveChannel.MAINTENANCE,
|
||||
"duration": AlertReceiveChannel.DURATION_ONE_HOUR.total_seconds(),
|
||||
"type": "alert_receive_channel",
|
||||
"alert_receive_channel_id": alert_receive_channel.public_primary_key,
|
||||
}
|
||||
response = client.post(url, data=data, format="json", **make_user_auth_headers(user, token))
|
||||
|
||||
alert_receive_channel.refresh_from_db()
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert alert_receive_channel.maintenance_mode == AlertReceiveChannel.MAINTENANCE
|
||||
assert alert_receive_channel.maintenance_duration == AlertReceiveChannel.DURATION_ONE_HOUR
|
||||
assert alert_receive_channel.maintenance_uuid is not None
|
||||
assert alert_receive_channel.maintenance_started_at is not None
|
||||
assert alert_receive_channel.maintenance_author is not None
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_start_maintenance_integration_user_team(
|
||||
maintenance_internal_api_setup, mock_start_disable_maintenance_task, make_user_auth_headers, make_team
|
||||
):
|
||||
token, organization, user, alert_receive_channel = maintenance_internal_api_setup
|
||||
another_team = make_team(organization)
|
||||
user.current_team = another_team
|
||||
user.save()
|
||||
|
||||
client = APIClient()
|
||||
|
||||
url = reverse("api-internal:start_maintenance")
|
||||
data = {
|
||||
"mode": AlertReceiveChannel.MAINTENANCE,
|
||||
"duration": AlertReceiveChannel.DURATION_ONE_HOUR.total_seconds(),
|
||||
"type": "alert_receive_channel",
|
||||
"alert_receive_channel_id": alert_receive_channel.public_primary_key,
|
||||
}
|
||||
response = client.post(url, data=data, format="json", **make_user_auth_headers(user, token))
|
||||
|
||||
alert_receive_channel.refresh_from_db()
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert alert_receive_channel.maintenance_mode == AlertReceiveChannel.MAINTENANCE
|
||||
assert alert_receive_channel.maintenance_duration == AlertReceiveChannel.DURATION_ONE_HOUR
|
||||
assert alert_receive_channel.maintenance_uuid is not None
|
||||
assert alert_receive_channel.maintenance_started_at is not None
|
||||
assert alert_receive_channel.maintenance_author is not None
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_start_maintenance_integration_different_team(
|
||||
maintenance_internal_api_setup, mock_start_disable_maintenance_task, make_user_auth_headers, make_team
|
||||
):
|
||||
token, organization, user, alert_receive_channel = maintenance_internal_api_setup
|
||||
another_team = make_team(organization)
|
||||
other_team = make_team(organization)
|
||||
user.current_team = another_team
|
||||
user.save()
|
||||
# integration belongs to non-general team, != user current team
|
||||
alert_receive_channel.team = other_team
|
||||
alert_receive_channel.save()
|
||||
|
||||
client = APIClient()
|
||||
|
||||
url = reverse("api-internal:start_maintenance")
|
||||
data = {
|
||||
"mode": AlertReceiveChannel.MAINTENANCE,
|
||||
"duration": AlertReceiveChannel.DURATION_ONE_HOUR.total_seconds(),
|
||||
"type": "alert_receive_channel",
|
||||
"alert_receive_channel_id": alert_receive_channel.public_primary_key,
|
||||
}
|
||||
response = client.post(url, data=data, format="json", **make_user_auth_headers(user, token))
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
|
||||
alert_receive_channel.refresh_from_db()
|
||||
assert alert_receive_channel.maintenance_mode is None
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_stop_maintenance_integration(
|
||||
maintenance_internal_api_setup,
|
||||
mock_start_disable_maintenance_task,
|
||||
make_user_auth_headers,
|
||||
):
|
||||
token, _, user, alert_receive_channel = maintenance_internal_api_setup
|
||||
client = APIClient()
|
||||
mode = AlertReceiveChannel.MAINTENANCE
|
||||
duration = AlertReceiveChannel.DURATION_ONE_HOUR.seconds
|
||||
alert_receive_channel.start_maintenance(mode, duration, user)
|
||||
url = reverse("api-internal:stop_maintenance")
|
||||
data = {
|
||||
"type": "alert_receive_channel",
|
||||
"alert_receive_channel_id": alert_receive_channel.public_primary_key,
|
||||
}
|
||||
response = client.post(url, data=data, format="json", **make_user_auth_headers(user, token))
|
||||
alert_receive_channel.refresh_from_db()
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert alert_receive_channel.maintenance_mode is None
|
||||
assert alert_receive_channel.maintenance_duration is None
|
||||
assert alert_receive_channel.maintenance_uuid is None
|
||||
assert alert_receive_channel.maintenance_started_at is None
|
||||
assert alert_receive_channel.maintenance_author is None
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_start_maintenance_organization(
|
||||
maintenance_internal_api_setup,
|
||||
mock_start_disable_maintenance_task,
|
||||
make_user_auth_headers,
|
||||
):
|
||||
token, organization, user, _ = maintenance_internal_api_setup
|
||||
client = APIClient()
|
||||
|
||||
url = reverse("api-internal:start_maintenance")
|
||||
data = {
|
||||
"mode": Organization.MAINTENANCE,
|
||||
"duration": Organization.DURATION_ONE_HOUR.total_seconds(),
|
||||
"type": "organization",
|
||||
}
|
||||
response = client.post(url, data=data, format="json", **make_user_auth_headers(user, token))
|
||||
|
||||
organization.refresh_from_db()
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert organization.maintenance_mode == Organization.MAINTENANCE
|
||||
assert organization.maintenance_duration == Organization.DURATION_ONE_HOUR
|
||||
assert organization.maintenance_uuid is not None
|
||||
assert organization.maintenance_started_at is not None
|
||||
assert organization.maintenance_author is not None
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_stop_maintenance_team(
|
||||
maintenance_internal_api_setup,
|
||||
mock_start_disable_maintenance_task,
|
||||
make_user_auth_headers,
|
||||
):
|
||||
token, organization, user, _ = maintenance_internal_api_setup
|
||||
client = APIClient()
|
||||
mode = Organization.MAINTENANCE
|
||||
duration = AlertReceiveChannel.DURATION_ONE_HOUR.seconds
|
||||
organization.start_maintenance(mode, duration, user)
|
||||
url = reverse("api-internal:stop_maintenance")
|
||||
data = {
|
||||
"type": "organization",
|
||||
}
|
||||
response = client.post(url, data=data, format="json", **make_user_auth_headers(user, token))
|
||||
organization.refresh_from_db()
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert organization.maintenance_mode is None
|
||||
assert organization.maintenance_duration is None
|
||||
assert organization.maintenance_uuid is None
|
||||
assert organization.maintenance_started_at is None
|
||||
assert organization.maintenance_author is None
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_maintenances_list(
|
||||
maintenance_internal_api_setup,
|
||||
mock_start_disable_maintenance_task,
|
||||
make_user_auth_headers,
|
||||
):
|
||||
token, organization, user, alert_receive_channel = maintenance_internal_api_setup
|
||||
client = APIClient()
|
||||
mode = AlertReceiveChannel.MAINTENANCE
|
||||
duration = AlertReceiveChannel.DURATION_ONE_HOUR.seconds
|
||||
alert_receive_channel.start_maintenance(mode, duration, user)
|
||||
organization.start_maintenance(mode, duration, user)
|
||||
url = reverse("api-internal:maintenance")
|
||||
response = client.get(url, format="json", **make_user_auth_headers(user, token))
|
||||
|
||||
expected_payload = [
|
||||
{
|
||||
"organization_id": organization.public_primary_key,
|
||||
"type": "organization",
|
||||
"maintenance_mode": 1,
|
||||
"maintenance_till_timestamp": organization.till_maintenance_timestamp,
|
||||
"started_at_timestamp": organization.started_at_timestamp,
|
||||
},
|
||||
{
|
||||
"alert_receive_channel_id": alert_receive_channel.public_primary_key,
|
||||
"type": "alert_receive_channel",
|
||||
"maintenance_mode": 1,
|
||||
"maintenance_till_timestamp": alert_receive_channel.till_maintenance_timestamp,
|
||||
"started_at_timestamp": alert_receive_channel.started_at_timestamp,
|
||||
},
|
||||
]
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.json() == expected_payload
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_maintenances_list_include_all_user_teams(
|
||||
maintenance_internal_api_setup,
|
||||
mock_start_disable_maintenance_task,
|
||||
make_user_auth_headers,
|
||||
make_team,
|
||||
):
|
||||
token, organization, user, alert_receive_channel = maintenance_internal_api_setup
|
||||
another_team = make_team(organization)
|
||||
other_team = make_team(organization)
|
||||
# setup user teams
|
||||
user.teams.add(another_team)
|
||||
user.teams.add(other_team)
|
||||
user.current_team = other_team
|
||||
user.save()
|
||||
# integration belongs to non-general team, != user current team
|
||||
alert_receive_channel.team = another_team
|
||||
alert_receive_channel.save()
|
||||
|
||||
client = APIClient()
|
||||
mode = AlertReceiveChannel.MAINTENANCE
|
||||
duration = AlertReceiveChannel.DURATION_ONE_HOUR.seconds
|
||||
alert_receive_channel.start_maintenance(mode, duration, user)
|
||||
url = reverse("api-internal:maintenance")
|
||||
response = client.get(url, format="json", **make_user_auth_headers(user, token))
|
||||
|
||||
expected_payload = [
|
||||
{
|
||||
"alert_receive_channel_id": alert_receive_channel.public_primary_key,
|
||||
"type": "alert_receive_channel",
|
||||
"maintenance_mode": 1,
|
||||
"maintenance_till_timestamp": alert_receive_channel.till_maintenance_timestamp,
|
||||
"started_at_timestamp": alert_receive_channel.started_at_timestamp,
|
||||
},
|
||||
]
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.json() == expected_payload
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_empty_maintenances_list(
|
||||
maintenance_internal_api_setup, mock_start_disable_maintenance_task, make_user_auth_headers
|
||||
):
|
||||
token, _, user, alert_receive_channel = maintenance_internal_api_setup
|
||||
client = APIClient()
|
||||
url = reverse("api-internal:maintenance")
|
||||
response = client.get(url, format="json", **make_user_auth_headers(user, token))
|
||||
|
||||
expected_payload = []
|
||||
alert_receive_channel.refresh_from_db()
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.data == expected_payload
|
||||
|
|
@ -15,7 +15,6 @@ from .views.features import FeaturesAPIView
|
|||
from .views.gitops import TerraformGitOpsView, TerraformStateView
|
||||
from .views.integration_heartbeat import IntegrationHeartBeatView
|
||||
from .views.live_setting import LiveSettingViewSet
|
||||
from .views.maintenance import MaintenanceAPIView, MaintenanceStartAPIView, MaintenanceStopAPIView
|
||||
from .views.on_call_shifts import OnCallShiftView
|
||||
from .views.organization import (
|
||||
CurrentOrganizationView,
|
||||
|
|
@ -84,9 +83,6 @@ urlpatterns = [
|
|||
),
|
||||
optional_slash_path("terraform_file", TerraformGitOpsView.as_view(), name="terraform_file"),
|
||||
optional_slash_path("terraform_imports", TerraformStateView.as_view(), name="terraform_imports"),
|
||||
optional_slash_path("maintenance", MaintenanceAPIView.as_view(), name="maintenance"),
|
||||
optional_slash_path("start_maintenance", MaintenanceStartAPIView.as_view(), name="start_maintenance"),
|
||||
optional_slash_path("stop_maintenance", MaintenanceStopAPIView.as_view(), name="stop_maintenance"),
|
||||
optional_slash_path("slack_settings", SlackTeamSettingsAPIView.as_view(), name="slack-settings"),
|
||||
optional_slash_path(
|
||||
"slack_settings/acknowledge_remind_options",
|
||||
|
|
|
|||
|
|
@ -1,161 +0,0 @@
|
|||
from rest_framework import status
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from apps.alerts.models import AlertReceiveChannel
|
||||
from apps.alerts.models.maintainable_object import MaintainableObject
|
||||
from apps.api.permissions import RBACPermission
|
||||
from apps.auth_token.auth import PluginAuthentication
|
||||
from common.api_helpers.exceptions import BadRequest
|
||||
from common.api_helpers.mixins import TeamFilteringMixin
|
||||
from common.exceptions import MaintenanceCouldNotBeStartedError
|
||||
|
||||
|
||||
class GetObjectMixin:
|
||||
def get_object(self, request):
|
||||
organization = request.auth.organization
|
||||
type = request.data.get("type", None)
|
||||
|
||||
if type == "organization":
|
||||
instance = organization
|
||||
elif type == "alert_receive_channel":
|
||||
pk = request.data.get("alert_receive_channel_id", None)
|
||||
if pk is not None:
|
||||
try:
|
||||
instance = AlertReceiveChannel.objects.get(
|
||||
public_primary_key=pk,
|
||||
organization=organization,
|
||||
)
|
||||
if instance.team is not None and instance.team not in self.request.user.teams.all():
|
||||
raise BadRequest(detail={"alert_receive_channel_id": ["unknown id"]})
|
||||
except AlertReceiveChannel.DoesNotExist:
|
||||
raise BadRequest(detail={"alert_receive_channel_id": ["unknown id"]})
|
||||
else:
|
||||
raise BadRequest(detail={"alert_receive_channel_id": ["id is required"]})
|
||||
else:
|
||||
raise BadRequest(detail={"type": ["Unknown type"]})
|
||||
|
||||
return instance
|
||||
|
||||
|
||||
class MaintenanceAPIView(APIView, TeamFilteringMixin):
|
||||
"""Deprecated. Maintenance management is now performed on integrations page (alert_receive_channel/ endpoint))"""
|
||||
|
||||
authentication_classes = (PluginAuthentication,)
|
||||
permission_classes = (IsAuthenticated, RBACPermission)
|
||||
|
||||
rbac_permissions = {
|
||||
"get": [RBACPermission.Permissions.MAINTENANCE_READ],
|
||||
"filters": [RBACPermission.Permissions.MAINTENANCE_READ],
|
||||
}
|
||||
|
||||
def get(self, request):
|
||||
organization = self.request.auth.organization
|
||||
|
||||
response = []
|
||||
integrations_under_maintenance = (
|
||||
AlertReceiveChannel.objects.filter(
|
||||
maintenance_mode__isnull=False, organization=organization, *self.available_teams_lookup_args
|
||||
)
|
||||
.distinct()
|
||||
.order_by("maintenance_started_at")
|
||||
)
|
||||
|
||||
if organization.maintenance_mode is not None:
|
||||
response.append(
|
||||
{
|
||||
"organization_id": organization.public_primary_key,
|
||||
"type": "organization",
|
||||
"maintenance_mode": organization.maintenance_mode,
|
||||
"maintenance_till_timestamp": organization.till_maintenance_timestamp,
|
||||
"started_at_timestamp": organization.started_at_timestamp,
|
||||
}
|
||||
)
|
||||
|
||||
for i in integrations_under_maintenance:
|
||||
response.append(
|
||||
{
|
||||
"alert_receive_channel_id": i.public_primary_key,
|
||||
"type": "alert_receive_channel",
|
||||
"maintenance_mode": i.maintenance_mode,
|
||||
"maintenance_till_timestamp": i.till_maintenance_timestamp,
|
||||
"started_at_timestamp": i.started_at_timestamp,
|
||||
}
|
||||
)
|
||||
|
||||
return Response(response, status=200)
|
||||
|
||||
@action(methods=["get"], detail=False)
|
||||
def filters(self, request):
|
||||
filter_name = request.query_params.get("search", None)
|
||||
api_root = "/api/internal/v1/"
|
||||
|
||||
filter_options = [
|
||||
{
|
||||
"name": "team",
|
||||
"type": "team_select",
|
||||
"href": api_root + "teams/",
|
||||
"global": True,
|
||||
},
|
||||
]
|
||||
|
||||
if filter_name is not None:
|
||||
filter_options = list(filter(lambda f: filter_name in f["name"], filter_options))
|
||||
|
||||
return Response(filter_options)
|
||||
|
||||
|
||||
class MaintenanceStartAPIView(GetObjectMixin, APIView):
|
||||
"""Deprecated. Maintenance management is now performed on integrations page (alert_receive_channel/ endpoint))"""
|
||||
|
||||
authentication_classes = (PluginAuthentication,)
|
||||
permission_classes = (IsAuthenticated, RBACPermission)
|
||||
rbac_permissions = {
|
||||
"post": [RBACPermission.Permissions.MAINTENANCE_WRITE],
|
||||
}
|
||||
|
||||
def post(self, request):
|
||||
mode = request.data.get("mode", None)
|
||||
duration = request.data.get("duration", None)
|
||||
try:
|
||||
mode = int(mode)
|
||||
except (ValueError, TypeError):
|
||||
raise BadRequest(detail={"mode": ["Invalid mode"]})
|
||||
if mode not in [MaintainableObject.DEBUG_MAINTENANCE, MaintainableObject.MAINTENANCE]:
|
||||
raise BadRequest(detail={"mode": ["Unknown mode"]})
|
||||
try:
|
||||
duration = int(duration)
|
||||
except (ValueError, TypeError):
|
||||
raise BadRequest(detail={"duration": ["Invalid duration"]})
|
||||
if duration not in MaintainableObject.maintenance_duration_options_in_seconds():
|
||||
raise BadRequest(detail={"mode": ["Unknown duration"]})
|
||||
|
||||
instance = self.get_object(request)
|
||||
try:
|
||||
instance.start_maintenance(mode, duration, request.user)
|
||||
except MaintenanceCouldNotBeStartedError as e:
|
||||
if type(instance) == AlertReceiveChannel:
|
||||
detail = {"alert_receive_channel_id": ["Already on maintenance"]}
|
||||
else:
|
||||
detail = str(e)
|
||||
raise BadRequest(detail=detail)
|
||||
|
||||
return Response(status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class MaintenanceStopAPIView(GetObjectMixin, APIView):
|
||||
"""Deprecated. Maintenance management is now performed on integrations page (alert_receive_channel/ endpoint))"""
|
||||
|
||||
authentication_classes = (PluginAuthentication,)
|
||||
permission_classes = (IsAuthenticated, RBACPermission)
|
||||
rbac_permissions = {
|
||||
"post": [RBACPermission.Permissions.MAINTENANCE_WRITE],
|
||||
}
|
||||
|
||||
def post(self, request):
|
||||
instance = self.get_object(request)
|
||||
user = request.user
|
||||
instance.force_disable_maintenance(user)
|
||||
return Response(status=status.HTTP_200_OK)
|
||||
|
|
@ -2,17 +2,11 @@ from rest_framework import serializers
|
|||
|
||||
from apps.user_management.models import Organization
|
||||
|
||||
from .maintenance import MaintainableObjectSerializerMixin
|
||||
|
||||
|
||||
class OrganizationSerializer(serializers.ModelSerializer, MaintainableObjectSerializerMixin):
|
||||
class OrganizationSerializer(serializers.ModelSerializer):
|
||||
id = serializers.ReadOnlyField(read_only=True, source="public_primary_key")
|
||||
|
||||
class Meta:
|
||||
model = Organization
|
||||
fields = MaintainableObjectSerializerMixin.Meta.fields + [
|
||||
"id",
|
||||
]
|
||||
read_only_fields = MaintainableObjectSerializerMixin.Meta.fields + [
|
||||
"id",
|
||||
]
|
||||
fields = ["id"]
|
||||
read_only_fields = ["id"]
|
||||
|
|
|
|||
|
|
@ -66,12 +66,7 @@ class AlertShootingStep(scenario_step.ScenarioStep):
|
|||
AlertGroup.all_objects.filter(pk=alert.group.pk).update(slack_message_sent=False)
|
||||
raise e
|
||||
|
||||
is_on_debug_mode = (
|
||||
alert.group.channel.maintenance_mode == AlertReceiveChannel.DEBUG_MAINTENANCE
|
||||
or alert.group.channel.organization.maintenance_mode == AlertReceiveChannel.DEBUG_MAINTENANCE
|
||||
)
|
||||
|
||||
if is_on_debug_mode:
|
||||
if alert.group.channel.maintenance_mode == AlertReceiveChannel.DEBUG_MAINTENANCE:
|
||||
self._send_debug_mode_notice(alert.group, channel_id)
|
||||
|
||||
if alert.group.is_maintenance_incident:
|
||||
|
|
|
|||
|
|
@ -11,8 +11,6 @@ from django.utils import timezone
|
|||
from mirage import fields as mirage_fields
|
||||
|
||||
from apps.alerts.models import MaintainableObject
|
||||
from apps.alerts.tasks import disable_maintenance
|
||||
from apps.slack.utils import post_message_to_channel
|
||||
from apps.user_management.subscription_strategy import FreePublicBetaSubscriptionStrategy
|
||||
from common.insight_log import ChatOpsEvent, ChatOpsTypePlug, write_chatops_insight_log
|
||||
from common.oncall_gateway import create_oncall_connector, delete_oncall_connector, delete_slack_connector
|
||||
|
|
@ -75,6 +73,9 @@ class OrganizationManager(models.Manager):
|
|||
return OrganizationQuerySet(self.model, using=self._db).filter(deleted_at__isnull=True)
|
||||
|
||||
|
||||
# TODO: in a subsequent PR, remove the inheritance from MaintainableObject (plus generate the database migration file)
|
||||
# this will remove the maintenance related columns that're no longer used on the organization object
|
||||
# class Organization(models.Model):
|
||||
class Organization(MaintainableObject):
|
||||
auth_tokens: "RelatedManager['ApiAuthToken']"
|
||||
mobile_app_auth_tokens: "RelatedManager['MobileAppAuthToken']"
|
||||
|
|
@ -255,39 +256,6 @@ class Organization(MaintainableObject):
|
|||
token_model = apps.get_model("auth_token", "PluginAuthToken")
|
||||
token_model.objects.filter(organization=self).delete()
|
||||
|
||||
"""
|
||||
Following methods: start_disable_maintenance_task, force_disable_maintenance, get_organization, get_verbal serve for
|
||||
MaintainableObject.
|
||||
"""
|
||||
|
||||
def start_disable_maintenance_task(self, countdown):
|
||||
maintenance_uuid = disable_maintenance.apply_async(
|
||||
args=(),
|
||||
kwargs={
|
||||
"organization_id": self.pk,
|
||||
},
|
||||
countdown=countdown,
|
||||
)
|
||||
return maintenance_uuid
|
||||
|
||||
def force_disable_maintenance(self, user):
|
||||
disable_maintenance(organization_id=self.pk, force=True, user_id=user.pk)
|
||||
|
||||
def get_organization(self):
|
||||
return self
|
||||
|
||||
def get_team(self):
|
||||
return None
|
||||
|
||||
def get_verbal(self):
|
||||
return self.org_title
|
||||
|
||||
def notify_about_maintenance_action(self, text, send_to_general_log_channel=True):
|
||||
# TODO: this method should be refactored.
|
||||
# It's binded to slack and sending maintenance notification only there.
|
||||
if send_to_general_log_channel:
|
||||
post_message_to_channel(self, self.general_log_channel_id, text)
|
||||
|
||||
"""
|
||||
Following methods:
|
||||
phone_calls_left, sms_left, emails_left
|
||||
|
|
|
|||
|
|
@ -0,0 +1,144 @@
|
|||
import { test, expect, Page, Locator } from '../fixtures';
|
||||
import { verifyThatAlertGroupIsRoutedCorrectlyButNotEscalated } from '../utils/alertGroup';
|
||||
import { EscalationStep, createEscalationChain } from '../utils/escalationChain';
|
||||
import { clickButton, generateRandomValue, selectDropdownValue } from '../utils/forms';
|
||||
import {
|
||||
assignEscalationChainToIntegration,
|
||||
createIntegration,
|
||||
filterIntegrationsTableAndGoToDetailPage,
|
||||
sendDemoAlert,
|
||||
} from '../utils/integrations';
|
||||
import { goToOnCallPage } from '../utils/navigation';
|
||||
|
||||
type MaintenanceModeType = 'Debug' | 'Maintenance';
|
||||
|
||||
test.describe('maintenance mode works', () => {
|
||||
test.slow(); // this test is doing a good amount of work, give it time
|
||||
|
||||
const MAINTENANCE_DURATION = '1 hour';
|
||||
const REMAINING_TIME_TEXT = '59m left';
|
||||
const REMAINING_TIME_TOOLTIP_TEST_ID = 'maintenance-mode-remaining-time-tooltip';
|
||||
|
||||
const createRoutedText = (escalationChainName: string): string =>
|
||||
`alert group assigned to route "default" with escalation chain "${escalationChainName}"`;
|
||||
|
||||
const _openIntegrationSettingsPopup = async (page: Page): Promise<Locator> => {
|
||||
const integrationSettingsPopupElement = page.getByTestId('integration-settings-context-menu');
|
||||
await integrationSettingsPopupElement.click();
|
||||
return integrationSettingsPopupElement;
|
||||
};
|
||||
|
||||
const getRemainingTimeTooltip = (page: Page): Locator => page.getByTestId(REMAINING_TIME_TOOLTIP_TEST_ID);
|
||||
|
||||
const enableMaintenanceMode = async (page: Page, mode: MaintenanceModeType): Promise<void> => {
|
||||
const integrationSettingsPopupElement = await _openIntegrationSettingsPopup(page);
|
||||
/**
|
||||
* we need to click twice here, because adding the escalation chain route
|
||||
* doesn't unfocus out of the select element after selecting an option
|
||||
*/
|
||||
await integrationSettingsPopupElement.click();
|
||||
|
||||
// open the maintenance mode settings drawer + fill in the maintenance details
|
||||
await page.getByTestId('integration-start-maintenance').click();
|
||||
|
||||
// fill in the form
|
||||
const maintenanceModeDrawer = page.getByTestId('maintenance-mode-drawer');
|
||||
|
||||
await selectDropdownValue({
|
||||
page,
|
||||
startingLocator: maintenanceModeDrawer,
|
||||
selectType: 'grafanaSelect',
|
||||
placeholderText: 'Choose mode',
|
||||
value: mode,
|
||||
optionExactMatch: false,
|
||||
});
|
||||
|
||||
await selectDropdownValue({
|
||||
page,
|
||||
startingLocator: maintenanceModeDrawer,
|
||||
selectType: 'grafanaSelect',
|
||||
placeholderText: 'Choose duration',
|
||||
value: MAINTENANCE_DURATION,
|
||||
optionExactMatch: false,
|
||||
});
|
||||
|
||||
await maintenanceModeDrawer.getByTestId('create-maintenance-button').click();
|
||||
|
||||
const maintenanceModeRemainingTimeTooltip = getRemainingTimeTooltip(page);
|
||||
await maintenanceModeRemainingTimeTooltip.waitFor({ state: 'visible' });
|
||||
|
||||
expect(await page.getByTestId(`${REMAINING_TIME_TOOLTIP_TEST_ID}-text`).innerText()).toContain(REMAINING_TIME_TEXT);
|
||||
};
|
||||
|
||||
const disableMaintenanceMode = async (page: Page, integrationName: string): Promise<void> => {
|
||||
await goToOnCallPage(page, 'integrations');
|
||||
|
||||
await filterIntegrationsTableAndGoToDetailPage(page, integrationName);
|
||||
await _openIntegrationSettingsPopup(page);
|
||||
|
||||
// click the stop maintenance button
|
||||
await page.getByTestId('integration-stop-maintenance').click();
|
||||
|
||||
// in the modal popup, confirm that we want to stop it
|
||||
await clickButton({
|
||||
page,
|
||||
buttonText: 'Stop',
|
||||
startingLocator: page.getByRole('dialog'),
|
||||
});
|
||||
|
||||
await getRemainingTimeTooltip(page).waitFor({ state: 'hidden' });
|
||||
};
|
||||
|
||||
const createIntegrationAndEscalationChainAndEnableMaintenanceMode = async (
|
||||
page: Page,
|
||||
userName: string,
|
||||
maintenanceModeType: MaintenanceModeType
|
||||
): Promise<{
|
||||
escalationChainName: string;
|
||||
integrationName: string;
|
||||
}> => {
|
||||
const escalationChainName = generateRandomValue();
|
||||
const integrationName = generateRandomValue();
|
||||
|
||||
await createEscalationChain(page, escalationChainName, EscalationStep.NotifyUsers, userName);
|
||||
await createIntegration(page, integrationName);
|
||||
await assignEscalationChainToIntegration(page, escalationChainName);
|
||||
await enableMaintenanceMode(page, maintenanceModeType);
|
||||
|
||||
return { escalationChainName, integrationName };
|
||||
};
|
||||
|
||||
test('debug mode', async ({ adminRolePage: { page, userName } }) => {
|
||||
const { escalationChainName, integrationName } = await createIntegrationAndEscalationChainAndEnableMaintenanceMode(
|
||||
page,
|
||||
userName,
|
||||
'Debug'
|
||||
);
|
||||
await sendDemoAlert(page);
|
||||
await verifyThatAlertGroupIsRoutedCorrectlyButNotEscalated(
|
||||
page,
|
||||
integrationName,
|
||||
createRoutedText(escalationChainName)
|
||||
);
|
||||
|
||||
await disableMaintenanceMode(page, integrationName);
|
||||
});
|
||||
|
||||
test('"maintenance" mode', async ({ adminRolePage: { page, userName } }) => {
|
||||
const { integrationName } = await createIntegrationAndEscalationChainAndEnableMaintenanceMode(
|
||||
page,
|
||||
userName,
|
||||
'Maintenance'
|
||||
);
|
||||
await sendDemoAlert(page);
|
||||
|
||||
// TODO: there seems to be a bug here where "maintenance" mode alert groups don't show up in the UI
|
||||
// await verifyThatAlertGroupIsRoutedCorrectlyButNotEscalated(
|
||||
// page,
|
||||
// integrationName,
|
||||
// createRoutedText(escalationChainName)
|
||||
// );
|
||||
|
||||
await disableMaintenanceMode(page, integrationName);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,10 +1,15 @@
|
|||
import { Page, expect } from '@playwright/test';
|
||||
import { Locator, Page, expect } from '@playwright/test';
|
||||
import { selectDropdownValue, selectValuePickerValue } from './forms';
|
||||
import { goToOnCallPage } from './navigation';
|
||||
|
||||
const MAX_RETRIES = 5;
|
||||
const ALERT_GROUP_REGISTERED_TEXT = 'alert group registered';
|
||||
|
||||
// const sleep = async (seconds: number) => new Promise((resolve) => setTimeout(resolve, seconds * 1000));
|
||||
const getIncidentTimelineList = async (page: Page): Promise<Locator> => {
|
||||
const incidentTimelineList = page.getByTestId('incident-timeline-list');
|
||||
await incidentTimelineList.waitFor({ state: 'visible' });
|
||||
return incidentTimelineList;
|
||||
};
|
||||
|
||||
/**
|
||||
* recursively refreshes the page waiting for the background celery workers to have done their job of
|
||||
|
|
@ -15,18 +20,28 @@ const incidentTimelineContainsStep = async (page: Page, triggeredStepText: strin
|
|||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
if (!page.getByTestId('incident-timeline-list').getByText(triggeredStepText)) {
|
||||
const incidentTimelineList = await getIncidentTimelineList(page);
|
||||
|
||||
if (!incidentTimelineList.getByText(triggeredStepText)) {
|
||||
await page.reload({ waitUntil: 'networkidle' });
|
||||
return incidentTimelineContainsStep(page, triggeredStepText, (retryNum += 1));
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
export const verifyThatAlertGroupIsTriggered = async (
|
||||
/**
|
||||
* recursively refreshes the page waiting for the background celery workers to have done their job of
|
||||
* creating the alert group
|
||||
*/
|
||||
export const filterAlertGroupsTableByIntegrationAndGoToDetailPage = async (
|
||||
page: Page,
|
||||
integrationName: string,
|
||||
triggeredStepText: string
|
||||
retryNum = 0
|
||||
): Promise<void> => {
|
||||
if (retryNum > MAX_RETRIES) {
|
||||
throw new Error('we were not able to properly filter the alert groups table by integration');
|
||||
}
|
||||
|
||||
await goToOnCallPage(page, 'incidents');
|
||||
|
||||
// filter by integration
|
||||
|
|
@ -40,10 +55,48 @@ export const verifyThatAlertGroupIsTriggered = async (
|
|||
await selectValuePickerValue(page, integrationName, false);
|
||||
|
||||
/**
|
||||
* wait for the alert groups to be filtered then
|
||||
* click on the alert group and go to the individual alert group page
|
||||
* wait for the alert groups to be filtered then by this particular integration (toBeVisible assertion),
|
||||
* then click on the alert group and go to the individual alert group page
|
||||
*/
|
||||
await (await page.waitForSelector('table > tbody > tr > td:nth-child(4) a')).click();
|
||||
const firstTableRow = page.locator('table > tbody > tr:first-child');
|
||||
|
||||
try {
|
||||
/**
|
||||
* wait for up to 5 seconds for the alert groups to be filtered, if the first row does not correspond
|
||||
* to `integrationName` assume that the background workers have not created it yet and lets
|
||||
* recursively retry this function
|
||||
*/
|
||||
await firstTableRow.getByText(integrationName).waitFor({ state: 'visible', timeout: 5000 });
|
||||
await firstTableRow.locator('td:nth-child(4) a').click();
|
||||
} catch (err) {
|
||||
return filterAlertGroupsTableByIntegrationAndGoToDetailPage(page, integrationName, (retryNum += 1));
|
||||
}
|
||||
};
|
||||
|
||||
export const verifyThatAlertGroupIsRoutedCorrectlyButNotEscalated = async (
|
||||
page: Page,
|
||||
integrationName: string,
|
||||
routedText: string
|
||||
): Promise<void> => {
|
||||
await filterAlertGroupsTableByIntegrationAndGoToDetailPage(page, integrationName);
|
||||
|
||||
/**
|
||||
* incidentTimelineContainsStep recursively reloads the alert group page until the engine
|
||||
* background workers have processed/escalated the alert group
|
||||
*/
|
||||
expect(await incidentTimelineContainsStep(page, ALERT_GROUP_REGISTERED_TEXT)).toBe(true);
|
||||
|
||||
const incidentTimelineList = await getIncidentTimelineList(page);
|
||||
expect(incidentTimelineList).toContainText(routedText);
|
||||
expect(incidentTimelineList).not.toContainText('triggered step');
|
||||
};
|
||||
|
||||
export const verifyThatAlertGroupIsTriggered = async (
|
||||
page: Page,
|
||||
integrationName: string,
|
||||
triggeredStepText: string
|
||||
): Promise<void> => {
|
||||
await filterAlertGroupsTableByIntegrationAndGoToDetailPage(page, integrationName);
|
||||
|
||||
expect(await incidentTimelineContainsStep(page, triggeredStepText)).toBe(true);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -13,6 +13,10 @@ type SelectDropdownValueArgs = {
|
|||
startingLocator?: Locator;
|
||||
// if true, when selecting the dropdown option, use an exact match, otherwise use a substring contains match
|
||||
optionExactMatch?: boolean;
|
||||
|
||||
// if true, will press enter in the select dropdown. Some dropdowns don't show a list of options
|
||||
// and instead the user must press enter to trigger the search
|
||||
pressEnterInsteadOfSelectingOption?: boolean;
|
||||
};
|
||||
|
||||
type ClickButtonArgs = {
|
||||
|
|
@ -87,9 +91,16 @@ const chooseDropdownValue = async ({ page, value, optionExactMatch = true }: Sel
|
|||
page.locator(`div[id^="react-select-"][id$="-listbox"] >> ${textMatchSelector(optionExactMatch, value)}`).click();
|
||||
|
||||
export const selectDropdownValue = async (args: SelectDropdownValueArgs): Promise<Locator> => {
|
||||
const { page, value, pressEnterInsteadOfSelectingOption } = args;
|
||||
|
||||
const selectElement = await openSelect(args);
|
||||
await selectElement.type(args.value);
|
||||
await chooseDropdownValue(args);
|
||||
await selectElement.type(value);
|
||||
|
||||
if (pressEnterInsteadOfSelectingOption) {
|
||||
await page.keyboard.press('Enter');
|
||||
} else {
|
||||
await chooseDropdownValue(args);
|
||||
}
|
||||
|
||||
return selectElement;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { Page } from '@playwright/test';
|
||||
import { clickButton } from './forms';
|
||||
import { clickButton, selectDropdownValue } from './forms';
|
||||
import { goToOnCallPage } from './navigation';
|
||||
|
||||
const CREATE_INTEGRATION_MODAL_TEST_ID_SELECTOR = 'div[data-testid="create-integration-modal"]';
|
||||
|
|
@ -15,11 +15,7 @@ export const openCreateIntegrationModal = async (page: Page): Promise<void> => {
|
|||
await page.waitForSelector(CREATE_INTEGRATION_MODAL_TEST_ID_SELECTOR);
|
||||
};
|
||||
|
||||
export const createIntegrationAndSendDemoAlert = async (
|
||||
page: Page,
|
||||
integrationName: string,
|
||||
_escalationChainName: string
|
||||
): Promise<void> => {
|
||||
export const createIntegration = async (page: Page, integrationName: string): Promise<void> => {
|
||||
await openCreateIntegrationModal(page);
|
||||
|
||||
// create a webhook integration
|
||||
|
|
@ -27,25 +23,56 @@ export const createIntegrationAndSendDemoAlert = async (
|
|||
|
||||
// fill in the required inputs
|
||||
(await page.waitForSelector('input[name="verbal_name"]', { state: 'attached' })).fill(integrationName);
|
||||
(await page.waitForSelector('textarea[name="description_short"]', { state: 'attached' })).fill("Here goes your integration description");
|
||||
(await page.waitForSelector('textarea[name="description_short"]', { state: 'attached' })).fill(
|
||||
'Here goes your integration description'
|
||||
);
|
||||
|
||||
const grafanaUpdateBtn = page.getByTestId("update-integration-button");
|
||||
const grafanaUpdateBtn = page.getByTestId('update-integration-button');
|
||||
await grafanaUpdateBtn.click();
|
||||
|
||||
/*
|
||||
* TODO: This is slightly more complicated now, change this in next iteration */
|
||||
// const integrationSettingsElement = page.getByTestId('integration-settings');
|
||||
|
||||
// // assign the escalation chain to the integration
|
||||
// await selectDropdownValue({
|
||||
// page,
|
||||
// selectType: 'grafanaSelect',
|
||||
// placeholderText: 'Select Escalation Chain',
|
||||
// value: escalationChainName,
|
||||
// startingLocator: integrationSettingsElement,
|
||||
// });
|
||||
|
||||
// send demo alert
|
||||
await clickButton({ page, buttonText: 'Send demo alert', dataTestId: 'send-demo-alert' });
|
||||
await clickButton({ page, buttonText: 'Send Alert', dataTestId: "submit-send-alert" })
|
||||
};
|
||||
|
||||
export const assignEscalationChainToIntegration = async (page: Page, escalationChainName: string): Promise<void> => {
|
||||
await page.getByTestId('integration-escalation-chain-not-selected').click();
|
||||
|
||||
// assign the escalation chain to the integration
|
||||
await selectDropdownValue({
|
||||
page,
|
||||
selectType: 'grafanaSelect',
|
||||
placeholderText: 'Select Escalation Chain',
|
||||
value: escalationChainName,
|
||||
startingLocator: page.getByTestId('integration-block-item'),
|
||||
});
|
||||
};
|
||||
|
||||
export const sendDemoAlert = async (page: Page): Promise<void> => {
|
||||
await clickButton({ page, buttonText: 'Send demo alert', dataTestId: 'send-demo-alert' });
|
||||
await clickButton({ page, buttonText: 'Send Alert', dataTestId: 'submit-send-alert' });
|
||||
await page.getByTestId('demo-alert-sent-notification').waitFor({ state: 'visible' });
|
||||
};
|
||||
|
||||
export const createIntegrationAndSendDemoAlert = async (
|
||||
page: Page,
|
||||
integrationName: string,
|
||||
escalationChainName: string
|
||||
): Promise<void> => {
|
||||
await createIntegration(page, integrationName);
|
||||
await assignEscalationChainToIntegration(page, escalationChainName);
|
||||
await sendDemoAlert(page);
|
||||
};
|
||||
|
||||
export const filterIntegrationsTableAndGoToDetailPage = async (page: Page, integrationName: string): Promise<void> => {
|
||||
// filter the integrations page by the integration in question, then go to its detail page
|
||||
await selectDropdownValue({
|
||||
page,
|
||||
selectType: 'grafanaSelect',
|
||||
placeholderText: 'Search or filter results...',
|
||||
value: integrationName,
|
||||
pressEnterInsteadOfSelectingOption: true,
|
||||
});
|
||||
|
||||
await (
|
||||
await page.waitForSelector(
|
||||
`div[data-testid="integrations-table"] table > tbody > tr > td:first-child a >> text=${integrationName}`
|
||||
)
|
||||
).click();
|
||||
};
|
||||
|
|
|
|||
|
|
@ -18,6 +18,10 @@ export const ADMIN_USER_STORAGE_STATE = path.join(__dirname, 'integration-tests/
|
|||
*/
|
||||
const config: PlaywrightTestConfig = {
|
||||
testDir: './integration-tests',
|
||||
|
||||
/* Maximum time all the tests can run for. */
|
||||
globalTimeout: 20 * 60 * 1000, // 20 minutes
|
||||
|
||||
/* Maximum time one test can run for. */
|
||||
timeout: 60 * 1000,
|
||||
expect: {
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ interface IntegrationBlockItemProps {
|
|||
|
||||
const IntegrationBlockItem: React.FC<IntegrationBlockItemProps> = (props) => {
|
||||
return (
|
||||
<div className={cx('blockItem')}>
|
||||
<div className={cx('blockItem')} data-testid="integration-block-item">
|
||||
<div className={cx('blockItem__leftDelimitator')} />
|
||||
<div className={cx('blockItem__content')}>{props.children}</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -24,7 +24,10 @@ interface TooltipBadgeProps {
|
|||
const cx = cn.bind(styles);
|
||||
|
||||
const TooltipBadge: FC<TooltipBadgeProps> = (props) => {
|
||||
const { borderType, text, tooltipTitle, tooltipContent, onHover, addPadding, icon, customIcon, className } = props;
|
||||
const { borderType, text, tooltipTitle, tooltipContent, onHover, addPadding, icon, customIcon, className, ...rest } =
|
||||
props;
|
||||
|
||||
const testId = rest['data-testid'];
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
|
|
@ -48,10 +51,18 @@ const TooltipBadge: FC<TooltipBadgeProps> = (props) => {
|
|||
className
|
||||
)}
|
||||
onMouseEnter={onHover}
|
||||
{...(testId ? { 'data-testid': testId } : {})}
|
||||
>
|
||||
<HorizontalGroup spacing="xs">
|
||||
{renderIcon()}
|
||||
{text && <Text className={cx('element__text', { [`element__text--${borderType}`]: true })}>{text}</Text>}
|
||||
{text && (
|
||||
<Text
|
||||
className={cx('element__text', { [`element__text--${borderType}`]: true })}
|
||||
{...(testId ? { 'data-testid': `${testId}-text` } : {})}
|
||||
>
|
||||
{text}
|
||||
</Text>
|
||||
)}
|
||||
</HorizontalGroup>
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ export const WithContextMenu: React.FC<WithContextMenuProps> = ({
|
|||
renderMenuItems,
|
||||
forceIsOpen = false,
|
||||
focusOnOpen = true,
|
||||
...rest
|
||||
}) => {
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false || forceIsOpen);
|
||||
const [menuPosition, setMenuPosition] = useState({ x: 0, y: 0 });
|
||||
|
|
@ -36,7 +37,7 @@ export const WithContextMenu: React.FC<WithContextMenuProps> = ({
|
|||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div {...rest}>
|
||||
{children({
|
||||
openMenu: (e) => {
|
||||
setIsMenuOpen(true);
|
||||
|
|
@ -56,6 +57,6 @@ export const WithContextMenu: React.FC<WithContextMenuProps> = ({
|
|||
focusOnOpen={focusOnOpen}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -145,7 +145,9 @@ const CollapsedIntegrationRouteDisplay: React.FC<CollapsedIntegrationRouteDispla
|
|||
<div className={cx('icon-exclamation')}>
|
||||
<Icon name="exclamation-triangle" />
|
||||
</div>
|
||||
<Text type="primary">Not selected</Text>
|
||||
<Text type="primary" data-testid="integration-escalation-chain-not-selected">
|
||||
Not selected
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { SelectableValue } from '@grafana/data';
|
|||
import Emoji from 'react-emoji-render';
|
||||
|
||||
import { FormItem, FormItemType } from 'components/GForm/GForm.types';
|
||||
import { MaintenanceMode } from 'models/maintenance/maintenance.types';
|
||||
import { MaintenanceMode } from 'models/alert_receive_channel/alert_receive_channel.types';
|
||||
|
||||
export const form: { name: string; fields: FormItem[] } = {
|
||||
name: 'Maintenance',
|
||||
|
|
@ -31,6 +31,7 @@ export const form: { name: string; fields: FormItem[] } = {
|
|||
validation: { required: true },
|
||||
normalize: (value) => value,
|
||||
extra: {
|
||||
placeholder: 'Choose mode',
|
||||
options: [
|
||||
{
|
||||
value: MaintenanceMode.Debug,
|
||||
|
|
@ -50,6 +51,7 @@ export const form: { name: string; fields: FormItem[] } = {
|
|||
type: FormItemType.Select,
|
||||
validation: { required: true },
|
||||
extra: {
|
||||
placeholder: 'Choose duration',
|
||||
options: [
|
||||
{
|
||||
value: 3600,
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import { observer } from 'mobx-react';
|
|||
import GForm from 'components/GForm/GForm';
|
||||
import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip';
|
||||
import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_channel.types';
|
||||
import { MaintenanceType } from 'models/maintenance/maintenance.types';
|
||||
import { useStore } from 'state/useStore';
|
||||
import { openNotification, showApiError } from 'utils';
|
||||
import { UserActions } from 'utils/authorization';
|
||||
|
|
@ -21,7 +20,6 @@ const cx = cn.bind(styles);
|
|||
|
||||
interface MaintenanceFormProps {
|
||||
initialData: {
|
||||
type?: MaintenanceType;
|
||||
alert_receive_channel_id?: AlertReceiveChannel['id'];
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
|
@ -35,23 +33,22 @@ const MaintenanceForm = observer((props: MaintenanceFormProps) => {
|
|||
|
||||
const store = useStore();
|
||||
|
||||
const { maintenanceStore } = store;
|
||||
const { alertReceiveChannelStore } = store;
|
||||
|
||||
const handleSubmit = useCallback((data) => {
|
||||
maintenanceStore
|
||||
.startMaintenanceMode(
|
||||
MaintenanceType.alert_receive_channel,
|
||||
const handleSubmit = useCallback(async (data) => {
|
||||
try {
|
||||
await alertReceiveChannelStore.startMaintenanceMode(
|
||||
initialData.alert_receive_channel_id,
|
||||
data.mode,
|
||||
data.duration,
|
||||
data.alert_receive_channel_id
|
||||
)
|
||||
.then(() => {
|
||||
onHide();
|
||||
onUpdate();
|
||||
data.duration
|
||||
);
|
||||
|
||||
openNotification('Maintenance has been started');
|
||||
})
|
||||
.catch(showApiError);
|
||||
onHide();
|
||||
onUpdate();
|
||||
openNotification('Maintenance has been started');
|
||||
} catch (err) {
|
||||
showApiError(err);
|
||||
}
|
||||
}, []);
|
||||
|
||||
if (initialData.disabled) {
|
||||
|
|
@ -65,7 +62,7 @@ const MaintenanceForm = observer((props: MaintenanceFormProps) => {
|
|||
|
||||
return (
|
||||
<Drawer width="640px" scrollableContent title="Start Maintenance Mode" onClose={onHide} closeOnMaskClick={false}>
|
||||
<div className={cx('content')}>
|
||||
<div className={cx('content')} data-testid="maintenance-mode-drawer">
|
||||
<VerticalGroup>
|
||||
Start maintenance mode when performing scheduled maintenance or updates on the infrastructure, which may
|
||||
trigger false alarms.
|
||||
|
|
@ -75,7 +72,7 @@ const MaintenanceForm = observer((props: MaintenanceFormProps) => {
|
|||
Cancel
|
||||
</Button>
|
||||
<WithPermissionControlTooltip userAction={UserActions.MaintenanceWrite}>
|
||||
<Button form={form.name} type="submit">
|
||||
<Button form={form.name} type="submit" data-testid="create-maintenance-button">
|
||||
Start
|
||||
</Button>
|
||||
</WithPermissionControlTooltip>
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import {
|
|||
AlertReceiveChannel,
|
||||
AlertReceiveChannelOption,
|
||||
AlertReceiveChannelCounters,
|
||||
MaintenanceMode,
|
||||
} from './alert_receive_channel.types';
|
||||
|
||||
export class AlertReceiveChannelStore extends BaseStore {
|
||||
|
|
@ -456,4 +457,18 @@ export class AlertReceiveChannelStore extends BaseStore {
|
|||
|
||||
this.counters = counters;
|
||||
}
|
||||
|
||||
startMaintenanceMode = (id: AlertReceiveChannel['id'], mode: MaintenanceMode, duration: number): Promise<void> =>
|
||||
makeRequest<null>(`${this.path}${id}/start_maintenance/`, {
|
||||
method: 'POST',
|
||||
data: {
|
||||
mode,
|
||||
duration,
|
||||
},
|
||||
});
|
||||
|
||||
stopMaintenanceMode = (id: AlertReceiveChannel['id']) =>
|
||||
makeRequest<null>(`${this.path}${id}/stop_maintenance/`, {
|
||||
method: 'POST',
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,55 +0,0 @@
|
|||
import { action, observable } from 'mobx';
|
||||
|
||||
import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_channel.types';
|
||||
import BaseStore from 'models/base_store';
|
||||
import { makeRequest } from 'network';
|
||||
import { RootStore } from 'state';
|
||||
|
||||
import { Maintenance, MaintenanceMode, MaintenanceType } from './maintenance.types';
|
||||
|
||||
export class MaintenanceStore extends BaseStore {
|
||||
@observable.shallow
|
||||
maintenances?: Maintenance[];
|
||||
|
||||
constructor(rootStore: RootStore) {
|
||||
super(rootStore);
|
||||
|
||||
this.path = '/maintenance/';
|
||||
}
|
||||
|
||||
@action
|
||||
async updateMaintenances() {
|
||||
this.maintenances = await this.getAll();
|
||||
}
|
||||
|
||||
@action
|
||||
async startMaintenanceMode(
|
||||
type: MaintenanceType,
|
||||
mode: MaintenanceMode,
|
||||
duration: number,
|
||||
alertReceiveChannelId?: AlertReceiveChannel['id']
|
||||
) {
|
||||
return await makeRequest(`/start_maintenance/`, {
|
||||
method: 'POST',
|
||||
data: {
|
||||
type,
|
||||
mode,
|
||||
duration,
|
||||
alert_receive_channel_id: alertReceiveChannelId,
|
||||
},
|
||||
withCredentials: true,
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
async stopMaintenanceMode(type: MaintenanceType, alertReceiveChannelId: AlertReceiveChannel['id']) {
|
||||
return await makeRequest(`/stop_maintenance/`, {
|
||||
method: 'POST',
|
||||
data: {
|
||||
type,
|
||||
alert_receive_channel_id: alertReceiveChannelId,
|
||||
},
|
||||
withCredentials: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_channel.types';
|
||||
|
||||
export enum MaintenanceType {
|
||||
alert_receive_channel = 'alert_receive_channel',
|
||||
organization = 'organization',
|
||||
}
|
||||
|
||||
export enum MaintenanceMode {
|
||||
Debug,
|
||||
Maintenance,
|
||||
}
|
||||
|
||||
export interface Maintenance {
|
||||
alert_receive_channel_id: AlertReceiveChannel['id'];
|
||||
type: MaintenanceType;
|
||||
maintenance_mode: MaintenanceMode;
|
||||
maintenance_till_timestamp: number;
|
||||
started_at_timestamp: number;
|
||||
}
|
||||
|
|
@ -58,7 +58,6 @@ import {
|
|||
} from 'models/alert_receive_channel/alert_receive_channel.types';
|
||||
import { AlertTemplatesDTO } from 'models/alert_templates';
|
||||
import { ChannelFilter } from 'models/channel_filter';
|
||||
import { MaintenanceType } from 'models/maintenance/maintenance.types';
|
||||
import { INTEGRATION_TEMPLATES_LIST } from 'pages/integration/Integration.config';
|
||||
import IntegrationHelper from 'pages/integration/Integration.helper';
|
||||
import styles from 'pages/integration/Integration.module.scss';
|
||||
|
|
@ -606,7 +605,7 @@ class Integration extends React.Component<IntegrationProps, IntegrationState> {
|
|||
|
||||
const DemoNotification: React.FC = () => {
|
||||
return (
|
||||
<div>
|
||||
<div data-testid="demo-alert-sent-notification">
|
||||
Demo alert was generated. Find it on the
|
||||
<PluginLink query={{ page: 'alert-groups' }}> "Alert Groups" </PluginLink>
|
||||
page and make sure it didn't freak out your colleagues 😉
|
||||
|
|
@ -727,7 +726,7 @@ const IntegrationActions: React.FC<IntegrationActionsProps> = ({
|
|||
alertReceiveChannel,
|
||||
changeIsTemplateSettingsOpen,
|
||||
}) => {
|
||||
const { maintenanceStore, alertReceiveChannelStore, heartbeatStore } = useStore();
|
||||
const { alertReceiveChannelStore, heartbeatStore } = useStore();
|
||||
|
||||
const history = useHistory();
|
||||
|
||||
|
|
@ -815,6 +814,7 @@ const IntegrationActions: React.FC<IntegrationActionsProps> = ({
|
|||
</WithPermissionControlTooltip>
|
||||
|
||||
<WithContextMenu
|
||||
data-testid="integration-settings-context-menu"
|
||||
renderMenuItems={() => (
|
||||
<div className={cx('integration__actionsList')} id="integration-menu-options">
|
||||
<div className={cx('integration__actionItem')} onClick={() => openIntegrationSettings()}>
|
||||
|
|
@ -831,7 +831,11 @@ const IntegrationActions: React.FC<IntegrationActionsProps> = ({
|
|||
|
||||
{!alertReceiveChannel.maintenance_till && (
|
||||
<WithPermissionControlTooltip userAction={UserActions.MaintenanceWrite}>
|
||||
<div className={cx('integration__actionItem')} onClick={openStartMaintenance}>
|
||||
<div
|
||||
className={cx('integration__actionItem')}
|
||||
onClick={openStartMaintenance}
|
||||
data-testid="integration-start-maintenance"
|
||||
>
|
||||
<Text type="primary">Start Maintenance</Text>
|
||||
</div>
|
||||
</WithPermissionControlTooltip>
|
||||
|
|
@ -862,6 +866,7 @@ const IntegrationActions: React.FC<IntegrationActionsProps> = ({
|
|||
),
|
||||
});
|
||||
}}
|
||||
data-testid="integration-stop-maintenance"
|
||||
>
|
||||
<Text type="primary">Stop Maintenance</Text>
|
||||
</div>
|
||||
|
|
@ -941,14 +946,13 @@ const IntegrationActions: React.FC<IntegrationActionsProps> = ({
|
|||
setMaintenanceData({ disabled: true, alert_receive_channel_id: alertReceiveChannel.id });
|
||||
}
|
||||
|
||||
function onStopMaintenance() {
|
||||
async function onStopMaintenance() {
|
||||
setConfirmModal(undefined);
|
||||
|
||||
maintenanceStore
|
||||
.stopMaintenanceMode(MaintenanceType.alert_receive_channel, id)
|
||||
.then(() => maintenanceStore.updateMaintenances())
|
||||
.then(() => openNotification('Maintenance has been stopped'))
|
||||
.then(() => alertReceiveChannelStore.updateItem(alertReceiveChannel.id));
|
||||
await alertReceiveChannelStore.stopMaintenanceMode(id);
|
||||
|
||||
openNotification('Maintenance has been stopped');
|
||||
await alertReceiveChannelStore.updateItem(id);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -1063,6 +1067,7 @@ const IntegrationHeader: React.FC<IntegrationHeaderProps> = ({
|
|||
|
||||
{alertReceiveChannel.maintenance_till && (
|
||||
<TooltipBadge
|
||||
data-testid="maintenance-mode-remaining-time-tooltip"
|
||||
borderType="primary"
|
||||
icon="pause"
|
||||
text={IntegrationHelper.getMaintenanceText(alertReceiveChannel.maintenance_till)}
|
||||
|
|
|
|||
|
|
@ -208,6 +208,7 @@ class Integrations extends React.Component<IntegrationsProps, IntegrationsState>
|
|||
/>
|
||||
<GTable
|
||||
emptyText={this.renderNotFound()}
|
||||
data-testid="integrations-table"
|
||||
rowKey="id"
|
||||
data={results}
|
||||
columns={columns}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ import { FiltersStore } from 'models/filters/filters';
|
|||
import { GlobalSettingStore } from 'models/global_setting/global_setting';
|
||||
import { GrafanaTeamStore } from 'models/grafana_team/grafana_team';
|
||||
import { HeartbeatStore } from 'models/heartbeat/heartbeat';
|
||||
import { MaintenanceStore } from 'models/maintenance/maintenance';
|
||||
import { OrganizationLogStore } from 'models/organization_log/organization_log';
|
||||
import { OutgoingWebhookStore } from 'models/outgoing_webhook/outgoing_webhook';
|
||||
import { OutgoingWebhook2Store } from 'models/outgoing_webhook_2/outgoing_webhook_2';
|
||||
|
|
@ -98,7 +97,6 @@ export class RootBaseStore {
|
|||
slackStore: SlackStore = new SlackStore(this);
|
||||
slackChannelStore: SlackChannelStore = new SlackChannelStore(this);
|
||||
heartbeatStore: HeartbeatStore = new HeartbeatStore(this);
|
||||
maintenanceStore: MaintenanceStore = new MaintenanceStore(this);
|
||||
scheduleStore: ScheduleStore = new ScheduleStore(this);
|
||||
userGroupStore: UserGroupStore = new UserGroupStore(this);
|
||||
alertGroupStore: AlertGroupStore = new AlertGroupStore(this);
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ import appEvents from 'grafana/app/core/app_events';
|
|||
import { isArray, concat, isPlainObject, flatMap, map, keys } from 'lodash-es';
|
||||
import qs from 'query-string';
|
||||
|
||||
import { isNetworkError } from 'network';
|
||||
|
||||
export class KeyValuePair<T = string | number> {
|
||||
key: T;
|
||||
value: string;
|
||||
|
|
@ -23,7 +25,7 @@ export const getTzOffsetHours = (): number => {
|
|||
};
|
||||
|
||||
export function showApiError(error: any) {
|
||||
if (error.response.status >= 400 && error.response.status < 500) {
|
||||
if (isNetworkError(error) && error.response && error.response.status >= 400 && error.response.status < 500) {
|
||||
const payload = error.response.data;
|
||||
const text =
|
||||
typeof payload === 'string'
|
||||
|
|
@ -38,7 +40,7 @@ export function showApiError(error: any) {
|
|||
}
|
||||
|
||||
export function refreshPageError(error: AxiosError) {
|
||||
if (error.response?.status === 502) {
|
||||
if (isNetworkError(error) && error.response?.status === 502) {
|
||||
const payload = error.response.data;
|
||||
const text = `Try to refresh the page. ${payload}`;
|
||||
openErrorNotification(text);
|
||||
|
|
@ -48,7 +50,7 @@ export function refreshPageError(error: AxiosError) {
|
|||
}
|
||||
|
||||
export function throttlingError(error: AxiosError) {
|
||||
if (error.response?.status === 429) {
|
||||
if (isNetworkError(error) && error.response?.status === 429) {
|
||||
const seconds = Number(error.response?.headers['retry-after']);
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const text =
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue