commit
4e592c685e
19 changed files with 211 additions and 101 deletions
15
DEVELOPER.md
15
DEVELOPER.md
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -171,5 +171,3 @@ services:
|
|||
volumes:
|
||||
dbdata:
|
||||
rabbitmqdata:
|
||||
caddy_data:
|
||||
caddy_config:
|
||||
|
|
|
|||
|
|
@ -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_`.
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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}, "
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -53,4 +53,5 @@ export interface User {
|
|||
status?: number;
|
||||
link?: string;
|
||||
cloud_connection_status?: number;
|
||||
hidden_fields?: boolean;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue