v1.2.25 (#1965)
Co-authored-by: Roman Pertl <533172+roock@users.noreply.github.com> Co-authored-by: Joey Orlando <joseph.t.orlando@gmail.com> Co-authored-by: Vadim Stepanov <vadimkerr@gmail.com> Co-authored-by: Rares Mardare <rares.mardare@grafana.com>
This commit is contained in:
parent
a155be2ca0
commit
d67ac8eca3
22 changed files with 3980 additions and 3415 deletions
10
.drone.yml
10
.drone.yml
|
|
@ -5,7 +5,7 @@ name: Build and Release
|
|||
|
||||
steps:
|
||||
- name: Build Plugin
|
||||
image: node:14.17.0-buster
|
||||
image: node:18.16.0-buster
|
||||
commands:
|
||||
- apt-get update
|
||||
- apt-get --assume-yes install jq
|
||||
|
|
@ -16,7 +16,7 @@ steps:
|
|||
- ls ./
|
||||
|
||||
- name: Sign and Package Plugin
|
||||
image: node:14.17.0-buster
|
||||
image: node:18.16.0-buster
|
||||
environment:
|
||||
GRAFANA_API_KEY:
|
||||
from_secret: gcom_plugin_publisher_api_key
|
||||
|
|
@ -174,7 +174,7 @@ name: OSS plugin release
|
|||
|
||||
steps:
|
||||
- name: build plugin
|
||||
image: node:14.17.0-buster
|
||||
image: node:18.16.0-buster
|
||||
commands:
|
||||
- apt-get update
|
||||
- apt-get --assume-yes install jq
|
||||
|
|
@ -185,7 +185,7 @@ steps:
|
|||
- ls ./
|
||||
|
||||
- name: sign and package plugin
|
||||
image: node:14.17.0-buster
|
||||
image: node:18.16.0-buster
|
||||
environment:
|
||||
GRAFANA_API_KEY:
|
||||
from_secret: gcom_plugin_publisher_api_key
|
||||
|
|
@ -418,4 +418,4 @@ kind: secret
|
|||
name: drone_token
|
||||
---
|
||||
kind: signature
|
||||
hmac: 8f34bbbb5a2efe479b40d616b087a0c380390de2e440857bdffe8fd48d860e55
|
||||
hmac: 3ef960dd2f2a121d795f7fe0b5447f7695bf6cd8f6fe552cd4dbab15d0acc9ee
|
||||
|
|
|
|||
6
.github/workflows/linting-and-tests.yml
vendored
6
.github/workflows/linting-and-tests.yml
vendored
|
|
@ -32,7 +32,7 @@ jobs:
|
|||
# following 2 steps - need to install the frontend dependencies for the eslint/prettier/stylelint steps
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 14.17.0
|
||||
node-version: 18.16.0
|
||||
cache: "yarn"
|
||||
cache-dependency-path: grafana-plugin/yarn.lock
|
||||
- name: Use cached frontend dependencies
|
||||
|
|
@ -54,7 +54,7 @@ jobs:
|
|||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 14.17.0
|
||||
node-version: 18.16.0
|
||||
cache: "yarn"
|
||||
cache-dependency-path: grafana-plugin/yarn.lock
|
||||
- name: Use cached frontend dependencies
|
||||
|
|
@ -285,7 +285,7 @@ jobs:
|
|||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 14.17.0
|
||||
node-version: 18.16.0
|
||||
cache: "yarn"
|
||||
cache-dependency-path: grafana-plugin/yarn.lock
|
||||
|
||||
|
|
|
|||
2
.github/workflows/snyk.yml
vendored
2
.github/workflows/snyk.yml
vendored
|
|
@ -22,7 +22,7 @@ jobs:
|
|||
cache-dependency-path: engine/requirements.txt
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 14.17.0
|
||||
node-version: 18.16.0
|
||||
cache: "yarn"
|
||||
cache-dependency-path: grafana-plugin/yarn.lock
|
||||
- uses: snyk/actions/setup@master
|
||||
|
|
|
|||
1
.nvmrc
1
.nvmrc
|
|
@ -1 +0,0 @@
|
|||
14.17.0
|
||||
|
|
@ -5,6 +5,12 @@ 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).
|
||||
|
||||
## v1.2.25 (2023-05-18)
|
||||
|
||||
### Added
|
||||
|
||||
- Test mobile push backend
|
||||
|
||||
## v1.2.24 (2023-05-17)
|
||||
|
||||
### Fixed
|
||||
|
|
|
|||
19
Makefile
19
Makefile
|
|
@ -68,6 +68,10 @@ define run_engine_docker_command
|
|||
$(call run_docker_compose_command,run --rm oncall_engine_commands $(1))
|
||||
endef
|
||||
|
||||
define run_ui_docker_command
|
||||
$(call run_docker_compose_command,run --rm oncall_ui sh -c '$(1)')
|
||||
endef
|
||||
|
||||
# touch SQLITE_DB_FILE if it does not exist and DB is eqaul to SQLITE_PROFILE
|
||||
start: ## start all of the docker containers
|
||||
ifeq ($(DB),$(SQLITE_PROFILE))
|
||||
|
|
@ -83,7 +87,7 @@ init: ## build the frontend plugin code then run make start
|
|||
# this makes sure that it will be available when the grafana container starts up without the need to
|
||||
# restart the grafana container initially
|
||||
ifeq ($(findstring $(UI_PROFILE),$(COMPOSE_PROFILES)),$(UI_PROFILE))
|
||||
cd grafana-plugin && yarn install && yarn build:dev
|
||||
$(call run_ui_docker_command,yarn install && yarn build:dev)
|
||||
endif
|
||||
|
||||
stop: # stop all of the docker containers
|
||||
|
|
@ -136,6 +140,19 @@ engine-manage: ## run Django's `manage.py` script, inside of a docker container
|
|||
## https://docs.djangoproject.com/en/4.1/ref/django-admin/#django-admin-makemigrations
|
||||
$(call run_engine_docker_command,python manage.py $(CMD))
|
||||
|
||||
ui-test: ## run the UI tests
|
||||
$(call run_ui_docker_command,yarn test)
|
||||
|
||||
ui-lint: ## run the UI linter
|
||||
$(call run_ui_docker_command,yarn lint)
|
||||
|
||||
ui-build: ## build the UI
|
||||
$(call run_ui_docker_command,yarn build)
|
||||
|
||||
ui-command: ## run any command, inside of a UI docker container, passing `$CMD` as arguments.
|
||||
## e.g. `make ui-command CMD="yarn test"`
|
||||
$(call run_ui_docker_command,$(CMD))
|
||||
|
||||
exec-engine: ## exec into engine container's bash
|
||||
docker exec -it oncall_engine bash
|
||||
|
||||
|
|
|
|||
|
|
@ -192,8 +192,6 @@ yarn test:integration
|
|||
|
||||
## Useful `make` commands
|
||||
|
||||
See [`COMPOSE_PROFILES`](#compose_profiles) for more information on what this option is and how to configure it.
|
||||
|
||||
> 🚶This part was moved to `make help` command. Run it to see all the available commands and their descriptions
|
||||
|
||||
## Setting environment variables
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ services:
|
|||
MOBILE_APP_QR_INTERVAL_QUEUE: 290000 # 4 minutes and 50 seconds
|
||||
volumes:
|
||||
- ./grafana-plugin:/etc/app
|
||||
- /etc/app/node_modules
|
||||
- node_modules_dev:/etc/app/node_modules
|
||||
# https://stackoverflow.com/a/60456034
|
||||
- ${ENTERPRISE_FRONTEND:-/dev/null}:/etc/app/frontend_enterprise
|
||||
profiles:
|
||||
|
|
@ -313,6 +313,8 @@ volumes:
|
|||
labels: *oncall-labels
|
||||
mysqldata_dev:
|
||||
labels: *oncall-labels
|
||||
node_modules_dev:
|
||||
labels: *oncall-labels
|
||||
|
||||
networks:
|
||||
default:
|
||||
|
|
|
|||
|
|
@ -4,3 +4,8 @@ from rest_framework.throttling import UserRateThrottle
|
|||
class TestCallThrottler(UserRateThrottle):
|
||||
scope = "make_test_call"
|
||||
rate = "5/m"
|
||||
|
||||
|
||||
class TestPushThrottler(UserRateThrottle):
|
||||
scope = "send_test_push"
|
||||
rate = "10/m"
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ FEATURE_GRAFANA_CLOUD_NOTIFICATIONS = "grafana_cloud_notifications"
|
|||
FEATURE_GRAFANA_CLOUD_CONNECTION = "grafana_cloud_connection"
|
||||
FEATURE_WEB_SCHEDULES = "web_schedules"
|
||||
FEATURE_WEBHOOKS2 = "webhooks2"
|
||||
FEATURE_MOBILE_TEST_PUSH = "mobile_test_push"
|
||||
|
||||
|
||||
class FeaturesAPIView(APIView):
|
||||
|
|
|
|||
|
|
@ -33,12 +33,15 @@ from apps.api.throttlers import (
|
|||
VerifyPhoneNumberThrottlerPerOrg,
|
||||
VerifyPhoneNumberThrottlerPerUser,
|
||||
)
|
||||
from apps.api.throttlers.test_call_throttler import TestPushThrottler
|
||||
from apps.auth_token.auth import PluginAuthentication
|
||||
from apps.auth_token.constants import SCHEDULE_EXPORT_TOKEN_NAME
|
||||
from apps.auth_token.models import UserScheduleExportAuthToken
|
||||
from apps.base.messaging import get_messaging_backend_from_id
|
||||
from apps.base.utils import live_settings
|
||||
from apps.mobile_app.auth import MobileAppAuthTokenAuthentication
|
||||
from apps.mobile_app.demo_push import send_test_push
|
||||
from apps.mobile_app.exceptions import DeviceNotSet
|
||||
from apps.schedules.models import OnCallSchedule
|
||||
from apps.telegram.client import TelegramClient
|
||||
from apps.telegram.models import TelegramVerificationCode
|
||||
|
|
@ -156,6 +159,7 @@ class UserView(
|
|||
"unlink_telegram": [RBACPermission.Permissions.USER_SETTINGS_WRITE],
|
||||
"unlink_backend": [RBACPermission.Permissions.USER_SETTINGS_WRITE],
|
||||
"make_test_call": [RBACPermission.Permissions.USER_SETTINGS_WRITE],
|
||||
"send_test_push": [RBACPermission.Permissions.USER_SETTINGS_WRITE],
|
||||
"export_token": [RBACPermission.Permissions.USER_SETTINGS_WRITE],
|
||||
"upcoming_shifts": [RBACPermission.Permissions.USER_SETTINGS_WRITE],
|
||||
}
|
||||
|
|
@ -177,6 +181,7 @@ class UserView(
|
|||
"unlink_telegram",
|
||||
"unlink_backend",
|
||||
"make_test_call",
|
||||
"send_test_push",
|
||||
"export_token",
|
||||
"upcoming_shifts",
|
||||
],
|
||||
|
|
@ -391,6 +396,25 @@ class UserView(
|
|||
|
||||
return Response(status=status.HTTP_200_OK)
|
||||
|
||||
@action(detail=True, methods=["post"], throttle_classes=[TestPushThrottler])
|
||||
def send_test_push(self, request, pk):
|
||||
user = self.get_object()
|
||||
critical = request.query_params.get("critical", "false") == "true"
|
||||
|
||||
try:
|
||||
send_test_push(user, critical)
|
||||
except DeviceNotSet:
|
||||
return Response(
|
||||
data="Mobile device not connected",
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.info(f"UserView.send_test_push: Unable to send test push due to {e}")
|
||||
return Response(
|
||||
data="Something went wrong while sending a test push", status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||
)
|
||||
return Response(status=status.HTTP_200_OK)
|
||||
|
||||
@action(detail=True, methods=["get"])
|
||||
def get_backend_verification_code(self, request, pk):
|
||||
backend_id = request.query_params.get("backend")
|
||||
|
|
|
|||
86
engine/apps/mobile_app/demo_push.py
Normal file
86
engine/apps/mobile_app/demo_push.py
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
import json
|
||||
import random
|
||||
import string
|
||||
|
||||
from fcm_django.models import FCMDevice
|
||||
from firebase_admin.messaging import APNSPayload, Aps, ApsAlert, CriticalSound, Message
|
||||
|
||||
from apps.mobile_app.exceptions import DeviceNotSet
|
||||
from apps.mobile_app.tasks import FCMMessageData, MessageType, _construct_fcm_message, _send_push_notification, logger
|
||||
from apps.user_management.models import User
|
||||
|
||||
TEST_PUSH_TITLE = "Hi, this is a test notification from Grafana OnCall"
|
||||
|
||||
|
||||
def send_test_push(user, critical=False):
|
||||
device_to_notify = FCMDevice.objects.filter(user=user).first()
|
||||
if device_to_notify is None:
|
||||
logger.info(f"send_test_push: fcm_device not found user_id={user.id}")
|
||||
raise DeviceNotSet
|
||||
message = _get_test_escalation_fcm_message(user, device_to_notify, critical)
|
||||
_send_push_notification(device_to_notify, message)
|
||||
|
||||
|
||||
def _get_test_escalation_fcm_message(user: User, device_to_notify: FCMDevice, critical: bool) -> Message:
|
||||
# TODO: this method is copied from _get_alert_group_escalation_fcm_message
|
||||
# to have same notification/sound/overrideDND logic. Ideally this logic should be abstracted, not repeated.
|
||||
from apps.mobile_app.models import MobileAppUserSettings
|
||||
|
||||
thread_id = f"{''.join(random.choices(string.digits, k=6))}:test_push"
|
||||
|
||||
mobile_app_user_settings, _ = MobileAppUserSettings.objects.get_or_create(user=user)
|
||||
# critical defines the type of notification.
|
||||
# we use overrideDND to establish if the notification should sound even if DND is on
|
||||
overrideDND = critical and mobile_app_user_settings.important_notification_override_dnd
|
||||
|
||||
# APNS only allows to specify volume for critical notifications
|
||||
apns_volume = mobile_app_user_settings.important_notification_volume if critical else None
|
||||
apns_sound_name = (
|
||||
mobile_app_user_settings.important_notification_sound_name
|
||||
if critical
|
||||
else mobile_app_user_settings.default_notification_sound_name
|
||||
) + MobileAppUserSettings.IOS_SOUND_NAME_EXTENSION # iOS app expects the filename to have an extension
|
||||
|
||||
fcm_message_data: FCMMessageData = {
|
||||
"title": TEST_PUSH_TITLE,
|
||||
# Pass user settings, so the Android app can use them to play the correct sound and volume
|
||||
"default_notification_sound_name": (
|
||||
mobile_app_user_settings.default_notification_sound_name
|
||||
+ MobileAppUserSettings.ANDROID_SOUND_NAME_EXTENSION
|
||||
),
|
||||
"default_notification_volume_type": mobile_app_user_settings.default_notification_volume_type,
|
||||
"default_notification_volume": str(mobile_app_user_settings.default_notification_volume),
|
||||
"default_notification_volume_override": json.dumps(
|
||||
mobile_app_user_settings.default_notification_volume_override
|
||||
),
|
||||
"important_notification_sound_name": (
|
||||
mobile_app_user_settings.important_notification_sound_name
|
||||
+ MobileAppUserSettings.ANDROID_SOUND_NAME_EXTENSION
|
||||
),
|
||||
"important_notification_volume_type": mobile_app_user_settings.important_notification_volume_type,
|
||||
"important_notification_volume": str(mobile_app_user_settings.important_notification_volume),
|
||||
"important_notification_volume_override": json.dumps(
|
||||
mobile_app_user_settings.important_notification_volume_override
|
||||
),
|
||||
"important_notification_override_dnd": json.dumps(mobile_app_user_settings.important_notification_override_dnd),
|
||||
}
|
||||
|
||||
apns_payload = APNSPayload(
|
||||
aps=Aps(
|
||||
thread_id=thread_id,
|
||||
alert=ApsAlert(title=TEST_PUSH_TITLE),
|
||||
sound=CriticalSound(
|
||||
# The notification shouldn't be critical if the user has disabled "override DND" setting
|
||||
critical=overrideDND,
|
||||
name=apns_sound_name,
|
||||
volume=apns_volume,
|
||||
),
|
||||
custom_data={
|
||||
"interruption-level": "critical" if overrideDND else "time-sensitive",
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
message_type = MessageType.CRITICAL if critical else MessageType.NORMAL
|
||||
|
||||
return _construct_fcm_message(message_type, device_to_notify, thread_id, fcm_message_data, apns_payload)
|
||||
6
engine/apps/mobile_app/exceptions.py
Normal file
6
engine/apps/mobile_app/exceptions.py
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
class DeviceNotSet(Exception):
|
||||
"""
|
||||
Indicates that user has no connected fcm device.
|
||||
Introduced only for test_push_notification handler.
|
||||
We should have generic test notifications system across all messaging backends.
|
||||
"""
|
||||
95
engine/apps/mobile_app/tests/test_demo_push.py
Normal file
95
engine/apps/mobile_app/tests/test_demo_push.py
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
import pytest
|
||||
from fcm_django.models import FCMDevice
|
||||
|
||||
from apps.mobile_app.demo_push import TEST_PUSH_TITLE, _get_test_escalation_fcm_message
|
||||
from apps.mobile_app.models import MobileAppUserSettings
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_test_escalation_fcm_message_user_settings(
|
||||
make_organization_and_user, make_alert_receive_channel, make_alert_group, make_alert
|
||||
):
|
||||
organization, user = make_organization_and_user()
|
||||
device = FCMDevice.objects.create(user=user, registration_id="test_device_id")
|
||||
|
||||
message = _get_test_escalation_fcm_message(user, device, critical=False)
|
||||
|
||||
# Check user settings are passed to FCM message
|
||||
assert message.data["default_notification_sound_name"] == "default_sound.mp3"
|
||||
assert message.data["default_notification_volume_type"] == "constant"
|
||||
assert message.data["default_notification_volume_override"] == "false"
|
||||
assert message.data["default_notification_volume"] == "0.8"
|
||||
assert message.data["important_notification_sound_name"] == "default_sound_important.mp3"
|
||||
assert message.data["important_notification_volume_type"] == "constant"
|
||||
assert message.data["important_notification_volume"] == "0.8"
|
||||
assert message.data["important_notification_volume_override"] == "true"
|
||||
assert message.data["important_notification_override_dnd"] == "true"
|
||||
|
||||
# Check APNS notification sound is set correctly
|
||||
apns_sound = message.apns.payload.aps.sound
|
||||
assert apns_sound.critical is False
|
||||
assert apns_sound.name == "default_sound.aiff"
|
||||
assert apns_sound.volume is None # APNS doesn't allow to specify volume for non-critical notifications
|
||||
|
||||
# Check expected test push content
|
||||
assert message.apns.payload.aps.badge is None
|
||||
assert message.apns.payload.aps.alert.title == TEST_PUSH_TITLE
|
||||
assert message.data["title"] == TEST_PUSH_TITLE
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_escalation_fcm_message_user_settings_critical(
|
||||
make_organization_and_user, make_alert_receive_channel, make_alert_group, make_alert
|
||||
):
|
||||
organization, user = make_organization_and_user()
|
||||
device = FCMDevice.objects.create(user=user, registration_id="test_device_id")
|
||||
|
||||
message = _get_test_escalation_fcm_message(user, device, critical=True)
|
||||
|
||||
# Check user settings are passed to FCM message
|
||||
assert message.data["default_notification_sound_name"] == "default_sound.mp3"
|
||||
assert message.data["default_notification_volume_type"] == "constant"
|
||||
assert message.data["default_notification_volume_override"] == "false"
|
||||
assert message.data["default_notification_volume"] == "0.8"
|
||||
assert message.data["important_notification_sound_name"] == "default_sound_important.mp3"
|
||||
assert message.data["important_notification_volume_type"] == "constant"
|
||||
assert message.data["important_notification_volume"] == "0.8"
|
||||
assert message.data["important_notification_volume_override"] == "true"
|
||||
assert message.data["important_notification_override_dnd"] == "true"
|
||||
|
||||
# Check APNS notification sound is set correctly
|
||||
apns_sound = message.apns.payload.aps.sound
|
||||
assert apns_sound.critical is True
|
||||
assert apns_sound.name == "default_sound_important.aiff"
|
||||
assert apns_sound.volume == 0.8
|
||||
assert message.apns.payload.aps.custom_data["interruption-level"] == "critical"
|
||||
|
||||
# Check expected test push content
|
||||
assert message.apns.payload.aps.badge is None
|
||||
assert message.apns.payload.aps.alert.title == TEST_PUSH_TITLE
|
||||
assert message.data["title"] == TEST_PUSH_TITLE
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_escalation_fcm_message_user_settings_critical_override_dnd_disabled(
|
||||
make_organization_and_user, make_alert_receive_channel, make_alert_group, make_alert
|
||||
):
|
||||
organization, user = make_organization_and_user()
|
||||
device = FCMDevice.objects.create(user=user, registration_id="test_device_id")
|
||||
|
||||
# Disable important notification override DND
|
||||
MobileAppUserSettings.objects.create(user=user, important_notification_override_dnd=False)
|
||||
message = _get_test_escalation_fcm_message(user, device, critical=True)
|
||||
|
||||
# Check user settings are passed to FCM message
|
||||
assert message.data["important_notification_override_dnd"] == "false"
|
||||
|
||||
# Check APNS notification sound is set correctly
|
||||
apns_sound = message.apns.payload.aps.sound
|
||||
assert apns_sound.critical is False
|
||||
assert message.apns.payload.aps.custom_data["interruption-level"] == "time-sensitive"
|
||||
|
||||
# Check expected test push content
|
||||
assert message.apns.payload.aps.badge is None
|
||||
assert message.apns.payload.aps.alert.title == TEST_PUSH_TITLE
|
||||
assert message.data["title"] == TEST_PUSH_TITLE
|
||||
|
|
@ -1,14 +1,6 @@
|
|||
FROM node:14.17.0-alpine
|
||||
FROM node:18.16.0-alpine
|
||||
|
||||
WORKDIR /etc/app
|
||||
ENV PATH /etc/app/node_modules/.bin:$PATH
|
||||
|
||||
# this allows hot reloading of the container
|
||||
# https://stackoverflow.com/a/72478714
|
||||
ENV WATCHPACK_POLLING true
|
||||
|
||||
COPY ./package.json ./
|
||||
COPY ./yarn.lock ./
|
||||
RUN yarn install
|
||||
|
||||
CMD ["yarn", "start"]
|
||||
|
|
|
|||
|
|
@ -56,8 +56,8 @@
|
|||
"@babel/preset-env": "^7.18.10",
|
||||
"@babel/preset-react": "^7.18.6",
|
||||
"@babel/preset-typescript": "^7.18.6",
|
||||
"@grafana/eslint-config": "^5.0.0",
|
||||
"@grafana/toolkit": "^9.2.4",
|
||||
"@grafana/eslint-config": "^5.1.0",
|
||||
"@grafana/toolkit": "^9.5.2",
|
||||
"@jest/globals": "^27.5.1",
|
||||
"@playwright/test": "^1.32.0",
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
|
|
@ -81,7 +81,7 @@
|
|||
"dompurify": "^2.3.12",
|
||||
"dotenv": "^16.0.3",
|
||||
"eslint": "^8.25.0",
|
||||
"eslint-plugin-jsdoc": "^39.3.14",
|
||||
"eslint-plugin-jsdoc": "^44.2.4",
|
||||
"eslint-plugin-react": "^7.31.10",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-rulesdir": "^0.2.1",
|
||||
|
|
|
|||
|
|
@ -14,6 +14,11 @@
|
|||
}
|
||||
}
|
||||
|
||||
.notification-buttons {
|
||||
width: 100%;
|
||||
padding-top: 12px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-top: -6px;
|
||||
margin-left: 4px;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { Button, Icon, LoadingPlaceholder, VerticalGroup } from '@grafana/ui';
|
||||
import { Button, HorizontalGroup, Icon, LoadingPlaceholder, VerticalGroup } from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
import { observer } from 'mobx-react';
|
||||
|
||||
|
|
@ -12,6 +12,7 @@ import { WithPermissionControlDisplay } from 'containers/WithPermissionControl/W
|
|||
import { User } from 'models/user/user.types';
|
||||
import { AppFeature } from 'state/features';
|
||||
import { useStore } from 'state/useStore';
|
||||
import { openErrorNotification, openNotification, openWarningNotification } from 'utils';
|
||||
import { UserActions } from 'utils/authorization';
|
||||
|
||||
import styles from './MobileAppConnection.module.scss';
|
||||
|
|
@ -73,6 +74,8 @@ const MobileAppConnection = observer(({ userPk }: Props) => {
|
|||
const [userTimeoutId, setUserTimeoutId] = useState<NodeJS.Timeout>(undefined);
|
||||
const [refreshTimeoutId, setRefreshTimeoutId] = useState<NodeJS.Timeout>(undefined);
|
||||
const [isQRBlurry, setIsQRBlurry] = useState<boolean>(false);
|
||||
const [isAttemptingTestNotification, setIsAttemptingTestNotification] = useState(false);
|
||||
const isCurrentUser = userStore.currentUserPk === userPk;
|
||||
|
||||
const fetchQRCode = useCallback(
|
||||
async (showLoader = true) => {
|
||||
|
|
@ -188,17 +191,58 @@ const MobileAppConnection = observer(({ userPk }: Props) => {
|
|||
userAction={UserActions.UserSettingsWrite}
|
||||
message="You do not have permission to perform this action. Ask an admin to upgrade your permissions."
|
||||
>
|
||||
<div className={cx('container')}>
|
||||
<Block shadowed bordered withBackground className={cx('container__box')}>
|
||||
<DownloadIcons />
|
||||
</Block>
|
||||
<Block shadowed bordered withBackground className={cx('container__box')}>
|
||||
{content}
|
||||
</Block>
|
||||
</div>
|
||||
<VerticalGroup>
|
||||
<div className={cx('container')}>
|
||||
<Block shadowed bordered withBackground className={cx('container__box')}>
|
||||
<DownloadIcons />
|
||||
</Block>
|
||||
<Block shadowed bordered withBackground className={cx('container__box')}>
|
||||
{content}
|
||||
</Block>
|
||||
</div>
|
||||
{false && // temporary disable test notifications
|
||||
mobileAppIsCurrentlyConnected &&
|
||||
isCurrentUser && (
|
||||
<div className={cx('notification-buttons')}>
|
||||
<HorizontalGroup spacing={'md'} justify={'flex-end'}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => onSendTestNotification()}
|
||||
disabled={isAttemptingTestNotification}
|
||||
>
|
||||
Send Test Push notification
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={() => onSendTestNotification(true)}
|
||||
disabled={isAttemptingTestNotification}
|
||||
>
|
||||
Send Critical Test Push notification
|
||||
</Button>
|
||||
</HorizontalGroup>
|
||||
</div>
|
||||
)}
|
||||
</VerticalGroup>
|
||||
</WithPermissionControlDisplay>
|
||||
);
|
||||
|
||||
async function onSendTestNotification(isCritical = false) {
|
||||
setIsAttemptingTestNotification(true);
|
||||
|
||||
try {
|
||||
await userStore.sendTestPushNotification(userPk, isCritical);
|
||||
openNotification('Notification was sent');
|
||||
} catch (ex) {
|
||||
if (ex.response?.status === 429) {
|
||||
openWarningNotification('Too much attempts, try again later');
|
||||
} else {
|
||||
openErrorNotification('There was an error sending the notification');
|
||||
}
|
||||
} finally {
|
||||
setIsAttemptingTestNotification(false);
|
||||
}
|
||||
}
|
||||
|
||||
function getParsedQRCodeValue() {
|
||||
try {
|
||||
return JSON.parse(QRCodeValue);
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -45,7 +45,7 @@ const originalError = console.error;
|
|||
|
||||
beforeEach(() => {
|
||||
delete global.window.location;
|
||||
global.window = Object.create(window);
|
||||
global.window ??= Object.create(window);
|
||||
global.window.location = {
|
||||
protocol: MOCK_PROTOCOL,
|
||||
host: MOCK_HOST,
|
||||
|
|
|
|||
|
|
@ -347,6 +347,16 @@ export class UserStore extends BaseStore {
|
|||
this.notificationChoices = get(response, 'actions.POST', []);
|
||||
}
|
||||
|
||||
@action
|
||||
async sendTestPushNotification(userId: User['pk'], isCritical: boolean) {
|
||||
return await makeRequest(`/users/${userId}/send_test_push`, {
|
||||
method: 'POST',
|
||||
params: {
|
||||
critical: isCritical,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
async updateNotifyByOptions() {
|
||||
const response = await makeRequest('/notification_policies/notify_by_options/', {});
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
Loading…
Add table
Reference in a new issue