Merge branch 'dev' into rares/grafana-faro

This commit is contained in:
teodosii 2022-12-19 13:30:47 +02:00
commit acfa730903
35 changed files with 173 additions and 177 deletions

View file

@ -5,7 +5,36 @@ 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.0 (TBD)
## v1.1.8 (2022-12-13)
### Added
- Added a `make` command, `enable-mobile-app-feature-flags`, which sets the backend feature flag in `./dev/.env.dev`,
and updates a record in the `base_dynamicsetting` database table, which are needed to enable the mobile
app backend features.
### Changed
- removed APNS support
- changed the `django-push-notification` library from the `iskhakov` fork to the [`grafana` fork](https://github.com/grafana/django-push-notifications).
This new fork basically patches an issue which affected the database migrations of this django app (previously the
library would not respect the `USER_MODEL` setting when creating its tables and would instead reference the
`auth_user` table.. which we don't want)
- add `--no-cache` flag to the `make build` command
### Fixed
- fix schedule UI types and permissions
## v1.1.7 (2022-12-09)
### Fixed
- Update fallback role for schedule write RBAC permission
- Mobile App Verification tab in the user settings modal is now hidden for users that do not have proper
permissions to use it
## v1.1.6 (2022-12-09)
### Added

View file

@ -86,7 +86,7 @@ restart:
$(call run_docker_compose_command,restart)
build:
$(call run_docker_compose_command,build)
$(call run_docker_compose_command,build --no-cache)
cleanup: stop
docker system prune --filter label="$(DOCKER_COMPOSE_DEV_LABEL)" --all --volumes
@ -126,6 +126,12 @@ engine-manage:
exec-engine:
docker exec -it oncall_engine bash
_enable-mobile-app-feature-flags:
$(shell ./dev/add_env_var.sh FEATURE_MOBILE_APP_INTEGRATION_ENABLED True $(DEV_ENV_FILE))
$(call run_engine_docker_command,python manage.py enable_mobile_app)
enable-mobile-app-feature-flags: _enable-mobile-app-feature-flags stop start
# The below commands are useful for running backend services outside of docker
define backend_command
export `grep -v '^#' $(DEV_ENV_FILE) | xargs -0` && \

View file

@ -120,6 +120,10 @@ make build # rebuild images (e.g. when changing requirements.txt)
# run Django's `manage.py` script, inside of a docker container, passing `$CMD` as arguments.
# e.g. `make engine-manage CMD="makemigrations"` - https://docs.djangoproject.com/en/4.1/ref/django-admin/#django-admin-makemigrations
make engine-manage CMD="..."
# sets a feature flag, related to mobile app backend functionality, in your ./dev/.env.dev
# and sets the necessary database values
# NOTE: you need to enable, and configure, the plugin before running this command
make enable-mobile-app-feature-flags
# this will remove all of the images, containers, volumes, and networks
# associated with your local OnCall developer setup

26
dev/add_env_var.sh Executable file
View file

@ -0,0 +1,26 @@
#!/usr/bin/env bash
# https://gist.github.com/maxpoletaev/4ed25183427a2cd7e57a
case "$OSTYPE" in
darwin*) PLATFORM="OSX" ;;
linux*) PLATFORM="LINUX" ;;
bsd*) PLATFORM="BSD" ;;
*) PLATFORM="UNKNOWN" ;;
esac
replace() {
if [[ "$PLATFORM" == "OSX" || "$PLATFORM" == "BSD" ]]; then
sed -i "" "$1" "$2"
elif [ "$PLATFORM" == "LINUX" ]; then
sed -i "$1" "$2"
fi
}
if grep -q $1 $3; then
# file contains string, lets replace it
# https://stackoverflow.com/a/42667816 - why we need the -i ''
replace "s~$1=.*~$1=$2~g" $3
else
# file doesn't contain string, lets append it
echo "$1=$2" >> $3
fi

View file

