diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e41e1d7..c7130cdf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,17 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## Unreleased +## v1.3.55 (2023-11-07) + +### Changed + +- Unify naming of Grafana Cloud / Cloud OnCall / Grafana Cloud OnCall + so that it's always Grafana Cloud OnCall ([#3279](https://github.com/grafana/oncall/pull/3279)) + +### Fixed + +- Fix escalation policy importance going back to default by @vadimkerr ([#3282](https://github.com/grafana/oncall/pull/3282)) +- Improve user permissions query ([#3291](https://github.com/grafana/oncall/pull/3291)) ## v1.3.54 (2023-11-06) diff --git a/docs/sources/mobile-app/installation-and-setup/index.md b/docs/sources/mobile-app/installation-and-setup/index.md index d463822d..c20da7d6 100644 --- a/docs/sources/mobile-app/installation-and-setup/index.md +++ b/docs/sources/mobile-app/installation-and-setup/index.md @@ -44,14 +44,14 @@ To access your QR code: 1. Navigate to the **Users** tab, then tap **View my profile** 1. tap **Mobile app connection** in your profile ->**Note**: The QR code will timeout for security purposes - Screenshots of the QR code are unlikely to work for authentication. +> **Note**: The QR code will timeout for security purposes - Screenshots of the QR code are unlikely to work for authentication. ### Connect to your open source Grafana OnCall account -Grafana OnCall OSS relies on Grafana Cloud as on relay for push notifications. -You must first connect your Grafana OnCall OSS to Grafana Cloud for the mobile app to work. +Grafana OnCall OSS relies on Grafana Cloud OnCall as on relay for push notifications. +You must first connect your Grafana OnCall OSS to Grafana Cloud OnCall for the mobile app to work. -To connect to Grafana Cloud, refer to the Cloud page in your OSS Grafana OnCall instance. +To connect to Grafana Cloud OnCall, refer to the Cloud page in your OSS Grafana OnCall instance. For Grafana OnCall OSS, the QR code includes an authentication token along with a backend URL. Your Grafana OnCall OSS instance should be reachable from the same network as your mobile device, preferably from the internet. diff --git a/docs/sources/open-source/_index.md b/docs/sources/open-source/_index.md index 11107e4d..ae0ca4bb 100644 --- a/docs/sources/open-source/_index.md +++ b/docs/sources/open-source/_index.md @@ -197,13 +197,13 @@ Refer to the following steps to configure the Telegram integration: ## Grafana OSS-Cloud Setup -The benefits of connecting to Grafana Cloud include: +The benefits of connecting to Grafana Cloud OnCall include: -- Cloud OnCall could monitor OSS OnCall uptime using heartbeat +- Grafana Cloud OnCall could monitor OSS OnCall uptime using heartbeat - SMS for user notifications - Phone calls for user notifications. -To connect to Grafana Cloud, refer to the **Cloud** page in your OSS Grafana OnCall instance. +To connect to Grafana Cloud OnCall, refer to the **Cloud** page in your OSS Grafana OnCall instance. ## Supported Phone Providers diff --git a/engine/apps/alerts/escalation_snapshot/escalation_snapshot_mixin.py b/engine/apps/alerts/escalation_snapshot/escalation_snapshot_mixin.py index e835eb69..0c06054c 100644 --- a/engine/apps/alerts/escalation_snapshot/escalation_snapshot_mixin.py +++ b/engine/apps/alerts/escalation_snapshot/escalation_snapshot_mixin.py @@ -239,7 +239,7 @@ class EscalationSnapshotMixin: self.raw_escalation_snapshot["next_step_eta"] = updated_next_step_eta.strftime("%Y-%m-%dT%H:%M:%S.%fZ") return self.raw_escalation_snapshot - def start_escalation_if_needed(self, countdown=START_ESCALATION_DELAY, eta=None): + def start_escalation_if_needed(self, countdown=START_ESCALATION_DELAY, eta=None, continue_escalation=False): """ :type self:AlertGroup """ @@ -259,9 +259,11 @@ class EscalationSnapshotMixin: logger.debug(f"Start escalation for alert group with pk: {self.pk}") - # take raw escalation snapshot from db if escalation is paused + # take raw escalation snapshot from db if escalation is paused or `continue_escalation` flag is True raw_escalation_snapshot = ( - self.build_raw_escalation_snapshot() if not self.pause_escalation else self.raw_escalation_snapshot + self.raw_escalation_snapshot + if self.pause_escalation or continue_escalation + else self.build_raw_escalation_snapshot() ) task_id = celery_uuid() diff --git a/engine/apps/alerts/migrations/0038_remove_alertgroup_is_restricted_db.py b/engine/apps/alerts/migrations/0038_remove_alertgroup_is_restricted_db.py new file mode 100644 index 00000000..facede84 --- /dev/null +++ b/engine/apps/alerts/migrations/0038_remove_alertgroup_is_restricted_db.py @@ -0,0 +1,21 @@ +# Generated by Django 4.2.6 on 2023-11-03 23:02 + +import common.migrations.remove_field +from django.db import migrations +import django_migration_linter as linter + + +class Migration(migrations.Migration): + + dependencies = [ + ('alerts', '0037_remove_alertgroup_is_restricted_state'), + ] + + operations = [ + linter.IgnoreMigration(), + common.migrations.remove_field.RemoveFieldDB( + model_name='AlertGroup', + name='is_restricted', + remove_state_migration=('alerts', '0037_remove_alertgroup_is_restricted_state'), + ), + ] diff --git a/engine/apps/alerts/models/alert_group.py b/engine/apps/alerts/models/alert_group.py index e17fbd66..a7be71da 100644 --- a/engine/apps/alerts/models/alert_group.py +++ b/engine/apps/alerts/models/alert_group.py @@ -143,6 +143,19 @@ class AlertGroupQuerySet(models.QuerySet): pass raise + def filter_active(self, *args, **kwargs): + # filter alert groups with active escalation + return super().filter( + *args, + ~Q(silenced=True, silenced_until__isnull=True), # filter silenced forever alert_groups + **kwargs, + maintenance_uuid__isnull=True, + is_escalation_finished=False, + resolved=False, + acknowledged=False, + root_alert_group=None, + ) + class AlertGroupSlackRenderingMixin: """ diff --git a/engine/apps/alerts/tasks/check_escalation_finished.py b/engine/apps/alerts/tasks/check_escalation_finished.py index f538b3e5..30ae392e 100644 --- a/engine/apps/alerts/tasks/check_escalation_finished.py +++ b/engine/apps/alerts/tasks/check_escalation_finished.py @@ -4,7 +4,6 @@ import typing import requests from celery import shared_task from django.conf import settings -from django.db.models import Q from django.utils import timezone from apps.alerts.tasks.task_logger import task_logger @@ -100,18 +99,9 @@ def check_escalation_finished_task() -> None: now = timezone.now() - datetime.timedelta(minutes=5) two_days_ago = now - datetime.timedelta(days=2) - alert_groups = AlertGroup.objects.using(get_random_readonly_database_key_if_present_otherwise_default()).filter( - ~Q(silenced=True, silenced_until__isnull=True), # filter silenced forever alert_groups - # here we should query maintenance_uuid rather than joining on channel__integration - # and checking for something like ~Q(channel__integration=AlertReceiveChannel.INTEGRATION_MAINTENANCE) - # this avoids an unnecessary join - maintenance_uuid__isnull=True, - is_escalation_finished=False, - resolved=False, - acknowledged=False, - root_alert_group=None, - started_at__range=(two_days_ago, now), - ) + alert_groups = AlertGroup.objects.using( + get_random_readonly_database_key_if_present_otherwise_default() + ).filter_active(started_at__range=(two_days_ago, now)) task_logger.info( f"There are {len(alert_groups)} alert group(s) to audit" diff --git a/engine/apps/alerts/tests/test_alert_group.py b/engine/apps/alerts/tests/test_alert_group.py index ab32c3ed..819b6af9 100644 --- a/engine/apps/alerts/tests/test_alert_group.py +++ b/engine/apps/alerts/tests/test_alert_group.py @@ -546,3 +546,34 @@ def test_alert_group_get_paged_users( paged_users = alert_group.get_paged_users() assert len(paged_users) == 1 assert alert_group.get_paged_users()[0]["pk"] == user.public_primary_key + + +@patch("apps.alerts.models.AlertGroup.start_unsilence_task", return_value=None) +@pytest.mark.django_db +def test_filter_active_alert_groups( + mocked_start_unsilence_task, + make_organization_and_user, + make_alert_receive_channel, + make_alert_group, +): + organization, user = make_organization_and_user() + alert_receive_channel = make_alert_receive_channel(organization) + + # alert groups with active escalation + alert_group_active = make_alert_group(alert_receive_channel) + alert_group_active_silenced = make_alert_group(alert_receive_channel) + alert_group_active_silenced.silence_by_user(user, silence_delay=1800) # silence by period + # alert groups with inactive escalation + alert_group_1 = make_alert_group(alert_receive_channel) + alert_group_1.acknowledge_by_user(user) + alert_group_2 = make_alert_group(alert_receive_channel) + alert_group_2.resolve_by_user(user) + alert_group_3 = make_alert_group(alert_receive_channel) + alert_group_3.attach_by_user(user, alert_group_active) + alert_group_4 = make_alert_group(alert_receive_channel) + alert_group_4.silence_by_user(user, silence_delay=None) # silence forever + + active_alert_groups = AlertGroup.objects.filter_active() + assert active_alert_groups.count() == 2 + assert alert_group_active in active_alert_groups + assert alert_group_active_silenced in active_alert_groups diff --git a/engine/apps/schedules/tests/test_on_call_schedule.py b/engine/apps/schedules/tests/test_on_call_schedule.py index 99409143..e3c12843 100644 --- a/engine/apps/schedules/tests/test_on_call_schedule.py +++ b/engine/apps/schedules/tests/test_on_call_schedule.py @@ -2698,12 +2698,13 @@ def test_shifts_for_user_only_two_users_with_shifts( now = timezone.now() today = now.replace(hour=0, minute=0, second=0, microsecond=0) + tomorrow = today + timezone.timedelta(days=1) start_date = today - timezone.timedelta(days=2) days = 7 data = { - "start": now + timezone.timedelta(hours=1), - "rotation_start": now + timezone.timedelta(hours=1), + "start": tomorrow + timezone.timedelta(hours=1), + "rotation_start": tomorrow + timezone.timedelta(hours=1), "duration": timezone.timedelta(hours=2), "priority_level": 1, "frequency": CustomOnCallShift.FREQUENCY_DAILY, @@ -2733,7 +2734,7 @@ def test_shifts_for_user_only_two_users_with_shifts( passed_shifts, current_shifts, upcoming_shifts = schedule.shifts_for_user(current_user, start_date, days) assert len(passed_shifts) == 0 assert len(current_shifts) == 0 - assert len(upcoming_shifts) == 5 + assert len(upcoming_shifts) == 4 for shift in upcoming_shifts: users = {u["pk"] for u in shift["users"]} assert current_user.public_primary_key in users diff --git a/engine/apps/slack/tests/test_user_group.py b/engine/apps/slack/tests/test_user_group.py index 2f4d354d..0af070f2 100644 --- a/engine/apps/slack/tests/test_user_group.py +++ b/engine/apps/slack/tests/test_user_group.py @@ -154,3 +154,25 @@ def test_populate_slack_usergroups_for_team( assert usergroup.handle == "test_handle" assert usergroup.members == ["test_user_1", "test_user_2"] assert usergroup.is_active + + +@pytest.mark.django_db +def test_get_users_from_members_for_organization( + make_organization_with_slack_team_identity, + make_slack_user_group, + make_user_with_slack_user_identity, +): + organization, slack_team_identity = make_organization_with_slack_team_identity() + + user_1, slack_user_identity_1 = make_user_with_slack_user_identity( + slack_team_identity, organization, slack_id="slack_id_1" + ) + user_2, slack_user_identity_2 = make_user_with_slack_user_identity( + slack_team_identity, organization, slack_id="slack_id_2" + ) + user_group = make_slack_user_group(slack_team_identity) + user_group.members = ["slack_id_1", "slack_id_2"] + user_group.save(update_fields=["members"]) + + users = user_group.get_users_from_members_for_organization(organization) + assert set(users) == {user_1, user_2} diff --git a/engine/apps/user_management/models/user.py b/engine/apps/user_management/models/user.py index 92829216..d6dbbd71 100644 --- a/engine/apps/user_management/models/user.py +++ b/engine/apps/user_management/models/user.py @@ -1,6 +1,7 @@ import datetime import json import logging +import re import typing from urllib.parse import urljoin @@ -34,6 +35,10 @@ if typing.TYPE_CHECKING: logger = logging.getLogger(__name__) +class PermissionsQuery(typing.TypedDict): + permissions__contains: typing.Dict + + class PermissionsRegexQuery(typing.TypedDict): permissions__regex: str @@ -387,7 +392,7 @@ class User(models.Model): @staticmethod def build_permissions_query( permission: LegacyAccessControlCompatiblePermission, organization - ) -> typing.Union[PermissionsRegexQuery, RoleInQuery]: + ) -> typing.Union[PermissionsQuery, PermissionsRegexQuery, RoleInQuery]: """ This method returns a django query filter that is compatible with RBAC as well as legacy "basic" role based authorization. If a permission is provided we simply do @@ -399,7 +404,11 @@ class User(models.Model): """ if organization.is_rbac_permissions_enabled: # https://stackoverflow.com/a/50251879 - return PermissionsRegexQuery(permissions__regex=r".*{0}.*".format(permission.value)) + if settings.DATABASE_TYPE == settings.DATABASE_TYPES.SQLITE3: + # https://docs.djangoproject.com/en/4.2/topics/db/queries/#contains + return PermissionsRegexQuery(permissions__regex=re.escape(permission.value)) + required_permission = {"action": permission.value} + return PermissionsQuery(permissions__contains=[required_permission]) return RoleInQuery(role__lte=permission.fallback_role.value) def get_or_create_notification_policies(self, important=False): diff --git a/engine/engine/management/commands/continue_escalation.py b/engine/engine/management/commands/continue_escalation.py new file mode 100644 index 00000000..a9f2eaab --- /dev/null +++ b/engine/engine/management/commands/continue_escalation.py @@ -0,0 +1,158 @@ +from celery import uuid as celery_uuid +from django.core.management import BaseCommand +from django.utils import timezone + +from apps.alerts.models import AlertGroup +from apps.alerts.tasks import escalate_alert_group, unsilence_task + + +class Command(BaseCommand): + """ + Start escalation for alert groups from the point it was stopped with optionally start unsilence task for silenced + alert groups. + + Usage example: + `python manage.py continue_escalation -ppk "ppk1" "ppk2"` - continue escalation for alert groups with these + public pks + `python manage.py continue_escalation -id 1 2 -uns` - continue escalation for alert groups with these ids and + schedule unsilence task for silenced alert groups + """ + + def add_arguments(self, parser): + group = parser.add_mutually_exclusive_group(required=True) + + group.add_argument( + "-id", "--alert_group_ids", type=int, nargs="+", help="Alert group IDs to restart escalation for." + ) + group.add_argument( + "-ppk", "--alert_group_ppk", type=str, nargs="+", help="Alert group public pks to restart escalation for." + ) + group.add_argument( + "--all", action="store_true", help="Restart escalation for all alert groups with unfinished escalation." + ) + parser.add_argument( + "-uns", "--unsilence_task", action="store_true", help="Restart unsilence task for selected alert groups." + ) + parser.add_argument( # used for cases with migrated organizations to actualize data in escalation snapshot + "-rebuild", + "--rebuild_escalation_snapshot", + action="store_true", + help="Rebuild escalation snapshot for selected alert groups.", + ) + + def handle(self, *args, **options): + alert_group_ids = options["alert_group_ids"] + alert_group_ppk = options["alert_group_ppk"] + restart_all = options["all"] + restart_unsilence_task = options["unsilence_task"] + rebuild_escalation_snapshot = options["rebuild_escalation_snapshot"] + + if restart_all: + self.stdout.write("Processing restart escalation for all active alert groups...") + alert_groups = AlertGroup.objects.filter_active() + elif alert_group_ids: + self.stdout.write(f"Processing restart escalation for alert groups with ids: {alert_group_ids}...") + alert_groups = AlertGroup.objects.filter( + pk__in=alert_group_ids, + raw_escalation_snapshot__isnull=False, + ) + else: + self.stdout.write(f"Processing restart escalation for alert groups with ppks: {alert_group_ppk}...") + alert_groups = AlertGroup.objects.filter( + public_primary_key__in=alert_group_ppk, + raw_escalation_snapshot__isnull=False, + ) + + if not alert_groups: + self.stdout.write("No escalations to restart.") + return + + tasks = [] + alert_groups_to_update = [] + now = timezone.now() + + for alert_group in alert_groups: + # rebuild escalation snapshot with keeping information about current escalation step + # this is used for migrated organizations to actualize data in escalation snapshot + if rebuild_escalation_snapshot: + self._write_stdout_log( + restart_all, + f"rebuild escalation snapshot for alert group (id: {alert_group.id}, ppk: " + f"{alert_group.public_primary_key})", + ) + original_escalation_snapshot = alert_group.raw_escalation_snapshot + new_escalation_snapshot = alert_group.build_raw_escalation_snapshot() + snapshot_fields_to_copy = ["last_active_escalation_policy_order", "next_step_eta", "pause_escalation"] + for field in snapshot_fields_to_copy: + new_escalation_snapshot[field] = original_escalation_snapshot[field] + alert_group.raw_escalation_snapshot = new_escalation_snapshot + + task_id = celery_uuid() + # if incident was silenced, start unsilence_task + if alert_group.is_silenced_for_period: + if not restart_unsilence_task: + self._write_stdout_log( + restart_all, + f"alert group (id: {alert_group.id}, ppk: {alert_group.public_primary_key}) is silenced, skip", + ) + continue + self._write_stdout_log( + restart_all, + f"alert group (id: {alert_group.id}, ppk: {alert_group.public_primary_key}) is silenced, " + f"scheduling unsilence task", + ) + alert_group.unsilence_task_uuid = task_id + + escalation_start_time = max(now, alert_group.silenced_until) + alert_groups_to_update.append(alert_group) + + tasks.append( + unsilence_task.signature( + args=(alert_group.pk,), + immutable=True, + task_id=task_id, + eta=escalation_start_time, + ) + ) + # otherwise start escalate_alert_group task + elif alert_group.escalation_snapshot: + self._write_stdout_log( + restart_all, + f"Run escalation for alert group (id: {alert_group.id}, ppk: {alert_group.public_primary_key})", + ) + alert_group.active_escalation_id = task_id + alert_groups_to_update.append(alert_group) + + tasks.append( + escalate_alert_group.signature( + args=(alert_group.pk,), + immutable=True, + task_id=task_id, + eta=alert_group.next_step_eta, + ) + ) + else: + self._write_stdout_log( + restart_all, + f"alert group (id: {alert_group.id}, ppk: {alert_group.public_primary_key}) doesn't have escalation" + f" snapshot, skip", + ) + + AlertGroup.objects.bulk_update( + alert_groups_to_update, + ["active_escalation_id", "unsilence_task_uuid", "raw_escalation_snapshot"], + batch_size=5000, + ) + + for task in tasks: + task.apply_async() + + restarted_alert_group_ids = ", ".join( + f"(id: {str(alert_group.pk)}, ppk: {alert_group.public_primary_key})" for alert_group in alert_groups + ) + self.stdout.write(f"Escalations restarted for alert groups: {restarted_alert_group_ids}") + + def _write_stdout_log(self, restart_all, text): + """Write log if restart escalation not for all alert groups""" + if not restart_all: + self.stdout.write(text) diff --git a/engine/engine/tests/test_views.py b/engine/engine/tests/test_views.py index d8e519b0..b7332742 100644 --- a/engine/engine/tests/test_views.py +++ b/engine/engine/tests/test_views.py @@ -15,7 +15,7 @@ def test_detached_integrations_startupprobe_populates_integrations_cache(): response = client.get("/startupprobe/") assert response.status_code == status.HTTP_200_OK - mock_update_cache.assert_called_once + mock_update_cache.assert_called_once() def test_startupprobe_populates_integrations_cache(): @@ -27,4 +27,4 @@ def test_startupprobe_populates_integrations_cache(): response = client.get("/startupprobe/") assert response.status_code == status.HTTP_200_OK - mock_update_cache.assert_called_once + mock_update_cache.assert_called_once() diff --git a/engine/settings/base.py b/engine/settings/base.py index cd58cb26..bbc14bb6 100644 --- a/engine/settings/base.py +++ b/engine/settings/base.py @@ -120,6 +120,7 @@ DATABASE_DEFAULTS = { }, } +DATABASE_TYPES = DatabaseTypes DATABASE_NAME = os.getenv("DATABASE_NAME") or os.getenv("MYSQL_DB_NAME") DATABASE_USER = os.getenv("DATABASE_USER") or os.getenv("MYSQL_USER") DATABASE_PASSWORD = os.getenv("DATABASE_PASSWORD") or os.getenv("MYSQL_PASSWORD") diff --git a/grafana-plugin/e2e-tests/escalationChains/escalationPolicy.test.ts b/grafana-plugin/e2e-tests/escalationChains/escalationPolicy.test.ts new file mode 100644 index 00000000..44679b11 --- /dev/null +++ b/grafana-plugin/e2e-tests/escalationChains/escalationPolicy.test.ts @@ -0,0 +1,19 @@ +import {expect, test} from "../fixtures"; +import {generateRandomValue} from "../utils/forms"; +import {createEscalationChain, EscalationStep, selectEscalationStepValue} from "../utils/escalationChain"; + +test('escalation policy does not go back to "Default" after adding users to notify', async ({ adminRolePage }) => { + const { page, userName } = adminRolePage; + const escalationChainName = generateRandomValue(); + + // create important escalation step + await createEscalationChain(page, escalationChainName, EscalationStep.NotifyUsers, null, true); + // add user to notify + await selectEscalationStepValue(page, EscalationStep.NotifyUsers, userName); + + // reload and check if important is still selected + await page.reload(); + await page.waitForLoadState('networkidle'); + + expect(await page.locator('text=Important').isVisible()).toBe(true); +}); diff --git a/grafana-plugin/e2e-tests/utils/escalationChain.ts b/grafana-plugin/e2e-tests/utils/escalationChain.ts index a0f908e4..c24c5afb 100644 --- a/grafana-plugin/e2e-tests/utils/escalationChain.ts +++ b/grafana-plugin/e2e-tests/utils/escalationChain.ts @@ -17,7 +17,8 @@ export const createEscalationChain = async ( page: Page, escalationChainName: string, escalationStep?: EscalationStep, - escalationStepValue?: string + escalationStepValue?: string, + important?: boolean ): Promise => { // go to the escalation chains page await goToOnCallPage(page, 'escalations'); @@ -40,18 +41,32 @@ export const createEscalationChain = async ( await clickButton({ page, buttonText: 'Create' }); await expect(page.getByTestId('escalation-chain-name')).toHaveText(escalationChainName); - if (!escalationStep || !escalationStepValue) { - return; + if (escalationStep) { + // add an escalation step + await selectDropdownValue({ + page, selectType: 'grafanaSelect', placeholderText: 'Add escalation step...', value: escalationStep, + }); + + // toggle important + if (important) { + await selectDropdownValue({ + page, + selectType: 'grafanaSelect', + placeholderText: "Default", + value: "Important", + }); + } + + // select the escalation step value (e.g. user or schedule) + if (escalationStepValue) {await selectEscalationStepValue(page, escalationStep, escalationStepValue);} } +}; - // add an escalation step - await selectDropdownValue({ - page, - selectType: 'grafanaSelect', - placeholderText: 'Add escalation step...', - value: escalationStep, - }); - +export const selectEscalationStepValue = async ( + page: Page, + escalationStep: EscalationStep, + escalationStepValue: string +): Promise => { await selectDropdownValue({ page, selectType: 'grafanaSelect', diff --git a/grafana-plugin/src/containers/MobileAppConnection/MobileAppConnection.tsx b/grafana-plugin/src/containers/MobileAppConnection/MobileAppConnection.tsx index 1a293c5b..4e87938f 100644 --- a/grafana-plugin/src/containers/MobileAppConnection/MobileAppConnection.tsx +++ b/grafana-plugin/src/containers/MobileAppConnection/MobileAppConnection.tsx @@ -45,15 +45,15 @@ const MobileAppConnection = observer(({ userPk }: Props) => { return ( - Please connect Cloud OnCall to use the mobile app + Please connect Grafana Cloud OnCall to use the mobile app diff --git a/grafana-plugin/src/containers/MobileAppConnection/__snapshots__/MobileAppConnection.test.tsx.snap b/grafana-plugin/src/containers/MobileAppConnection/__snapshots__/MobileAppConnection.test.tsx.snap index b583c4b8..1f46a2a3 100644 --- a/grafana-plugin/src/containers/MobileAppConnection/__snapshots__/MobileAppConnection.test.tsx.snap +++ b/grafana-plugin/src/containers/MobileAppConnection/__snapshots__/MobileAppConnection.test.tsx.snap @@ -2901,7 +2901,7 @@ exports[`MobileAppConnection it shows a warning when cloud is not connected 1`] - Please connect Cloud OnCall to use the mobile app + Please connect Grafana Cloud OnCall to use the mobile app
- Connect Cloud OnCall + Connect Grafana Cloud OnCall diff --git a/grafana-plugin/src/containers/UserSettings/parts/connectors/PhoneConnector.tsx b/grafana-plugin/src/containers/UserSettings/parts/connectors/PhoneConnector.tsx index 434a9ba9..aa35f74e 100644 --- a/grafana-plugin/src/containers/UserSettings/parts/connectors/PhoneConnector.tsx +++ b/grafana-plugin/src/containers/UserSettings/parts/connectors/PhoneConnector.tsx @@ -35,11 +35,11 @@ const PhoneConnector = (props: PhoneConnectorProps) => { - + - + ); @@ -49,7 +49,7 @@ const PhoneConnector = (props: PhoneConnectorProps) => { @@ -63,11 +63,11 @@ const PhoneConnector = (props: PhoneConnectorProps) => { - + ); case 3: @@ -76,7 +76,7 @@ const PhoneConnector = (props: PhoneConnectorProps) => { @@ -90,7 +90,7 @@ const PhoneConnector = (props: PhoneConnectorProps) => { label="Phone" disabled={true} labelWidth={12} - tooltip={'OnCall uses Grafana Cloud for SMS and phone call notifications'} + tooltip={'OnCall uses Grafana Cloud OnCall for SMS and phone call notifications'} > diff --git a/grafana-plugin/src/containers/UserSettings/parts/tabs/CloudPhoneSettings/CloudPhoneSettings.tsx b/grafana-plugin/src/containers/UserSettings/parts/tabs/CloudPhoneSettings/CloudPhoneSettings.tsx index 29809d02..405582f6 100644 --- a/grafana-plugin/src/containers/UserSettings/parts/tabs/CloudPhoneSettings/CloudPhoneSettings.tsx +++ b/grafana-plugin/src/containers/UserSettings/parts/tabs/CloudPhoneSettings/CloudPhoneSettings.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useState } from 'react'; -import { Button, VerticalGroup, LoadingPlaceholder } from '@grafana/ui'; +import { Button, LoadingPlaceholder, VerticalGroup } from '@grafana/ui'; import { observer } from 'mobx-react'; import PluginLink from 'components/PluginLink/PluginLink'; @@ -53,10 +53,10 @@ const CloudPhoneSettings = observer((props: CloudPhoneSettingsProps) => { case 0: return ( - Cloud notifications enabled, but Grafana Cloud instance is not connected. + Cloud notifications enabled, but Grafana Cloud OnCall instance is not connected. @@ -65,11 +65,11 @@ const CloudPhoneSettings = observer((props: CloudPhoneSettingsProps) => { return ( - We can’t find a matching account in the connected Grafana Cloud instance (matching by e-mail + We can’t find a matching account in the connected Grafana Cloud OnCall instance (matching by e-mail {email && ': ' + email}). ); @@ -77,10 +77,10 @@ const CloudPhoneSettings = observer((props: CloudPhoneSettingsProps) => { return ( - Your account successfully matched with the Grafana Cloud account. Please verify your phone number.{' '} + Your account successfully matched with the Grafana Cloud OnCall account. Please verify your phone number.{' '} ); @@ -88,10 +88,10 @@ const CloudPhoneSettings = observer((props: CloudPhoneSettingsProps) => { return ( - Your account successfully matched with the Grafana Cloud account. Your phone number is verified.{' '} + Your account successfully matched with the Grafana Cloud OnCall account. Your phone number is verified.{' '} ); @@ -99,11 +99,11 @@ const CloudPhoneSettings = observer((props: CloudPhoneSettingsProps) => { return ( - We can’t find a matching account in the connected Grafana Cloud instance (matching by e-mail + We can’t find a matching account in the connected Grafana Cloud OnCall instance (matching by e-mail {email && ': ' + email}). ); @@ -113,10 +113,10 @@ const CloudPhoneSettings = observer((props: CloudPhoneSettingsProps) => { return ( - OnCall uses Grafana Cloud for SMS and phone call notifications + OnCall uses Grafana Cloud OnCall for SMS and phone call notifications {syncing ? (