From 8539514d854253e1c6f31bbb0ced66ea9894725a Mon Sep 17 00:00:00 2001 From: Michael Derynck Date: Mon, 3 Jun 2024 14:24:31 -0600 Subject: [PATCH 1/7] Add twilio flow templates for docs (#4380) Adding JSON for Twilio flows to go with upcoming docs and blog post --- tools/twilio/basic_flow.json | 198 +++++++++++++++ tools/twilio/flow_with_routes.json | 386 +++++++++++++++++++++++++++++ 2 files changed, 584 insertions(+) create mode 100644 tools/twilio/basic_flow.json create mode 100644 tools/twilio/flow_with_routes.json diff --git a/tools/twilio/basic_flow.json b/tools/twilio/basic_flow.json new file mode 100644 index 0000000000..b168d8e2ee --- /dev/null +++ b/tools/twilio/basic_flow.json @@ -0,0 +1,198 @@ +{ + "description": "Basic SMS and Call escalation", + "states": [ + { + "name": "Trigger", + "type": "trigger", + "transitions": [ + { + "next": "send_alert_from_sms", + "event": "incomingMessage" + }, + { + "next": "describe_alert_from_call", + "event": "incomingCall" + }, + { + "event": "incomingConversationMessage" + }, + { + "event": "incomingRequest" + }, + { + "event": "incomingParent" + } + ], + "properties": { + "offset": { + "x": 0, + "y": 0 + } + } + }, + { + "name": "send_alert_from_sms", + "type": "make-http-request", + "transitions": [ + { + "next": "send_alert_from_sms_success", + "event": "success" + }, + { + "next": "send_alert_from_sms_fail", + "event": "failed" + } + ], + "properties": { + "offset": { + "x": -180, + "y": 250 + }, + "method": "POST", + "content_type": "application/json;charset=utf-8", + "body": "{\"from\":\"{{trigger.message.From}}\",\"message\":\"{{trigger.message.Body}}\"}", + "url": "" + } + }, + { + "name": "send_alert_from_sms_success", + "type": "send-message", + "transitions": [ + { + "event": "sent" + }, + { + "event": "failed" + } + ], + "properties": { + "offset": { + "x": -410, + "y": 590 + }, + "service": "{{trigger.message.InstanceSid}}", + "channel": "{{trigger.message.ChannelSid}}", + "from": "{{flow.channel.address}}", + "message_type": "custom", + "to": "{{contact.channel.address}}", + "body": "Alert sent successfully" + } + }, + { + "name": "send_alert_from_sms_fail", + "type": "send-message", + "transitions": [ + { + "event": "sent" + }, + { + "event": "failed" + } + ], + "properties": { + "offset": { + "x": -60, + "y": 590 + }, + "service": "{{trigger.message.InstanceSid}}", + "channel": "{{trigger.message.ChannelSid}}", + "from": "{{flow.channel.address}}", + "message_type": "custom", + "to": "{{contact.channel.address}}", + "body": "Failed to send alert: Status({{widgets.send_escalation.status_code}})" + } + }, + { + "name": "describe_alert_from_call", + "type": "gather-input-on-call", + "transitions": [ + { + "event": "keypress" + }, + { + "next": "send_alert_from_call", + "event": "speech" + }, + { + "event": "timeout" + } + ], + "properties": { + "speech_timeout": "auto", + "offset": { + "x": 350, + "y": 240 + }, + "loop": 1, + "finish_on_key": "#", + "say": "Describe the alert to send. Press pound when finished.", + "stop_gather": true, + "gather_language": "en", + "profanity_filter": "true", + "timeout": 60 + } + }, + { + "name": "send_alert_from_call", + "type": "make-http-request", + "transitions": [ + { + "next": "send_alert_from_call_success", + "event": "success" + }, + { + "next": "send_alert_from_call_fail", + "event": "failed" + } + ], + "properties": { + "offset": { + "x": 360, + "y": 590 + }, + "method": "POST", + "content_type": "application/json;charset=utf-8", + "body": "{\"from\":\"{{trigger.call.From}}\", \"message\":\"{{widgets.describe_alert_from_call.SpeechResult}} \"}", + "url": "" + } + }, + { + "name": "send_alert_from_call_success", + "type": "say-play", + "transitions": [ + { + "event": "audioComplete" + } + ], + "properties": { + "offset": { + "x": 90, + "y": 900 + }, + "loop": 1, + "say": "Alert sent successfully" + } + }, + { + "name": "send_alert_from_call_fail", + "type": "say-play", + "transitions": [ + { + "event": "audioComplete" + } + ], + "properties": { + "offset": { + "x": 520, + "y": 900 + }, + "loop": 1, + "say": "Failed to send alert: Status ({{widgets.send_alert_from_call.status_code}})" + } + } + ], + "initial_state": "Trigger", + "flags": { + "allow_concurrent_calls": true + } +} diff --git a/tools/twilio/flow_with_routes.json b/tools/twilio/flow_with_routes.json new file mode 100644 index 0000000000..add3add40a --- /dev/null +++ b/tools/twilio/flow_with_routes.json @@ -0,0 +1,386 @@ +{ + "description": "Added Routes SMS and Call escalation", + "states": [ + { + "name": "Trigger", + "type": "trigger", + "transitions": [ + { + "next": "sms_select_target", + "event": "incomingMessage" + }, + { + "next": "call_select_target", + "event": "incomingCall" + }, + { + "event": "incomingConversationMessage" + }, + { + "event": "incomingRequest" + }, + { + "event": "incomingParent" + } + ], + "properties": { + "offset": { + "x": 80, + "y": -200 + } + } + }, + { + "name": "send_alert_from_sms", + "type": "make-http-request", + "transitions": [ + { + "next": "send_alert_from_sms_success", + "event": "success" + }, + { + "next": "send_alert_from_sms_fail", + "event": "failed" + } + ], + "properties": { + "offset": { + "x": -370, + "y": 500 + }, + "method": "POST", + "content_type": "application/json;charset=utf-8", + "body": "{\"from\":\"{{trigger.message.From}}\",\"message\":\"{{trigger.message.Body}}\",\"target\":\"{{widgets.sms_select_target.inbound.Body}}\"}", + "url": "" + } + }, + { + "name": "send_alert_from_sms_success", + "type": "send-message", + "transitions": [ + { + "event": "sent" + }, + { + "event": "failed" + } + ], + "properties": { + "offset": { + "x": -700, + "y": 780 + }, + "service": "{{trigger.message.InstanceSid}}", + "channel": "{{trigger.message.ChannelSid}}", + "from": "{{flow.channel.address}}", + "message_type": "custom", + "to": "{{contact.channel.address}}", + "body": "Alert sent successfully" + } + }, + { + "name": "send_alert_from_sms_fail", + "type": "send-message", + "transitions": [ + { + "event": "sent" + }, + { + "event": "failed" + } + ], + "properties": { + "offset": { + "x": -340, + "y": 780 + }, + "service": "{{trigger.message.InstanceSid}}", + "channel": "{{trigger.message.ChannelSid}}", + "from": "{{flow.channel.address}}", + "message_type": "custom", + "to": "{{contact.channel.address}}", + "body": "Failed to send alert: Status({{widgets.send_escalation.status_code}})" + } + }, + { + "name": "describe_alert_from_call", + "type": "gather-input-on-call", + "transitions": [ + { + "event": "keypress" + }, + { + "next": "send_alert_from_call", + "event": "speech" + }, + { + "event": "timeout" + } + ], + "properties": { + "speech_timeout": "auto", + "offset": { + "x": 350, + "y": 310 + }, + "loop": 1, + "finish_on_key": "#", + "say": "Describe the alert to send. Press pound when finished.", + "stop_gather": true, + "gather_language": "en", + "profanity_filter": "true", + "timeout": 60 + } + }, + { + "name": "send_alert_from_call", + "type": "make-http-request", + "transitions": [ + { + "next": "send_alert_from_call_success", + "event": "success" + }, + { + "next": "send_alert_from_call_fail", + "event": "failed" + } + ], + "properties": { + "offset": { + "x": 350, + "y": 580 + }, + "method": "POST", + "content_type": "application/json;charset=utf-8", + "body": "{\"from\":\"{{trigger.call.From}}\", \"message\":\"{{widgets.describe_alert_from_call.SpeechResult}} \",\"target\":\"{{widgets.call_set_target.target}}\"}", + "url": "" + } + }, + { + "name": "send_alert_from_call_success", + "type": "say-play", + "transitions": [ + { + "event": "audioComplete" + } + ], + "properties": { + "offset": { + "x": 200, + "y": 950 + }, + "loop": 1, + "say": "Alert sent successfully" + } + }, + { + "name": "send_alert_from_call_fail", + "type": "say-play", + "transitions": [ + { + "event": "audioComplete" + } + ], + "properties": { + "offset": { + "x": 630, + "y": 950 + }, + "loop": 1, + "say": "Failed to send alert: Status ({{widgets.send_alert_from_call.status_code}})" + } + }, + { + "name": "sms_select_target", + "type": "send-and-wait-for-reply", + "transitions": [ + { + "next": "sms_validate_target", + "event": "incomingMessage" + }, + { + "next": "sms_select_target_timeout", + "event": "timeout" + }, + { + "event": "deliveryFailure" + } + ], + "properties": { + "offset": { + "x": -330, + "y": -50 + }, + "service": "{{trigger.message.InstanceSid}}", + "channel": "{{trigger.message.ChannelSid}}", + "from": "{{flow.channel.address}}", + "message_type": "custom", + "body": "Which target do you want to send the alert to?\nabc \ndefault", + "timeout": "300" + } + }, + { + "name": "sms_select_target_timeout", + "type": "send-message", + "transitions": [ + { + "event": "sent" + }, + { + "event": "failed" + } + ], + "properties": { + "offset": { + "x": -80, + "y": 210 + }, + "service": "{{trigger.message.InstanceSid}}", + "channel": "{{trigger.message.ChannelSid}}", + "from": "{{flow.channel.address}}", + "message_type": "custom", + "to": "{{contact.channel.address}}", + "body": "Target select timed out, send the alert again to start over." + } + }, + { + "name": "sms_validate_target", + "type": "split-based-on", + "transitions": [ + { + "next": "sms_validate_target_fail", + "event": "noMatch" + }, + { + "next": "send_alert_from_sms", + "event": "match", + "conditions": [ + { + "friendly_name": "If value equal_to abc", + "arguments": ["{{widgets.sms_select_target.inbound.Body}}"], + "type": "matches_any_of", + "value": "abc,default" + } + ] + } + ], + "properties": { + "input": "{{widgets.sms_select_target.inbound.Body}}", + "offset": { + "x": -590, + "y": 210 + } + } + }, + { + "name": "sms_validate_target_fail", + "type": "send-message", + "transitions": [ + { + "event": "sent" + }, + { + "event": "failed" + } + ], + "properties": { + "offset": { + "x": -700, + "y": 500 + }, + "service": "{{trigger.message.InstanceSid}}", + "channel": "{{trigger.message.ChannelSid}}", + "from": "{{flow.channel.address}}", + "message_type": "custom", + "to": "{{contact.channel.address}}", + "body": "{{widgets.sms_select_target.inbound.Body}} is not a valid target." + } + }, + { + "name": "call_select_target", + "type": "gather-input-on-call", + "transitions": [ + { + "next": "call_select_validate", + "event": "keypress" + }, + { + "event": "speech" + }, + { + "event": "timeout" + } + ], + "properties": { + "number_of_digits": 1, + "speech_timeout": "auto", + "offset": { + "x": 350, + "y": 50 + }, + "loop": 1, + "finish_on_key": "#", + "say": "Which target do you want to send to? Press 1 for ABC. \nPress 2 for default.", + "stop_gather": true, + "gather_language": "en", + "profanity_filter": "true", + "timeout": 5 + } + }, + { + "name": "call_select_validate", + "type": "split-based-on", + "transitions": [ + { + "next": "call_select_target", + "event": "noMatch" + }, + { + "next": "call_set_target", + "event": "match", + "conditions": [ + { + "friendly_name": "If value matches_any_of 1,2", + "arguments": ["{{widgets.call_select_target.Digits}}"], + "type": "matches_any_of", + "value": "1,2" + } + ] + } + ], + "properties": { + "input": "{{widgets.call_select_target.Digits}}", + "offset": { + "x": 760, + "y": 50 + } + } + }, + { + "name": "call_set_target", + "type": "set-variables", + "transitions": [ + { + "next": "describe_alert_from_call", + "event": "next" + } + ], + "properties": { + "variables": [ + { + "value": "{% if widgets.call_select_target.Digits == \"1\" %}abc{% elsif widgets.call_select_target.Digits == \"2\" %}default{% endif %}", + "key": "target" + } + ], + "offset": { + "x": 760, + "y": 300 + } + } + } + ], + "initial_state": "Trigger", + "flags": { + "allow_concurrent_calls": true + } +} From d0dd15453e192755e9d858b179da92aa71686e71 Mon Sep 17 00:00:00 2001 From: Andrey Oleynik Date: Tue, 4 Jun 2024 08:34:57 +0300 Subject: [PATCH 2/7] change zvonok call verification (#4393) # Change zvonok call verification After May 27, the Zvonok service will block number verification that was not set up as part of the phone number verification campaign. This PR modifies the number verification process. ## Checklist - [x] Unit, integration, and e2e (if applicable) tests updated - [x] Documentation added (or `pr:no public docs` PR label added if not required) - [x] Added the relevant release notes label (see labels prefixed w/ `release:`). These labels dictate how your PR will show up in the autogenerated release notes. Co-authored-by: Innokentii Konstantinov --- docs/sources/set-up/open-source/index.md | 3 +- engine/apps/base/models/live_setting.py | 4 +- engine/apps/zvonok/phone_provider.py | 40 +++++++-------- .../apps/zvonok/tests/test_zvonok_provider.py | 50 ++++++++----------- engine/settings/base.py | 2 +- 5 files changed, 42 insertions(+), 57 deletions(-) diff --git a/docs/sources/set-up/open-source/index.md b/docs/sources/set-up/open-source/index.md index b925c7b38e..734ec8fc59 100644 --- a/docs/sources/set-up/open-source/index.md +++ b/docs/sources/set-up/open-source/index.md @@ -234,8 +234,7 @@ Zvonok.com, complete the following steps: to the variable `ZVONOK_AUDIO_ID` (optional step). 6. To make a call with a specific voice, you can set the `ZVONOK_SPEAKER_ID`. By default, the ID used is `Salli` (optional step). -7. To change the voice message for phone verification, you can set the variable `ZVONOK_VERIFICATION_TEMPLATE` - with the following format (optional step): `Your verification code is $verification_code, have a nice day.`. +7. Create phone number verification campaign with type `tellcode` and assign its ID value to `ZVONOK_VERIFICATION_CAMPAIGN_ID`. 8. To process the call status, it is required to add a postback with the GET/POST method on the side of the zvonok.com service with the following format (optional step): `${ONCALL_BASE_URL}/zvonok/call_status_events?campaign_id={ct_campaign_id}&call_id={ct_call_id}&status={ct_status}&user_choice={ct_user_choice}` diff --git a/engine/apps/base/models/live_setting.py b/engine/apps/base/models/live_setting.py index f6e8b7e553..7c9d39b060 100644 --- a/engine/apps/base/models/live_setting.py +++ b/engine/apps/base/models/live_setting.py @@ -71,7 +71,7 @@ class LiveSetting(models.Model): "ZVONOK_POSTBACK_STATUS", "ZVONOK_POSTBACK_USER_CHOICE", "ZVONOK_POSTBACK_USER_CHOICE_ACK", - "ZVONOK_VERIFICATION_TEMPLATE", + "ZVONOK_VERIFICATION_CAMPAIGN_ID", ) DESCRIPTIONS = { @@ -170,7 +170,7 @@ class LiveSetting(models.Model): "ZVONOK_POSTBACK_STATUS": "'Postback' status (ct_status) query parameter name to validate a postback request.", "ZVONOK_POSTBACK_USER_CHOICE": "'Postback' user choice (ct_user_choice) query parameter name (optional).", "ZVONOK_POSTBACK_USER_CHOICE_ACK": "'Postback' user choice (ct_user_choice) query parameter value for acknowledge alert group (optional).", - "ZVONOK_VERIFICATION_TEMPLATE": "The message template used for phone number verification (optional).", + "ZVONOK_VERIFICATION_CAMPAIGN_ID": "The phone number verification campaign ID. You can get it after verification campaign creation.", } SECRET_SETTING_NAMES = ( diff --git a/engine/apps/zvonok/phone_provider.py b/engine/apps/zvonok/phone_provider.py index e19e771f24..4af75b6b3f 100644 --- a/engine/apps/zvonok/phone_provider.py +++ b/engine/apps/zvonok/phone_provider.py @@ -1,6 +1,5 @@ import logging from random import randint -from string import Template from typing import Optional import requests @@ -12,6 +11,7 @@ from apps.zvonok.models.phone_call import ZvonokCallStatuses, ZvonokPhoneCall ZVONOK_CALL_URL = "https://zvonok.com/manager/cabapi_external/api/v1/phones/call/" +ZVONOK_VERIFICATION_CALL_URL = "https://zvonok.com/manager/cabapi_external/api/v1/phones/tellcode/" logger = logging.getLogger(__name__) @@ -96,6 +96,15 @@ def _call_create(self, number: str, text: str, speaker: Optional[str] = None): return requests.post(ZVONOK_CALL_URL, params=params) + def _verification_call_create(self, number: str, code: int): + params = { + "public_key": live_settings.ZVONOK_API_KEY, + "campaign_id": live_settings.ZVONOK_VERIFICATION_CAMPAIGN_ID, + "phone": number, + "pincode": code, + } + return requests.post(ZVONOK_VERIFICATION_CALL_URL, params=params) + def _get_graceful_msg(self, body, number): if body: status = body.get("status") @@ -105,34 +114,19 @@ def _get_graceful_msg(self, body, number): return f"Failed make call to {number}" def make_verification_call(self, number: str): + body = None code = self._generate_verification_code() cache.set(self._cache_key(number), code, timeout=10 * 60) - codewspaces = " ".join(code) - - body = None - speaker = live_settings.ZVONOK_SPEAKER_ID - if live_settings.ZVONOK_VERIFICATION_TEMPLATE: - message = Template(live_settings.ZVONOK_VERIFICATION_TEMPLATE).safe_substitute( - verification_code=codewspaces + if not live_settings.ZVONOK_VERIFICATION_CAMPAIGN_ID: + raise FailedToStartVerification( + graceful_msg="Failed make verification call, verification campaign id not set." ) - else: - message = f"Your verification code is {codewspaces}" + try: - response = self._call_create( - number, - message, - speaker, - ) - response.raise_for_status() + response = self._verification_call_create(number, code) body = response.json() - if not body: - logger.error("ZvonokPhoneProvider.make_verification_call: failed, empty body") - raise FailedToMakeCall(graceful_msg=f"Failed make verification call to {number}, empty body") - - call_id = body.get("call_id") - if not call_id: - raise FailedToStartVerification(graceful_msg=self._get_graceful_msg(body, number)) + response.raise_for_status() except requests.exceptions.HTTPError as http_err: logger.error(f"ZvonokPhoneProvider.make_verification_call: failed {http_err}") raise FailedToStartVerification(graceful_msg=self._get_graceful_msg(body, number)) diff --git a/engine/apps/zvonok/tests/test_zvonok_provider.py b/engine/apps/zvonok/tests/test_zvonok_provider.py index 04eab9dac4..5e25045fa8 100644 --- a/engine/apps/zvonok/tests/test_zvonok_provider.py +++ b/engine/apps/zvonok/tests/test_zvonok_provider.py @@ -3,6 +3,7 @@ import pytest from django.test import override_settings +from apps.phone_notifications.exceptions import FailedToStartVerification from apps.zvonok.phone_provider import ZvonokPhoneProvider @@ -12,46 +13,37 @@ def provider(): @pytest.mark.django_db -def test_make_verification_call_with_template_set(provider): - verification_code = "123456" +def test_make_verification_call(provider): + verification_code = "123456789" number = "1234567890" - speaker_id = "Salli" - template_value = 'Your code is $verification_code' - excepted_message = 'Your code is 1 2 3 4 5 6' - - with override_settings(ZVONOK_VERIFICATION_TEMPLATE=template_value, ZVONOK_SPEAKER_ID=speaker_id): + campaign_id = "123456" + with override_settings(ZVONOK_VERIFICATION_CAMPAIGN_ID=campaign_id): with patch("django.core.cache.cache.set"): - provider._call_create = MagicMock(return_value=MagicMock(json=lambda: {"call_id": "12345"})) + provider._verification_call_create = MagicMock(return_value=MagicMock(json=lambda: {"status": "ok"})) provider._generate_verification_code = MagicMock(return_value=verification_code) provider.make_verification_call(number) - provider._call_create.assert_called_once_with(number, excepted_message, speaker_id) + provider._verification_call_create.assert_called_once_with(number, verification_code) @pytest.mark.django_db -def test_make_verification_call_with_invalid_template_set(provider): - verification_code = "123456" +def test_make_verification_call_without_campaign_id(provider): number = "1234567890" - speaker_id = "Salli" - template_value = "Your code is" - excepted_message = "Your code is" - - with override_settings(ZVONOK_VERIFICATION_TEMPLATE=template_value, ZVONOK_SPEAKER_ID=speaker_id): - with patch("django.core.cache.cache.set"): - provider._call_create = MagicMock(return_value=MagicMock(json=lambda: {"call_id": "12345"})) - provider._generate_verification_code = MagicMock(return_value=verification_code) + with patch("django.core.cache.cache.set"): + with pytest.raises(FailedToStartVerification): provider.make_verification_call(number) - provider._call_create.assert_called_once_with(number, excepted_message, speaker_id) @pytest.mark.django_db -def test_make_verification_call_without_template_set(provider): - verification_code = "123456" +def test_make_verification_call_with_error(provider): number = "1234567890" - speaker_id = "Salli" - excepted_message = "Your verification code is 1 2 3 4 5 6" - with override_settings(ZVONOK_SPEAKER_ID=speaker_id): + campaign_id = "123456" + + with override_settings(ZVONOK_VERIFICATION_CAMPAIGN_ID=campaign_id): with patch("django.core.cache.cache.set"): - provider._call_create = MagicMock(return_value=MagicMock(json=lambda: {"call_id": "12345"})) - provider._generate_verification_code = MagicMock(return_value=verification_code) - provider.make_verification_call(number) - provider._call_create.assert_called_once_with(number, excepted_message, speaker_id) + with pytest.raises(FailedToStartVerification): + provider._verification_call_create = MagicMock( + return_value=MagicMock( + json={"status": "error", "data": "Form isn't valid: * campaign_id\n * Invalid campaign type"} + ) + ) + provider.make_verification_call(number) diff --git a/engine/settings/base.py b/engine/settings/base.py index 48d3fbdda9..c528ba3ce4 100644 --- a/engine/settings/base.py +++ b/engine/settings/base.py @@ -912,7 +912,7 @@ class BrokerTypes: ZVONOK_POSTBACK_STATUS = os.getenv("ZVONOK_POSTBACK_STATUS", "status") ZVONOK_POSTBACK_USER_CHOICE = os.getenv("ZVONOK_POSTBACK_USER_CHOICE", None) ZVONOK_POSTBACK_USER_CHOICE_ACK = os.getenv("ZVONOK_POSTBACK_USER_CHOICE_ACK", None) -ZVONOK_VERIFICATION_TEMPLATE = os.getenv("ZVONOK_VERIFICATION_TEMPLATE", None) +ZVONOK_VERIFICATION_CAMPAIGN_ID = os.getenv("ZVONOK_VERIFICATION_CAMPAIGN_ID", None) DETACHED_INTEGRATIONS_SERVER = getenv_boolean("DETACHED_INTEGRATIONS_SERVER", default=False) From 2f1ffcf2ffbfdc85e24d3bb4951f7441264a6d22 Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Tue, 4 Jun 2024 11:16:25 +0300 Subject: [PATCH 3/7] Fix display of schedule final in preview mode (#4447) # What this PR does - Fixes the display of preview schedule when you click on a row Part of #4428 --- .../src/components/GTable/GTable.tsx | 37 ++++++++++--------- .../src/pages/schedules/Schedules.tsx | 1 + 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/grafana-plugin/src/components/GTable/GTable.tsx b/grafana-plugin/src/components/GTable/GTable.tsx index 8244464526..b251a4eee6 100644 --- a/grafana-plugin/src/components/GTable/GTable.tsx +++ b/grafana-plugin/src/components/GTable/GTable.tsx @@ -1,7 +1,6 @@ import React, { useCallback, useMemo, ChangeEvent, ReactElement } from 'react'; import { css, cx } from '@emotion/css'; -import { GrafanaTheme2 } from '@grafana/data'; import { Pagination, Checkbox, Icon, useStyles2 } from '@grafana/ui'; import Table from 'rc-table'; import { TableProps } from 'rc-table/lib/Table'; @@ -136,7 +135,7 @@ export const GTable = (props: }, [rowSelection, columnsProp, data]); return ( -
+
expandable={expandable} rowKey={rowKey} @@ -156,20 +155,24 @@ export const GTable = (props: ); }; -const getGTableStyles = (_theme: GrafanaTheme2) => { - return { - root: css` - table { - width: 100%; - } - `, +const getGTableStyles = () => ({ + root: css` + table { + width: 100%; + } + `, + + fixed: css` + table { + table-layout: fixed; + } + `, - pagination: css` - margin-top: 20px; - `, + pagination: css` + margin-top: 20px; + `, - checkbox: css` - display: inline-flex; - `, - }; -}; + checkbox: css` + display: inline-flex; + `, +}); diff --git a/grafana-plugin/src/pages/schedules/Schedules.tsx b/grafana-plugin/src/pages/schedules/Schedules.tsx index 7a16317d09..de49ee6dfd 100644 --- a/grafana-plugin/src/pages/schedules/Schedules.tsx +++ b/grafana-plugin/src/pages/schedules/Schedules.tsx @@ -115,6 +115,7 @@ class _SchedulesPage extends React.Component Date: Tue, 4 Jun 2024 14:51:02 +0300 Subject: [PATCH 4/7] conditionally enable searching for alert groups via env var (#4287) # What this PR does re-enables the search for alert groups ## Which issue(s) this PR closes Closes #2232 ## Checklist - [x] Unit, integration, and e2e (if applicable) tests updated - [x] Documentation added (or `pr:no public docs` PR label added if not required) - [x] Added the relevant release notes label (see labels prefixed w/ `release:`). These labels dictate how your PR will show up in the autogenerated release notes. --------- Co-authored-by: Joey Orlando Co-authored-by: Joey Orlando --- engine/apps/api/views/alert_group.py | 6 ++++++ engine/settings/base.py | 1 + grafana-plugin/src/models/alertgroup/alertgroup.ts | 2 +- 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/engine/apps/api/views/alert_group.py b/engine/apps/api/views/alert_group.py index f47f3ccfbf..96d49aded3 100644 --- a/engine/apps/api/views/alert_group.py +++ b/engine/apps/api/views/alert_group.py @@ -1,5 +1,6 @@ import typing +from django.conf import settings from django.core.exceptions import ObjectDoesNotExist from django.db.models import Count, Max, Q from django_filters import rest_framework as filters @@ -275,6 +276,11 @@ class AlertGroupView( pagination_class = AlertGroupCursorPaginator filter_backends = [SearchFilter, filters.DjangoFilterBackend] + search_fields = ( + ["=public_primary_key", "=inside_organization_number", "web_title_cache"] + if settings.FEATURE_ALERT_GROUP_SEARCH_ENABLED + else [] + ) filterset_class = AlertGroupFilter def get_serializer_class(self): diff --git a/engine/settings/base.py b/engine/settings/base.py index c528ba3ce4..eef9d16ee3 100644 --- a/engine/settings/base.py +++ b/engine/settings/base.py @@ -71,6 +71,7 @@ FEATURE_LABELS_ENABLED_FOR_ALL = getenv_boolean("FEATURE_LABELS_ENABLED_FOR_ALL", default=False) # Enable labels feature for organizations from the list. Use OnCall organization ID, for this flag FEATURE_LABELS_ENABLED_PER_ORG = getenv_list("FEATURE_LABELS_ENABLED_PER_ORG", default=list()) +FEATURE_ALERT_GROUP_SEARCH_ENABLED = getenv_boolean("FEATURE_ALERT_GROUP_SEARCH_ENABLED", default=False) TWILIO_API_KEY_SID = os.environ.get("TWILIO_API_KEY_SID") TWILIO_API_KEY_SECRET = os.environ.get("TWILIO_API_KEY_SECRET") diff --git a/grafana-plugin/src/models/alertgroup/alertgroup.ts b/grafana-plugin/src/models/alertgroup/alertgroup.ts index 8241213a50..f3c9397202 100644 --- a/grafana-plugin/src/models/alertgroup/alertgroup.ts +++ b/grafana-plugin/src/models/alertgroup/alertgroup.ts @@ -65,7 +65,7 @@ export class AlertGroupStore { params: { query: { ...incidentFilters, - search, + search: incidentFilters?.search || search, perpage: this.alertsSearchResult?.page_size, cursor: this.incidentsCursor, is_root: true, From 038d78df2a758f4478bdf9a297be6a9b57a47907 Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Tue, 4 Jun 2024 17:12:16 +0300 Subject: [PATCH 5/7] Show correct swap request dates (#4456) # What this PR does Part of https://github.com/grafana/oncall/issues/4428 Additionally it fixes - NPE within `SchedulePersonal` - changes the line-break to use `contrastText` instead of `main` (from grafana theme) --- .../containers/Rotations/SchedulePersonal.tsx | 17 +++++++---- .../src/pages/schedule/Schedule.tsx | 28 +++++++++++++------ grafana-plugin/src/styles/utils.styles.ts | 2 +- 3 files changed, 32 insertions(+), 15 deletions(-) diff --git a/grafana-plugin/src/containers/Rotations/SchedulePersonal.tsx b/grafana-plugin/src/containers/Rotations/SchedulePersonal.tsx index 9adb92d7ea..57145fc2a6 100644 --- a/grafana-plugin/src/containers/Rotations/SchedulePersonal.tsx +++ b/grafana-plugin/src/containers/Rotations/SchedulePersonal.tsx @@ -2,12 +2,13 @@ import React, { FC, useEffect } from 'react'; import { cx } from '@emotion/css'; import { GrafanaTheme2 } from '@grafana/data'; -import { Badge, Button, HorizontalGroup, Icon, useStyles2, withTheme2 } from '@grafana/ui'; +import { Badge, BadgeColor, Button, HorizontalGroup, Icon, useStyles2, withTheme2 } from '@grafana/ui'; import { observer } from 'mobx-react'; import { RouteComponentProps, withRouter } from 'react-router-dom'; import { CSSTransition, TransitionGroup } from 'react-transition-group'; import { Avatar } from 'components/Avatar/Avatar'; +import { RenderConditionally } from 'components/RenderConditionally/RenderConditionally'; import { Text } from 'components/Text/Text'; import { Rotation } from 'containers/Rotation/Rotation'; import { TimelineMarks } from 'containers/TimelineMarks/TimelineMarks'; @@ -102,14 +103,18 @@ const _SchedulePersonal: FC = observer(({ userPk, onSlotC
- - On-call schedule {storeUser.username} - + ( + + On-call schedule {storeUser.username} + + )} + /> {isOncall ? ( ) : ( - /* @ts-ignore */ - + )} diff --git a/grafana-plugin/src/pages/schedule/Schedule.tsx b/grafana-plugin/src/pages/schedule/Schedule.tsx index 703627adc7..80fb1bdb3a 100644 --- a/grafana-plugin/src/pages/schedule/Schedule.tsx +++ b/grafana-plugin/src/pages/schedule/Schedule.tsx @@ -598,23 +598,35 @@ class _SchedulePage extends React.Component { + handleShowShiftSwapForm = (id: ShiftSwap['id'] | 'new', swap?: { swap_start: string; swap_end: string }) => { + const { filters } = this.state; const { store, + store: { + userStore: { currentUserPk }, + timezoneStore: { currentDateInSelectedTimezone }, + }, match: { params: { id: scheduleId }, }, } = this.props; - const { - userStore: { currentUserPk }, - timezoneStore: { currentDateInSelectedTimezone }, - } = store; - - const layers = getLayersFromStore(store, scheduleId, store.timezoneStore.calendarStartDate); + if (swap) { + if (!filters.users.includes(currentUserPk)) { + this.setState({ filters: { ...filters, users: [...this.state.filters.users, currentUserPk] } }); + this.highlightMyShiftsWasToggled = true; + } - const { filters } = this.state; + return this.setState({ + shiftSwapIdToShowForm: id, + shiftSwapParamsToShowForm: { + swap_start: swap.swap_start, + swap_end: swap.swap_end, + }, + }); + } + const layers = getLayersFromStore(store, scheduleId, store.timezoneStore.calendarStartDate); const closestEvent = findClosestUserEvent(dayjs(), currentUserPk, layers); const swapStart = closestEvent ? dayjs(closestEvent.start) diff --git a/grafana-plugin/src/styles/utils.styles.ts b/grafana-plugin/src/styles/utils.styles.ts index 4731fb5748..13a1786af6 100644 --- a/grafana-plugin/src/styles/utils.styles.ts +++ b/grafana-plugin/src/styles/utils.styles.ts @@ -19,7 +19,7 @@ export const getUtilStyles = (theme: GrafanaTheme2) => { thinLineBreak: css` width: 100%; - border-top: 1px solid ${theme.colors.secondary.main}; + border-top: 1px solid ${theme.colors.secondary.contrastText}; margin-top: 8px; opacity: 15%; `, From b3a56cdffc42c81cf39a5081e1e6aa3363b86ea1 Mon Sep 17 00:00:00 2001 From: Vadim Stepanov Date: Tue, 4 Jun 2024 19:11:15 +0100 Subject: [PATCH 6/7] Reduce size of payload on `/escalate` Slack command (#4458) --- engine/apps/slack/scenarios/paging.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/engine/apps/slack/scenarios/paging.py b/engine/apps/slack/scenarios/paging.py index ecce5b5d76..22532dc0f9 100644 --- a/engine/apps/slack/scenarios/paging.py +++ b/engine/apps/slack/scenarios/paging.py @@ -681,7 +681,7 @@ def _create_user_option_groups( "text": f"{user.name or user.username}", "emoji": True, }, - "value": make_value({"id": user.pk}, organization), + "value": json.dumps({"id": user.pk}), } for user in users ] From f40634a6eb5176a26fe945440439b362149612fa Mon Sep 17 00:00:00 2001 From: Vadim Stepanov Date: Tue, 4 Jun 2024 20:06:59 +0100 Subject: [PATCH 7/7] only validate existing settings (#4459) # What this PR does Only validate live settings in `AVAILABLE_NAMES` to avoid issues when a live setting is renamed (one of the settings was renamed in https://github.com/grafana/oncall/pull/4287 earlier) ## Checklist - [x] Unit, integration, and e2e (if applicable) tests updated - [x] Documentation added (or `pr:no public docs` PR label added if not required) - [x] Added the relevant release notes label (see labels prefixed w/ `release:`). These labels dictate how your PR will show up in the autogenerated release notes. --- engine/apps/base/models/live_setting.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/engine/apps/base/models/live_setting.py b/engine/apps/base/models/live_setting.py index 7c9d39b060..e4c4a2ab9a 100644 --- a/engine/apps/base/models/live_setting.py +++ b/engine/apps/base/models/live_setting.py @@ -240,7 +240,7 @@ def populate_settings_if_needed(cls): @classmethod def validate_settings(cls): - settings_to_validate = cls.objects.all() + settings_to_validate = cls.objects.filter(name__in=cls.AVAILABLE_NAMES) for setting in settings_to_validate: setting.error = LiveSettingValidator(live_setting=setting).get_error() setting.save(update_fields=["error"])