@ -74,7 +74,7 @@ class AlertGroupQuerySet(models.QuerySet):
# Try to return the last open group
# Note that (channel, channel_filter, distinction, is_open_for_grouping) is in unique_together
try:
return self.get(**search_params, is_open_for_grouping=True), False
return self.get(**search_params, is_open_for_grouping__isnull=False), False
except self.model.DoesNotExist:
pass

View file

@ -109,7 +109,7 @@ class RBACPermission(permissions.BasePermission):
Resources.SCHEDULES, Actions.READ, LegacyAccessControlRole.VIEWER
)
SCHEDULES_WRITE = LegacyAccessControlCompatiblePermission(
Resources.SCHEDULES, Actions.WRITE, LegacyAccessControlRole.ADMIN
Resources.SCHEDULES, Actions.WRITE, LegacyAccessControlRole.EDITOR
)
SCHEDULES_EXPORT = LegacyAccessControlCompatiblePermission(
Resources.SCHEDULES, Actions.EXPORT, LegacyAccessControlRole.EDITOR

View file

@ -926,7 +926,7 @@ def test_create_on_call_shift_override_invalid_data(on_call_shift_internal_api_s
"role,expected_status",
[
(LegacyAccessControlRole.ADMIN, status.HTTP_201_CREATED),
(LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN),
(LegacyAccessControlRole.EDITOR, status.HTTP_201_CREATED),
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
],
)
@ -958,7 +958,7 @@ def test_on_call_shift_create_permissions(
"role,expected_status",
[
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
(LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN),
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
],
)
@ -1080,7 +1080,7 @@ def test_on_call_shift_retrieve_permissions(
"role,expected_status",
[
(LegacyAccessControlRole.ADMIN, status.HTTP_204_NO_CONTENT),
(LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN),
(LegacyAccessControlRole.EDITOR, status.HTTP_204_NO_CONTENT),
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
],
)
@ -1185,7 +1185,7 @@ def test_on_call_shift_days_options_permissions(
"role,expected_status",
[
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
(LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN),
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
],
)

View file

@ -1204,7 +1204,7 @@ def test_filter_events_invalid_type(
"role,expected_status",
[
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
(LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN),
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
],
)
@ -1242,7 +1242,7 @@ def test_schedule_create_permissions(
"role,expected_status",
[
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
(LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN),
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
],
)
@ -1360,7 +1360,7 @@ def test_schedule_retrieve_permissions(
"role,expected_status",
[
(LegacyAccessControlRole.ADMIN, status.HTTP_204_NO_CONTENT),
(LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN),
(LegacyAccessControlRole.EDITOR, status.HTTP_204_NO_CONTENT),
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
],
)
@ -1436,7 +1436,7 @@ def test_events_permissions(
"role,expected_status",
[
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
(LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN),
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
],
)

View file

@ -1,7 +1,5 @@
from django.conf import settings
from django.urls import include, path, re_path
from apps.mobile_app.views import APNSDeviceAuthorizedViewSet
from common.api_helpers.optional_slash_router import OptionalSlashRouter, optional_slash_path
from .views import UserNotificationPolicyView, auth
@ -68,10 +66,6 @@ router.register(r"tokens", PublicApiTokenView, basename="api_token")
router.register(r"live_settings", LiveSettingViewSet, basename="live_settings")
router.register(r"oncall_shifts", OnCallShiftView, basename="oncall_shifts")
# TODO: remove this when the hackathon app is deprecated (APNSDeviceAuthorizedViewSet is registered in mobile_app)
if settings.FEATURE_MOBILE_APP_INTEGRATION_ENABLED:
router.register(r"device/apns", APNSDeviceAuthorizedViewSet)
urlpatterns = [
path("", include(router.urls)),
optional_slash_path("user", CurrentUserView.as_view(), name="api-user"),

View file

@ -28,8 +28,7 @@ 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, MobileAppVerificationTokenAuthentication
from apps.mobile_app.models import MobileAppAuthToken
from apps.mobile_app.auth import MobileAppAuthTokenAuthentication
from apps.telegram.client import TelegramClient
from apps.telegram.models import TelegramVerificationCode
from apps.twilioapp.phone_manager import PhoneManager
@ -128,7 +127,6 @@ class UserView(
"unlink_backend": [RBACPermission.Permissions.USER_SETTINGS_WRITE],
"make_test_call": [RBACPermission.Permissions.USER_SETTINGS_WRITE],
"export_token": [RBACPermission.Permissions.USER_SETTINGS_WRITE],
"mobile_app_auth_token": [RBACPermission.Permissions.USER_SETTINGS_WRITE],
}
rbac_object_permissions = {
@ -149,7 +147,6 @@ class UserView(
"unlink_backend",
"make_test_call",
"export_token",
"mobile_app_auth_token",
],
}
@ -471,64 +468,3 @@ class UserView(
except UserScheduleExportAuthToken.DoesNotExist:
raise NotFound
return Response(status=status.HTTP_204_NO_CONTENT)
@action(
methods=["get", "post", "delete"],
detail=False,
authentication_classes=(MobileAppVerificationTokenAuthentication,),
)
def mobile_app_auth_token(self, request):
"""
TODO: remove after hackathon app is deprecated (see apps.mobile_app.views.MobileAppAuthTokenAPIView)
"""
DynamicSetting = apps.get_model("base", "DynamicSetting")
if not settings.FEATURE_MOBILE_APP_INTEGRATION_ENABLED:
return Response(status=status.HTTP_404_NOT_FOUND)
mobile_app_settings = DynamicSetting.objects.get_or_create(
name="mobile_app_settings",
defaults={
"json_value": {
"org_ids": [],
}
},
)[0]
if self.request.auth.organization.pk not in mobile_app_settings.json_value["org_ids"]:
return Response(status=status.HTTP_404_NOT_FOUND)
if self.request.method == "GET":
try:
token = MobileAppAuthToken.objects.get(user=self.request.user)
except MobileAppAuthToken.DoesNotExist:
raise NotFound
response = {
"token_id": token.id,
"user_id": token.user_id,
"organization_id": token.organization_id,
"created_at": token.created_at,
"revoked_at": token.revoked_at,
}
return Response(response, status=status.HTTP_200_OK)
if self.request.method == "POST":
# If token already exists revoke it
try:
token = MobileAppAuthToken.objects.get(user=self.request.user)
token.delete()
except MobileAppAuthToken.DoesNotExist:
pass
instance, token = MobileAppAuthToken.create_auth_token(self.request.user, self.request.user.organization)
data = {"id": instance.pk, "token": token, "created_at": instance.created_at}
return Response(data, status=status.HTTP_201_CREATED)
if self.request.method == "DELETE":
try:
token = MobileAppAuthToken.objects.get(user=self.request.user)
token.delete()
except MobileAppAuthToken.DoesNotExist:
raise NotFound
return Response(status=status.HTTP_204_NO_CONTENT)

View file

@ -191,7 +191,7 @@ class GcomAPIClient(APIClient):
super().__init__(settings.GRAFANA_COM_API_URL, api_token)
def get_instance_info(self, stack_id: str) -> Optional[GCOMInstanceInfo]:
data, _ = self.api_get(f"instances/{stack_id}?config=true")
data, _ = self.api_get(f"instances/{stack_id}")
return data
def get_instances(self, query: str):

View file

@ -44,8 +44,6 @@ def check_gcom_permission(token_string: str, context) -> Optional["GcomToken"]:
if not instance_info or str(instance_info["orgId"]) != org_id:
raise InvalidToken
rbac_is_enabled = client.is_rbac_enabled_for_organization()
if not organization:
DynamicSetting = apps.get_model("base", "DynamicSetting")
allow_signup = DynamicSetting.objects.get_or_create(
@ -62,7 +60,6 @@ def check_gcom_permission(token_string: str, context) -> Optional["GcomToken"]:
region_slug=instance_info["regionSlug"],
gcom_token=token_string,
gcom_token_org_last_time_synced=timezone.now(),
is_rbac_permissions_enabled=rbac_is_enabled,
)
else:
organization.stack_slug = instance_info["slug"]
@ -72,7 +69,6 @@ def check_gcom_permission(token_string: str, context) -> Optional["GcomToken"]:
organization.grafana_url = instance_info["url"]
organization.gcom_token = token_string
organization.gcom_token_org_last_time_synced = timezone.now()
organization.is_rbac_permissions_enabled = rbac_is_enabled
organization.save(
update_fields=[
"stack_slug",
@ -82,7 +78,6 @@ def check_gcom_permission(token_string: str, context) -> Optional["GcomToken"]:
"grafana_url",
"gcom_token",
"gcom_token_org_last_time_synced",
"is_rbac_permissions_enabled",
]
)
logger.debug(f"Finish authenticate by making request to gcom api for org={org_id}, stack_id={stack_id}")

View file

@ -1,7 +1,7 @@
import json
from django.conf import settings
from push_notifications.models import APNSDevice, GCMDevice
from push_notifications.models import GCMDevice
from apps.base.messaging import BaseMessagingBackend
from apps.mobile_app.tasks import notify_user_async
@ -35,7 +35,6 @@ class MobileAppBackend(BaseMessagingBackend):
token.delete()
# delete push notification related info for user
APNSDevice.objects.filter(user=user).delete()
GCMDevice.objects.filter(user=user).delete()
def serialize_user(self, user):

View file

@ -1,6 +1,6 @@
from celery.utils.log import get_task_logger
from django.conf import settings
from push_notifications.models import APNSDevice, GCMDevice
from push_notifications.models import GCMDevice
from apps.alerts.models import AlertGroup
from apps.mobile_app.alert_rendering import get_push_notification_message
@ -34,12 +34,10 @@ def notify_user_async(user_pk, alert_group_pk, notification_policy_pk, critical)
logger.warning(f"User notification policy {notification_policy_pk} does not exist")
return
# APNS is for notifying iOS devices, GCM for Android
apns_devices_to_notify = APNSDevice.objects.filter(user=user)
gcm_devices_to_notify = GCMDevice.objects.filter(user=user)
# create an error log in case user has no devices set up
if not apns_devices_to_notify.exists() and not gcm_devices_to_notify.exists():
if not gcm_devices_to_notify.exists():
UserNotificationPolicyLogRecord.objects.create(
author=user,
type=UserNotificationPolicyLogRecord.TYPE_PERSONAL_NOTIFICATION_FAILED,
@ -67,29 +65,21 @@ def notify_user_async(user_pk, alert_group_pk, notification_policy_pk, critical)
"sound": "bingbong.aiff",
}
apns_devices_to_notify.send_message(
message,
thread_id=thread_id,
category="USER_NEW_INCIDENT", # TODO: rename to USER_NEW_ALERT_GROUP
extra={
"orgId": alert_group.channel.organization.public_primary_key,
"orgName": alert_group.channel.organization.stack_slug,
"alertGroupId": alert_group.public_primary_key,
"incidentId": alert_group.public_primary_key, # TODO: remove after hackathon app is deprecated
"status": alert_group.status,
"aps": aps,
},
extra = {
"orgId": alert_group.channel.organization.public_primary_key,
"orgName": alert_group.channel.organization.stack_slug,
"alertGroupId": alert_group.public_primary_key,
"status": alert_group.status,
"aps": aps,
}
logger.info(f"Sending push notification with message: {message}; thread-id: {thread_id}; extra: {extra}")
# TODO: rename category to USER_NEW_ALERT_GROUP
fcm_response = gcm_devices_to_notify.send_message(
message, thread_id=thread_id, category="USER_NEW_INCIDENT", extra=extra
)
gcm_devices_to_notify.send_message(
message,
thread_id=thread_id,
category="USER_NEW_INCIDENT", # TODO: rename to USER_NEW_ALERT_GROUP
extra={
"orgId": alert_group.channel.organization.public_primary_key,
"orgName": alert_group.channel.organization.stack_slug,
"alertGroupId": alert_group.public_primary_key,
"status": alert_group.status,
"aps": aps,
},
)
# NOTE: we may want to further handle the response from FCM, but for now lets simply log it out
# https://firebase.google.com/docs/cloud-messaging/http-server-ref#interpret-downstream
logger.info(f"FCM response was: {fcm_response}")

View file

@ -1,13 +1,12 @@
from django.conf import settings
from apps.mobile_app.fcm_relay import FCMRelayView
from apps.mobile_app.views import APNSDeviceAuthorizedViewSet, FCMDeviceAuthorizedViewSet, MobileAppAuthTokenAPIView
from apps.mobile_app.views import FCMDeviceAuthorizedViewSet, MobileAppAuthTokenAPIView
from common.api_helpers.optional_slash_router import OptionalSlashRouter, optional_slash_path
app_name = "mobile_app"
router = OptionalSlashRouter()
router.register("apns", APNSDeviceAuthorizedViewSet, basename="apns")
router.register("fcm", FCMDeviceAuthorizedViewSet, basename="fcm")
urlpatterns = [

View file

@ -1,4 +1,3 @@
from push_notifications.api.rest_framework import APNSDeviceAuthorizedViewSet as BaseAPNSDeviceAuthorizedViewSet
from push_notifications.api.rest_framework import GCMDeviceAuthorizedViewSet, GCMDeviceSerializer
from rest_framework import status
from rest_framework.exceptions import NotFound
@ -10,10 +9,6 @@ from apps.mobile_app.auth import MobileAppAuthTokenAuthentication, MobileAppVeri
from apps.mobile_app.models import MobileAppAuthToken
class APNSDeviceAuthorizedViewSet(BaseAPNSDeviceAuthorizedViewSet):
authentication_classes = (MobileAppAuthTokenAuthentication,)
class FCMDeviceAuthorizedViewSet(GCMDeviceAuthorizedViewSet):
class FCMDeviceSerializer(GCMDeviceSerializer):
"""

View file

@ -143,6 +143,15 @@ class SlackEventApiEndpointView(APIView):
if isinstance(payload, str):
payload = json.JSONDecoder().decode(payload)
logger.info(
"team_id: %s channel_id: %s user_id: %s command: %s event: %s",
payload.get("team_id"),
payload.get("channel_id"),
payload.get("user_id"),
payload.get("command"),
payload.get("event", {}).get("type"),
)
# Checking if it's repeated Slack request
if "HTTP_X_SLACK_RETRY_NUM" in request.META and int(request.META["HTTP_X_SLACK_RETRY_NUM"]) > 1:
logger.critical(

View file

@ -17,18 +17,7 @@ def sync_organization(organization):
rbac_is_enabled = client.is_rbac_enabled_for_organization()
organization.is_rbac_permissions_enabled = rbac_is_enabled
if organization.gcom_token:
gcom_client = GcomAPIClient(organization.gcom_token)
instance_info = gcom_client.get_instance_info(organization.stack_id)
if not instance_info or str(instance_info["orgId"]) != organization.org_id:
return
organization.stack_slug = instance_info["slug"]
organization.org_slug = instance_info["orgSlug"]
organization.org_title = instance_info["orgName"]
organization.region_slug = instance_info["regionSlug"]
organization.grafana_url = instance_info["url"]
organization.gcom_token_org_last_time_synced = timezone.now()
_sync_instance_info(organization)
api_users = client.get_users(rbac_is_enabled)
@ -53,6 +42,22 @@ def sync_organization(organization):
)
def _sync_instance_info(organization):
if organization.gcom_token:
gcom_client = GcomAPIClient(organization.gcom_token)
instance_info = gcom_client.get_instance_info(organization.stack_id)
if not instance_info or instance_info["orgId"] != organization.org_id:
return
organization.stack_slug = instance_info["slug"]
organization.org_slug = instance_info["orgSlug"]
organization.org_title = instance_info["orgName"]
organization.region_slug = instance_info["regionSlug"]
organization.grafana_url = instance_info["url"]
organization.gcom_token_org_last_time_synced = timezone.now()
def sync_users_and_teams(client, api_users, organization):
# check if api_users are shaped correctly. e.g. for paused instance, the response is not a list.
if not api_users or not isinstance(api_users, (tuple, list)):

View file

@ -0,0 +1,21 @@
from django.core.management.base import BaseCommand, CommandError
from apps.base.models.dynamic_setting import DynamicSetting
from apps.user_management.models.organization import Organization
class Command(BaseCommand):
note = "Note: you will also need to set the appropriate environment variables in your ./dev/.env.dev file."
help = f"Handles the database portion of enabling the mobile app related features. {note}"
def handle(self, *args, **options):
org = Organization.objects.first()
if not org:
raise CommandError("No organization exists. Have you enabled, and configured, the plugin?")
DynamicSetting.objects.update_or_create(
name="mobile_app_settings", defaults={"json_value": {"org_ids": [org.pk]}}
)
self.stdout.write(self.style.SUCCESS(f"Mobile app successfully enabled."))

View file

@ -33,12 +33,11 @@ django-log-request-id==1.6.0
django-polymorphic==3.0.0
django-rest-polymorphic==0.1.9
pre-commit==2.15.0
https://github.com/iskhakov/django-push-notifications/archive/refs/tags/3.0.0-fix-migration.tar.gz
https://github.com/grafana/django-push-notifications/archive/refs/tags/3.0.0-fix-migration.tar.gz
django-mirage-field==1.3.0
django-mysql==4.6.0
PyMySQL==1.0.2
psycopg2-binary==2.9.3
emoji==1.7.0
apns2==0.7.2
regex==2021.11.2
psutil==5.9.4

View file

@ -556,12 +556,6 @@ PUSH_NOTIFICATIONS_SETTINGS = {
"FCM_POST_URL": os.getenv("FCM_POST_URL", default="https://fcm.googleapis.com/fcm/send"),
"USER_MODEL": "user_management.User",
"UPDATE_ON_DUPLICATE_REG_ID": True,
# TODO: remove APNS related endpoints after the hackathon app is deprecated
"APNS_AUTH_KEY_PATH": os.environ.get("APNS_AUTH_KEY_PATH", None),
"APNS_TOPIC": os.environ.get("APNS_TOPIC", None),
"APNS_AUTH_KEY_ID": os.environ.get("APNS_AUTH_KEY_ID", None),
"APNS_TEAM_ID": os.environ.get("APNS_TEAM_ID", None),
"APNS_USE_SANDBOX": getenv_boolean("APNS_USE_SANDBOX", True),
}
FCM_RELAY_ENABLED = getenv_boolean("FCM_RELAY_ENABLED", default=False)

View file

@ -50,7 +50,7 @@ const NewScheduleSelector: FC<NewScheduleSelectorProps> = (props) => {
</VerticalGroup>
</HorizontalGroup>
<WithPermissionControl userAction={UserActions.SchedulesWrite}>
<Button variant="primary" icon="plus" onClick={getCreateScheduleClickHandler(ScheduleType.Calendar)}>
<Button variant="primary" icon="plus" onClick={getCreateScheduleClickHandler(ScheduleType.API)}>
Create
</Button>
</WithPermissionControl>

View file

@ -72,7 +72,7 @@ const SchedulesFilters = (props: SchedulesFiltersProps) => {
{ label: 'All', value: undefined },
{
label: 'Web',
value: ScheduleType.Calendar,
value: ScheduleType.API,
},
{
label: 'ICal',
@ -80,7 +80,7 @@ const SchedulesFilters = (props: SchedulesFiltersProps) => {
},
{
label: 'API',
value: ScheduleType.API,
value: ScheduleType.Calendar,
},
]}
value={value?.type}

View file

@ -39,6 +39,11 @@
opacity: 0.2;
}
.qr-code {
background-color: #fff;
margin-bottom: 12px;
}
.qr-loader {
position: absolute;
z-index: 10;

View file

@ -133,7 +133,7 @@ const MobileAppVerification = observer(({ userPk }: Props) => {
</Text>
<Text type="primary">Open Grafana IRM mobile application and scan this code to sync it with your account.</Text>
<div className={cx('u-width-100', 'u-flex', 'u-flex-center', 'u-position-relative')}>
<QRCode className={cx({ blurry: isQRBlurry })} value={QRCodeValue} />
<QRCode className={cx({ 'qr-code': true, blurry: isQRBlurry })} value={QRCodeValue} />
{isQRBlurry && <QRLoading />}
</div>
</VerticalGroup>

View file

@ -109,7 +109,7 @@ exports[`MobileAppVerification if we disconnect the app, it disconnects and fetc
class="u-width-100 u-flex u-flex-center u-position-relative"
>
<div
class="root root_bordered"
class="root qr-code root_bordered"
>
<svg
height="256"

View file

@ -7,7 +7,9 @@ import { useMediaQuery } from 'react-responsive';
import { Tabs, TabsContent } from 'containers/UserSettings/parts';
import { User as UserType } from 'models/user/user.types';
import { AppFeature } from 'state/features';
import { useStore } from 'state/useStore';
import { isUserActionAllowed, UserActions } from 'utils/authorization';
import { BREAKPOINT_TABS } from 'utils/consts';
import { UserSettingsTab } from './UserSettings.types';
@ -19,13 +21,12 @@ const cx = cn.bind(styles);
interface UserFormProps {
onHide: () => void;
id: UserType['pk'] | 'new';
showMobileAppScreen: boolean;
onCreate?: (data: UserType) => void;
onUpdate?: () => void;
tab?: UserSettingsTab;
}
const UserSettings = observer(({ id, showMobileAppScreen, onHide, tab = UserSettingsTab.UserInfo }: UserFormProps) => {
const UserSettings = observer(({ id, onHide, tab = UserSettingsTab.UserInfo }: UserFormProps) => {
const store = useStore();
const { userStore, teamStore } = store;
@ -59,7 +60,7 @@ const UserSettings = observer(({ id, showMobileAppScreen, onHide, tab = UserSett
!isDesktopOrLaptop,
isCurrent && teamStore.currentTeam?.slack_team_identity && !storeUser.slack_user_identity,
isCurrent && !storeUser.telegram_configuration,
showMobileAppScreen,
isCurrent && store.hasFeature(AppFeature.MobileApp) && isUserActionAllowed(UserActions.UserSettingsWrite),
];
return (

View file

@ -6,9 +6,9 @@ import { User } from 'models/user/user.types';
import { UserGroup } from 'models/user_group/user_group.types';
export enum ScheduleType {
'API',
'Ical',
'Calendar',
'Ical',
'API',
}
export interface RotationFormLiveParams {

View file

@ -276,7 +276,7 @@ class IncidentPage extends React.Component<IncidentPageProps, IncidentPageState>
</a>
{showLinkTo && (
<Button variant="primary" size="sm" icon="link" onClick={this.showAttachIncidentForm}>
Attach to another incident
Attach to another alert group
</Button>
)}
<PluginLink query={{ page: 'integrations', id: incident.alert_receive_channel.id }}>

View file

@ -209,7 +209,7 @@ class SchedulesPage extends React.Component<SchedulesPageProps, SchedulesPageSta
};
handleCreateSchedule = (data: Schedule) => {
if (data.type === ScheduleType.Calendar) {
if (data.type === ScheduleType.API) {
LocationHelper.update({ page: 'schedule', id: data.id }, 'partial');
}
};

View file

@ -21,7 +21,6 @@ import UserSettings from 'containers/UserSettings/UserSettings';
import { WithPermissionControl } from 'containers/WithPermissionControl/WithPermissionControl';
import { User as UserType } from 'models/user/user.types';
import { pages } from 'pages';
import { AppFeature } from 'state/features';
import { PageProps, WithStoreProps } from 'state/types';
import { withMobXProviderContext } from 'state/withStore';
import LocationHelper from 'utils/LocationHelper';
@ -117,11 +116,7 @@ class Users extends React.Component<UsersProps, UsersState> {
render() {
const { usersFilters, userPkToEdit, page, errorData } = this.state;
const {
store,
query,
query: { id },
} = this.props;
const { store, query } = this.props;
const { userStore } = store;
const columns = [
@ -162,8 +157,6 @@ class Users extends React.Component<UsersProps, UsersState> {
});
const { count, results } = userStore.getSearchResult();
const showMobileAppScreen: boolean =
id !== undefined && id !== 'me' && id === userStore.currentUserPk && store.hasFeature(AppFeature.MobileApp);
return (
<PluginPage pageNav={pages['users'].getPageNav()}>
@ -239,13 +232,7 @@ class Users extends React.Component<UsersProps, UsersState> {
/>
)}
</div>
{userPkToEdit && (
<UserSettings
id={userPkToEdit}
onHide={this.handleHideUserSettings}
showMobileAppScreen={showMobileAppScreen}
/>
)}
{userPkToEdit && <UserSettings id={userPkToEdit} onHide={this.handleHideUserSettings} />}
</div>
</>
)}

View file

@ -250,6 +250,7 @@
{ "action": "grafana-oncall-app.escalation-chains:read" },
{ "action": "grafana-oncall-app.schedules:read" },
{ "action": "grafana-oncall-app.schedules:write" },
{ "action": "grafana-oncall-app.schedules:export" },
{ "action": "grafana-oncall-app.chatops:read" },

View file

@ -122,7 +122,7 @@ export const UserActions: { [action in Actions]: UserAction } = {
EscalationChainsWrite: constructAction(Resource.ESCALATION_CHAINS, Action.WRITE, OrgRole.Admin),
SchedulesRead: constructAction(Resource.SCHEDULES, Action.READ, OrgRole.Viewer),
SchedulesWrite: constructAction(Resource.SCHEDULES, Action.WRITE, OrgRole.Admin),
SchedulesWrite: constructAction(Resource.SCHEDULES, Action.WRITE, OrgRole.Editor),
SchedulesExport: constructAction(Resource.SCHEDULES, Action.WRITE, OrgRole.Editor),
ChatOpsRead: constructAction(Resource.CHATOPS, Action.READ, OrgRole.Viewer),

View file

@ -5809,9 +5809,9 @@ decimal.js@^10.2.1:
integrity sha512-ic1yEvwT6GuvaYwBLLY6/aFFgjZdySKTE8en/fkU3QICTmRtgtSlFn0u0BXN06InZwtfCelR7j8LRiDI/02iGA==
decode-uri-component@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545"
integrity sha512-hjf+xovcEn31w/EUYdTXQh/8smFL/dzYjohQGEIgjyNavaJfBY2p5F527Bo1VPATxv0VYTUC2bOcXvqFwk78Og==
version "0.2.2"
resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.2.tgz#e69dbe25d37941171dd540e024c444cd5188e1e9"
integrity sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==
dedent@^0.7.0:
version "0.7.0"

View file

@ -34,6 +34,7 @@ from migrator.resources.users import (
def main() -> None:
session = APISession(PAGERDUTY_API_TOKEN)
session.timeout = 20
print("▶ Fetching users...")
users = session.list_all("users", params={"include[]": "notification_rules"})