Merge pull request #141 from grafana/dev

Merge dev to main
This commit is contained in:
Innokentii Konstantinov 2022-06-27 13:40:05 +04:00 committed by GitHub
commit 4e592c685e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 211 additions and 101 deletions

View file

@ -64,7 +64,7 @@ python manage.py createsuperuser
3. Launch the backend:
```bash
# Http server:
python manage.py runserver 8080
python manage.py runserver 0.0.0.0:8080
# Worker for background tasks (run it in the parallel terminal, don't forget to export .env there)
python manage.py start_celery
@ -203,11 +203,20 @@ Credentials: admin/admin
### Running tests locally
In the `engine` directory, with the `.env` vars exported and virtualenv activated
```bash
pytest
```
# in the engine directory, with the virtualenv activated
pytest --ds=settings.dev
You can also install `pytest.xdist` in your env and run tests in parallel:
```bash
pip install pytest.xdist
pytest -n4
```
## IDE Specific Instructions
### PyCharm

View file

@ -22,7 +22,7 @@ The OnCall developers and community are expected to follow the values defined in
## Projects
Each project must have a [`MAINTAINERS.md`][maintainers] file with at least one maintainer. Where a project has a release process, access and documentation should be such that more than one person can perform a release. Releases should be announced on the [announcements][https://github.com/grafana/oncall/discussions/categories/announcements] category at the GitHub Discussions. Any new projects should be first proposed on the [team mailing list][team] following the voting procedures listed below.
Each project must have a [`MAINTAINERS.md`][maintainers] file with at least one maintainer. Where a project has a release process, access and documentation should be such that more than one person can perform a release. Releases should be announced on the [announcements][announce] category at the GitHub Discussions. Any new projects should be first proposed on the [team mailing list][team] following the voting procedures listed below.
## Decision making
@ -57,7 +57,6 @@ The current team members are:
- Yulia Shanyrova — [@Ukochka](https://github.com/Ukochka) ([Grafana Labs](https://grafana.com/))
- Maxim Mordasov — [@maskin25](https://github.com/maskin25) ([Grafana Labs](https://grafana.com/))
- Julia Artyukhina — [@Ferril](https://github.com/Ferril) ([Grafana Labs](https://grafana.com/))
- Julia Artyukhina — [@Ferril](https://github.com/Ferril) ([Grafana Labs](https://grafana.com/))
Previous team members:
@ -67,7 +66,7 @@ Previous team members:
Maintainers lead one or more project(s) or parts thereof and serve as a point of conflict resolution amongst the contributors to this project. Ideally, maintainers are also team members, but exceptions are possible for suitable maintainers that, for whatever reason, are not yet team members.
Changes in maintainership have to be announced on the [announcemount][https://github.com/grafana/oncall/discussions/categories/announcements] category at the GitHub Discussions. They are decided by [rough consensus](#consensus) and formalized by changing the [`MAINTAINERS.md`][maintainers] file of the respective repository.
Changes in maintainership have to be announced on the [announcemount][announce] category at the GitHub Discussions. They are decided by [rough consensus](#consensus) and formalized by changing the [`MAINTAINERS.md`][maintainers] file of the respective repository.
Maintainers are granted commit rights to all projects covered by this governance.
@ -77,7 +76,7 @@ A project may have multiple maintainers, as long as the responsibilities are cle
### Technical decisions
Technical decisions that only affect a single project are made informally by the maintainer of this project, and [rough consensus](#consensus) is assumed. Technical decisions that span multiple parts of the project should be discussed and made on the the [GitHub Discussions][https://github.com/grafana/oncall/discussions].
Technical decisions that only affect a single project are made informally by the maintainer of this project, and [rough consensus](#consensus) is assumed. Technical decisions that span multiple parts of the project should be discussed and made on the the [GitHub Discussions][discussions].
Decisions are usually made by [rough consensus](#consensus). If no consensus can be reached, the matter may be resolved by [majority vote](#majority-vote).
@ -87,7 +86,7 @@ Changes to this document are made by Grafana Labs.
### Other matters
Any matter that needs a decision may be called to a vote by any member if they deem it necessary. For private or personnel matters, discussion and voting takes place on the [team mailing list][team], otherwise on the [GitHub Discussions][https://github.com/grafana/oncall/discussions].
Any matter that needs a decision may be called to a vote by any member if they deem it necessary. For private or personnel matters, discussion and voting takes place on the [team mailing list][team], otherwise on the [GitHub Discussions][discussions].
## Voting
@ -99,7 +98,7 @@ For all votes, voting must be open for at least one week. The end date should be
In all cases, all and only [team members](#team-members) are eligible to vote, with the sole exception of the forced removal of a team member, in which said member is not eligible to vote.
Discussion and votes on personnel matters (including but not limited to team membership and maintainership) are held in private on the [team mailing list][team]. All other discussion and votes are held in public on the [GitHub Discussions][https://github.com/grafana/oncall/discussions].
Discussion and votes on personnel matters (including but not limited to team membership and maintainership) are held in private on the [team mailing list][team]. All other discussion and votes are held in public on the [GitHub Discussions][discussions].
For public discussions, anyone interested is encouraged to participate. Formal power to object or vote is limited to [team members](#team-members).
@ -107,7 +106,7 @@ For public discussions, anyone interested is encouraged to participate. Formal p
The default decision making mechanism for the OnCall project is [rough][rough] consensus. This means that any decision on technical issues is considered supported by the [team][team] as long as nobody objects or the objection has been considered but not necessarily accommodated.
Silence on any consensus decision is implicit agreement and equivalent to explicit agreement. Explicit agreement may be stated at will. Decisions may, but do not need to be called out and put up for decision on the [GitHub Discussions][https://github.com/grafana/oncall/discussions] at any time and by anyone.
Silence on any consensus decision is implicit agreement and equivalent to explicit agreement. Explicit agreement may be stated at will. Decisions may, but do not need to be called out and put up for decision on the [GitHub Discussions][discussions] at any time and by anyone.
Consensus decisions can never override or go against the spirit of an earlier explicit vote.
@ -142,7 +141,7 @@ If there are multiple alternatives, members may vote for one or more alternative
The new member is
- added to the list of [team members](#team-members). Ideally by sending a PR of their own, at least approving said PR.
- announced on the [GitHub Discussions][https://github.com/grafana/oncall/discussions] by an existing team member. Ideally, the new member replies in this thread, acknowledging team membership.
- announced on the [GitHub Discussions][discussions] by an existing team member. Ideally, the new member replies in this thread, acknowledging team membership.
- added to the projects with commit rights.
- added to the [team mailing list][team].
@ -157,3 +156,11 @@ The ex-member is
- added to a list of previous members if they so choose.
If needed, we reserve the right to publicly announce removal.
[announce]: https://github.com/grafana/oncall/discussions/categories/announcements
[coc]: https://github.com/grafana/oncall/blob/dev/CODE_OF_CONDUCT.md
[maintainers]: https://github.com/grafana/oncall/blob/dev/MAINTAINERS.md
[rough]: https://tools.ietf.org/html/rfc7282
[discussions]: https://github.com/grafana/oncall/discussions/
[team]: TBD

View file

@ -5,7 +5,10 @@ The following are the main/default maintainers:
Some parts of the codebase have other maintainers, the package paths also include all sub-packages:
n/a
Some parts of the codebase have other maintainers:
- `docs`:
- Eve Meelan - [@Eve832](https://github.com/Eve832) ([Grafana Labs](https://grafana.com/))
- Alyssa Wada - [@alyssawada](https://github.com/alyssawada) ([Grafana Labs](https://grafana.com/))
For the sake of brevity, not all subtrees are explicitly listed. Due to the
size of this repository, the natural changes in focus of maintainers over time,

View file

@ -46,7 +46,7 @@ docker-compose --env-file .env_hobby -f docker-compose.yml up --build -d
docker-compose --env-file .env_hobby -f docker-compose.yml run engine python manage.py issue_invite_for_the_frontend --override
```
5. Go to [OnCall Plugin Configuration](http://localhost:3000/plugins/grafana-oncall-app) (or find OnCall plugin in configuration->plugins) and connect OnCall _plugin_ with OnCall _backend_:
5. Go to [OnCall Plugin Configuration](http://localhost:3000/plugins/grafana-oncall-app), using log in credentials as defined above: `admin`/`admin` (or find OnCall plugin in configuration->plugins) and connect OnCall _plugin_ with OnCall _backend_:
```
Invite token: ^^^ from the previous step.
OnCall backend URL: http://engine:8080

View file

@ -171,5 +171,3 @@ services:
volumes:
dbdata:
rabbitmqdata:
caddy_data:
caddy_config:

View file

@ -8,26 +8,37 @@ title: Open Source
weight: 300
---
# Open Source
# Grafana OnCall open source guide
We prepared three environments for OSS users:
- **Hobby** environment for local usage & playing around: [README.md](https://github.com/grafana/oncall#getting-started).
Grafana OnCall is a developer-friendly incident response tool that's available to Grafana open source and Grafana Cloud users. The OSS version of Grafana OnCall provides the same reliable on-call management solution along with the flexibility of a self-managed environment.
This guide describes the necessary installation and configuration steps needed to configure OSS Grafana OnCall.
The intended audience for this guide includes:
- Grafana open source admins who are responsible for deploying and configuring Grafana OnCall.
- Grafana open source users who need to configure SMS and phone notifications using Grafana Cloud.
## Install Grafana OnCall OSS
There are three Grafana OnCall OSS environments available:
- **Hobby** playground environment for local usage: [README.md](https://github.com/grafana/oncall#getting-started)
- **Development** environment for contributors: [DEVELOPER.md](https://github.com/grafana/oncall/blob/dev/DEVELOPER.md)
- **Production** environment for reliable cloud installation using Helm: [Production Environment](#production-environment)
- **Production** environment for reliable Cloud installation: [Production Environment](#production-environment)
## Production Environment
For detailed installation instructions and additional resources, refer to the OSS Grafana OnCall [README.md](https://github.com/grafana/oncall#getting-started)
We prepared the helm chart for production environment: https://github.com/grafana/oncall/tree/dev/helm/oncall
For more information on production environment installation, refer to the following OSS Grafana OnCall [production environment helm chart](https://github.com/grafana/oncall/helm)
## Slack Setup
## Configure Slack for Grafana OnCall OSS
Grafana OnCall Slack integration use a lot of Slack API features:
- Subscription on Slack events requires OnCall to be externally available and provide https endpoint.
- You will need to register new Slack App.
The Slack integration for Grafana OnCall leverages Slack API features to provide a customizable and useful integration. Refer to the following steps to configure the Slack integration:
1. Make sure your OnCall is up and running.
1. Ensure your Grafana OnCall environment is up and running.
1. Grafana OnCall must be accessible through HTTPS. For development purposes, use [localtunnel](https://github.com/localtunnel/localtunnel). For production purposes, consider establishing a proper web server with HTTPS termination.
For localtunnel, refer to the following configuration:
2. You need OnCall to be accessible through https. For development purposes we suggest using [localtunnel](https://github.com/localtunnel/localtunnel). For production purposes please consider setting up proper web server with HTTPS termination. For localtunnel:
```bash
# Choose the unique prefix instead of pretty-turkey-83
# Localtunnel will generate an url, e.g. https://pretty-turkey-83.loca.lt
@ -35,15 +46,15 @@ Grafana OnCall Slack integration use a lot of Slack API features:
lt --port 8080 -s pretty-turkey-83 --print-requests
```
3. If you use localtunnel, open your external URL and click "Continue" to allow requests to bypass the warning page.
1. If using localtunnel, open your external URL and click **Continue** to allow requests to bypass the warning page.
4. [Create a Slack Workspace](https://slack.com/create) for development, or use your company workspace.
1. [Create a Slack Workspace](https://slack.com/create) for development, or use your company workspace.
5. Go to https://api.slack.com/apps and click Create New App button
1. Go to https://api.slack.com/apps and click **Create an App** .
6. Select `From an app manifest` option and choose the right workspace
1. Select `From an app manifest` option and select your workspace.
7. Copy and paste the following block with the correct <YOUR_BOT_NAME> and <ONCALL_ENGINE_PUBLIC_URL> fields
1. Replace the text with the following YAML code block . Be sure to replace `<YOUR_BOT_NAME>` and `<ONCALL_ENGINE_PUBLIC_URL>` fields with the appropriate information.
```yaml
_metadata:
@ -133,7 +144,7 @@ lt --port 8080 -s pretty-turkey-83 --print-requests
socket_mode_enabled: false
```
6. Go to your "OnCall" -> "Env Variables" and set:
1. Set environment variables by navigating to your Grafana OnCall, then click **Env Variables** and set the following:
```
SLACK_CLIENT_OAUTH_ID = Basic Information -> App Credentials -> Client ID
SLACK_CLIENT_OAUTH_SECRET = Basic Information -> App Credentials -> Client Secret
@ -141,30 +152,40 @@ lt --port 8080 -s pretty-turkey-83 --print-requests
SLACK_INSTALL_RETURN_REDIRECT_HOST = << OnCall external URL >>
```
7. Go to "OnCall" -> "ChatOps" -> "Slack" and install Slack Integration
1. In OnCall, navigate to **ChatOps**, select Slack and click **Install Slack integration**.
8. All set!
1. Configure additional Slack settings.
## Telegram Setup
## Configure Telegram for Grafana OnCall OSS
- Telegram integrations requires OnCall to be externally available and provide https endpoint.
- Telegram integration in OnCall is designed for collaborative team work. It requires Telegram Group and a Telegram Channel (private) for alerts.
The Telegram integration for Grafana OnCall is designed for collaborative team work and improved incident response. Refer to the following steps to configure the Telegram integration:
1. Make sure your OnCall is up and running.
1. Ensure your OnCall environment is up and running.
2. Respectfully ask [BotFather](https://t.me/BotFather) for a key, put it in `TELEGRAM_TOKEN` in "OnCall" -> "Env Variables".
1. Request [BotFather](https://t.me/BotFather) for a key, then add your key in `TELEGRAM_TOKEN` in your Grafana OnCall **Env Variables**.
3. Set `TELEGRAM_WEBHOOK_HOST` with your external url for OnCall.
1. Set `TELEGRAM_WEBHOOK_HOST` with your external URL for your Grafana OnCall.
4. Go to "OnCall" -> "ChatOps" -> Telegram and enjoy!
1. From the **ChatOps** tab in Grafana OnCall, click **Telegram**.
## Grafana OSS-Cloud Setup
## Connect Grafana Cloud to Grafana OnCall OSS
Grafana OSS could be connected to Grafana Cloud for heartbeat and SMS / Phone Calls. We tried our best in making Grafana OSS <-> Cloud self-explanatory. Check "Cloud" page in your OSS OnCall instance.
Open source Grafana OnCall can be connected to Grafana Cloud to configure a variety of notifications.
Please note that it's possible either to use Grafana Cloud either Twilio for SMS/Phone calls.
The benefits of connecting to Grafana Cloud include:
- Heartbeat notification
- SMS for user notifications
- Phone calls for user notifications.
## Twilio Setup
To connect to Grafana Cloud, refer to the **Cloud** page in your OSS Grafana OnCall instance.
1. Make sure Grafana OSS <-> Cloud connector is disabled. Set `GRAFANA_CLOUD_NOTIFICATIONS_ENABLED` as False.
2. Check "OnCall" -> "Env Variables" and set all variables starting with `TWILIO_`
>**NOTE:** As an alternative option to Grafana Cloud, phone call and SMS notifications can be configured using Twilio.
## Connect Twilio for Grafana OnCall OSS
Grafana OnCall supports Twilio SMS and phone call notifications delivery. If you prefer to configure SMS and phone call notifications using Twilio, complete the following steps:
1. Set `GRAFANA_CLOUD_NOTIFICATIONS_ENABLED` as **False** to ensure the Grafana OSS <-> Cloud connector is disabled.
1. From your **OnCall** environment, select **Env Variables** and configure all variables starting with `TWILIO_`.

View file

@ -3,6 +3,7 @@ from rest_framework import serializers
from apps.api.serializers.user_group import UserGroupSerializer
from apps.schedules.ical_utils import list_users_to_notify_from_ical
from apps.schedules.models import OnCallSchedule
from apps.schedules.tasks import schedule_notify_about_empty_shifts_in_schedule, schedule_notify_about_gaps_in_schedule
from common.api_helpers.custom_fields import TeamPrimaryKeyRelatedField
from common.api_helpers.mixins import EagerLoadingMixin
@ -83,3 +84,14 @@ class ScheduleBaseSerializer(EagerLoadingMixin, serializers.ModelSerializer):
created_schedule.check_gaps_for_next_week()
schedule_notify_about_gaps_in_schedule.apply_async((created_schedule.pk,))
return created_schedule
class ScheduleFastSerializer(serializers.ModelSerializer):
id = serializers.CharField(read_only=True, source="public_primary_key")
class Meta:
model = OnCallSchedule
fields = [
"id",
"name",
]

View file

@ -119,6 +119,7 @@ class UserHiddenFieldsSerializer(UserSerializer):
for field in ret:
if field not in self.available_for_all_roles_fields:
ret[field] = "******"
ret["hidden_fields"] = True
return ret

View file

@ -17,6 +17,7 @@ from rest_framework.views import Response
from rest_framework.viewsets import ModelViewSet
from apps.api.permissions import MODIFY_ACTIONS, READ_ACTIONS, ActionPermission, AnyRole, IsAdmin, IsAdminOrEditor
from apps.api.serializers.schedule_base import ScheduleFastSerializer
from apps.api.serializers.schedule_polymorphic import (
PolymorphicScheduleCreateSerializer,
PolymorphicScheduleSerializer,
@ -31,10 +32,17 @@ from apps.slack.models import SlackChannel
from apps.slack.tasks import update_slack_user_group_for_schedules
from apps.user_management.organization_log_creator import OrganizationLogType, create_organization_log
from common.api_helpers.exceptions import BadRequest, Conflict
from common.api_helpers.mixins import CreateSerializerMixin, PublicPrimaryKeyMixin, UpdateSerializerMixin
from common.api_helpers.mixins import (
CreateSerializerMixin,
PublicPrimaryKeyMixin,
ShortSerializerMixin,
UpdateSerializerMixin,
)
class ScheduleView(PublicPrimaryKeyMixin, CreateSerializerMixin, UpdateSerializerMixin, ModelViewSet):
class ScheduleView(
PublicPrimaryKeyMixin, ShortSerializerMixin, CreateSerializerMixin, UpdateSerializerMixin, ModelViewSet
):
authentication_classes = (PluginAuthentication,)
permission_classes = (IsAuthenticated, ActionPermission)
action_permissions = {
@ -56,6 +64,7 @@ class ScheduleView(PublicPrimaryKeyMixin, CreateSerializerMixin, UpdateSerialize
serializer_class = PolymorphicScheduleSerializer
create_serializer_class = PolymorphicScheduleCreateSerializer
update_serializer_class = PolymorphicScheduleUpdateSerializer
short_serializer_class = ScheduleFastSerializer
@cached_property
def can_update_user_groups(self):
@ -80,19 +89,22 @@ class ScheduleView(PublicPrimaryKeyMixin, CreateSerializerMixin, UpdateSerialize
return context
def get_queryset(self):
is_short_request = self.request.query_params.get("short", "false") == "true"
organization = self.request.auth.organization
slack_channels = SlackChannel.objects.filter(
slack_team_identity=organization.slack_team_identity,
slack_id=OuterRef("channel"),
)
queryset = OnCallSchedule.objects.filter(
organization=organization,
team=self.request.user.current_team,
).annotate(
slack_channel_name=Subquery(slack_channels.values("name")[:1]),
slack_channel_pk=Subquery(slack_channels.values("public_primary_key")[:1]),
)
queryset = self.serializer_class.setup_eager_loading(queryset)
if not is_short_request:
slack_channels = SlackChannel.objects.filter(
slack_team_identity=organization.slack_team_identity,
slack_id=OuterRef("channel"),
)
queryset = queryset.annotate(
slack_channel_name=Subquery(slack_channels.values("name")[:1]),
slack_channel_pk=Subquery(slack_channels.values("public_primary_key")[:1]),
)
queryset = self.serializer_class.setup_eager_loading(queryset)
return queryset
def get_object(self):

View file

@ -197,13 +197,13 @@ class UserNotificationPolicyLogRecord(models.Model):
elif notification_channel is None:
result += f"failed to notify {user_verbal}. Phone number is not verified"
if self.notification_error_code == UserNotificationPolicyLogRecord.ERROR_NOTIFICATION_NOT_ABLE_TO_SEND_SMS:
result += f"Amixr was not able to send an SMS to {user_verbal}"
result += f"OnCall was not able to send an SMS to {user_verbal}"
elif self.notification_error_code == UserNotificationPolicyLogRecord.ERROR_NOTIFICATION_NOT_ABLE_TO_CALL:
result += f"Amixr was not able to call to {user_verbal}"
result += f"OnCall was not able to call to {user_verbal}"
elif (
self.notification_error_code == UserNotificationPolicyLogRecord.ERROR_NOTIFICATION_NOT_ABLE_TO_SEND_MAIL
):
result += f"Amixr was not able to send an email to {user_verbal}"
result += f"OnCall was not able to send an email to {user_verbal}"
elif (
self.notification_error_code
== UserNotificationPolicyLogRecord.ERROR_NOTIFICATION_POSTING_TO_SLACK_IS_DISABLED
@ -314,7 +314,8 @@ class UserNotificationPolicyLogRecord(models.Model):
@receiver(post_save, sender=UserNotificationPolicyLogRecord)
def listen_for_usernotificationpolicylogrecord_model_save(sender, instance, created, *args, **kwargs):
alert_group_pk = instance.alert_group.drop_cached_after_resolve_report_json()
instance.alert_group.drop_cached_after_resolve_report_json()
alert_group_pk = instance.alert_group.pk
if instance.type != UserNotificationPolicyLogRecord.TYPE_PERSONAL_NOTIFICATION_FINISHED:
logger.debug(
f"send_update_log_report_signal for alert_group {alert_group_pk}, "

View file

@ -69,7 +69,7 @@ class CloudConnector(models.Model):
page = 1
while fetch_next_page:
try:
url = urljoin(users_url, f"?page={page}&?short=true")
url = urljoin(users_url, f"?page={page}&short=true&roles=0&roles=1")
r = requests.get(url, headers={"AUTHORIZATION": api_token}, timeout=5)
if r.status_code != 200:
logger.warning(
@ -115,7 +115,7 @@ class CloudConnector(models.Model):
logger.warning(f"Unable to sync_user_with cloud user_id {user.id}. GRAFANA_CLOUD_ONCALL_TOKEN is not set")
error_msg = "GRAFANA_CLOUD_ONCALL_TOKEN is not set"
else:
url = urljoin(GRAFANA_CLOUD_ONCALL_API_URL, f"api/v1/users/?email={user.email}")
url = urljoin(GRAFANA_CLOUD_ONCALL_API_URL, f"api/v1/users/?email={user.email}&roles=0&roles=1&short=true")
try:
r = requests.get(url, headers={"AUTHORIZATION": api_token}, timeout=5)
if r.status_code != 200:

View file

@ -3,6 +3,8 @@ from django.urls import reverse
from rest_framework import status
from rest_framework.test import APIClient
from common.constants.role import Role
@pytest.fixture()
def user_public_api_setup(
@ -140,3 +142,38 @@ def test_forbidden_access(
response = client.get(url, format="json", HTTP_AUTHORIZATION=another_org_token)
assert response.status_code == status.HTTP_404_NOT_FOUND
@pytest.mark.django_db
def test_get_users_list_all_role_users(
user_public_api_setup,
make_user_for_organization,
):
organization, admin, token, _, _ = user_public_api_setup
editor = make_user_for_organization(organization, role=Role.EDITOR)
viewer = make_user_for_organization(organization, role=Role.VIEWER)
client = APIClient()
url = reverse("api-public:users-list")
response = client.get(f"{url}?short=true", format="json", HTTP_AUTHORIZATION=token)
expected_users = [(admin, "admin"), (editor, "editor"), (viewer, "viewer")]
expected_response = {
"count": 3,
"next": None,
"previous": None,
"results": [
{
"id": user.public_primary_key,
"email": user.email,
"username": user.username,
"role": role,
"is_phone_number_verified": False,
}
for user, role in expected_users
],
}
assert response.status_code == status.HTTP_200_OK
assert response.json() == expected_response

View file

@ -1,3 +1,4 @@
from django_filters import rest_framework as filters
from rest_framework.decorators import action
from rest_framework.exceptions import NotFound
from rest_framework.permissions import IsAuthenticated
@ -16,6 +17,20 @@ from common.api_helpers.paginators import HundredPageSizePaginator
from common.constants.role import Role
class UserFilter(filters.FilterSet):
"""
https://django-filter.readthedocs.io/en/master/guide/rest_framework.html
"""
email = filters.CharFilter(field_name="email", lookup_expr="iexact")
roles = filters.MultipleChoiceFilter(field_name="role", choices=Role.choices())
username = filters.CharFilter(field_name="username", lookup_expr="iexact")
class Meta:
model = User
fields = ["email", "roles", "username"]
class UserView(RateLimitHeadersMixin, ShortSerializerMixin, ReadOnlyModelViewSet):
authentication_classes = (ApiTokenAuthentication,)
permission_classes = (IsAuthenticated,)
@ -25,23 +40,17 @@ class UserView(RateLimitHeadersMixin, ShortSerializerMixin, ReadOnlyModelViewSet
serializer_class = UserSerializer
short_serializer_class = FastUserSerializer
filterset_class = UserFilter
filter_backends = (filters.DjangoFilterBackend,)
throttle_classes = [UserThrottle]
def get_queryset(self):
username = self.request.query_params.get("username")
email = self.request.query_params.get("email")
is_short_request = self.request.query_params.get("short", "false") == "true"
queryset = self.request.auth.organization.users.filter(role__in=[Role.ADMIN, Role.EDITOR]).distinct()
if username is not None:
queryset = queryset.filter(username=username)
if email is not None:
queryset = queryset.filter(email=email)
queryset = self.request.auth.organization.users.all()
if not is_short_request:
queryset = self.serializer_class.setup_eager_loading(queryset)
queryset = self.filter_queryset(queryset)
return queryset.order_by("id")
def get_object(self):

View file

@ -283,6 +283,7 @@ class OnCallScheduleICal(OnCallSchedule):
self.save(update_fields=["cached_ical_file_primary", "prev_ical_file_primary", "ical_file_error_primary"])
def _refresh_overrides_ical_file(self):
self.prev_ical_file_overrides = self.cached_ical_file_overrides
if self.ical_url_overrides is not None:
self.cached_ical_file_overrides, self.ical_file_error_overrides = fetch_ical_file_or_get_error(
self.ical_url_overrides,

View file

@ -11,6 +11,7 @@ import PluginLink from 'components/PluginLink/PluginLink';
import TimeRange from 'components/TimeRange/TimeRange';
import Timeline from 'components/Timeline/Timeline';
import GSelect from 'containers/GSelect/GSelect';
import RemoteSelect from 'containers/RemoteSelect/RemoteSelect';
import UserTooltip from 'containers/UserTooltip/UserTooltip';
import { WithPermissionControl } from 'containers/WithPermissionControl/WithPermissionControl';
import { ActionDTO } from 'models/action';
@ -271,14 +272,15 @@ export class EscalationPolicy extends React.Component<EscalationPolicyProps, any
return (
<WithPermissionControl key="notify_schedule" disableByPaywall userAction={UserAction.UpdateEscalationPolicies}>
<GSelect
modelName="scheduleStore"
displayField="name"
valueField="id"
placeholder="Select Schedule"
<RemoteSelect
showSearch={false}
className={cx('select', 'control')}
value={notify_schedule}
valueField="id"
onChange={this._getOnChangeHandler('notify_schedule')}
href={'/schedules/?short=true'}
fieldToShow="name"
placeholder="Select Schedule"
/>
</WithPermissionControl>
);

View file

@ -726,7 +726,7 @@ class AlertRules extends React.Component<AlertRulesProps, AlertRulesState> {
/>
</WithPermissionControl>
</div>
<div>{this.renderChannelFilterButtons(channelFilterId, index)}</div>
<div onClick={(e) => e.stopPropagation()}>{this.renderChannelFilterButtons(channelFilterId, index)}</div>
</div>
);
};

View file

@ -53,4 +53,5 @@ export interface User {
status?: number;
link?: string;
cloud_connection_status?: number;
hidden_fields?: boolean;
}

View file

@ -232,7 +232,7 @@ const CloudPage = observer((props: CloudPageProps) => {
<span className={cx('heart-icon')}>
<HeartIcon />
</span>
Monitor cloud instance with heartbeat
Monitor instance with heartbeat
</Text.Title>
<Text type="secondary">
Once connected, current OnCall instance will send heartbeats every 3 minutes to the cloud Instance. If no
@ -268,7 +268,7 @@ const CloudPage = observer((props: CloudPageProps) => {
<div style={{ width: '100%' }}>
<Text type="secondary">
{
'Ask your users to sign up in Grafana Cloud, verify phone number and feel free to set up SMS & phone call notificaitons in personal settings! Only users with Admin or Editor role will be synced.'
'Ask your users to sign up in Grafana Cloud, verify phone number and feel free to set up SMS & phone call notifications in personal settings! Only users with Admin or Editor role will be synced.'
}
</Text>
@ -349,7 +349,7 @@ const CloudPage = observer((props: CloudPageProps) => {
<span className={cx('heart-icon')}>
<HeartIcon />
</span>
Monitor cloud instance with heartbeat
Monitor instance with heartbeat
</Text.Title>
<Text type="secondary">
Once connected, current OnCall instance will send heartbeats every 3 minutes to the cloud Instance. If no

View file

@ -59,7 +59,6 @@ class Users extends React.Component<UsersProps, UsersState> {
store,
query: { p },
} = this.props;
this.setState({ page: p ? Number(p) : 1 }, this.updateUsers);
this.parseParams();
@ -292,37 +291,34 @@ class Users extends React.Component<UsersProps, UsersState> {
};
renderNote = (user: UserType) => {
const { store } = this.props;
let phone_verified;
let phone_verified_message;
if (store.hasFeature(AppFeature.CloudNotifications)) {
// If cloud notifications is enabled show message about its status, not local phone verification.
if (user.hidden_fields === true) {
return null;
}
let phone_verified = user.verified_phone_number !== null;
let phone_not_verified_message = 'Phone not verified';
if (user.cloud_connection_status !== null) {
phone_verified = false;
switch (user.cloud_connection_status) {
case 0:
phone_verified = false;
phone_verified_message = 'Cloud is not synced';
phone_not_verified_message = 'Cloud is not synced';
break;
case 1:
phone_verified = false;
phone_verified_message = 'User not matched with cloud';
phone_not_verified_message = 'User not matched with cloud';
break;
case 2:
phone_verified = false;
phone_verified_message = 'Phone number is not verified in Grafana Cloud';
phone_not_verified_message = 'Phone number is not verified in Grafana Cloud';
break;
case 3:
phone_verified = false;
phone_verified_message = 'Phone number is verified in Grafana Cloud';
phone_verified = true;
break;
}
} else {
phone_verified = user.verified_phone_number;
phone_verified_message = 'Phone not verified';
}
if (!phone_verified || !user.slack_user_identity || !user.telegram_configuration) {
let texts = [];
if (!phone_verified) {
texts.push(phone_verified_message);
texts.push(phone_not_verified_message);
}
if (!user.slack_user_identity) {
texts.push('Slack not verified');