├── .cursor └── rules │ └── planit-mode.mdc ├── .dockerignore ├── .gitattributes ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── 0-bug-report-template.yml │ ├── 1-feature-request-template.yml │ └── config.yml ├── actions │ ├── build-sign-and-package-plugin │ │ └── action.yml │ ├── install-frontend-dependencies │ │ └── action.yml │ ├── set-engine-version-in-settings │ │ └── action.yml │ └── setup-python │ │ └── action.yml ├── helm-ci.yml ├── pull_request_template.md ├── release.yml ├── verify-public-docs-updated.sh └── workflows │ ├── build-engine-docker-image-and-publish-to-dockerhub.yml │ ├── e2e-tests.yml │ ├── linting-and-tests.yml │ ├── on-commits-to-dev.yml │ ├── on-helm-release-pr-merged.yml │ ├── on-issue-closed.yml │ ├── on-issue-creation.yml │ ├── on-pull-requests.yml │ ├── on-release-published.yml │ ├── publish-technical-documentation-next.yml │ ├── publish-technical-documentation-release.yml │ ├── snyk-security-scan.yml │ ├── triage-stale-pull-requests.yml │ ├── update-make-docs.yml │ ├── verify-public-docs-updated.yml │ └── verify-release-label-added.yml ├── .gitignore ├── .markdownlint.json ├── .markdownlintignore ├── .nvmrc ├── .pre-commit-config.yaml ├── .prettierignore ├── .prettierrc.js ├── .tilt ├── backend │ └── Tiltfile ├── deps │ └── Tiltfile ├── plugin │ └── Tiltfile └── tests │ └── Tiltfile ├── .yamllint.yml ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.md ├── Tiltfile ├── dev ├── .env.dev.example ├── .env.mysql.dev ├── .env.postgres.dev ├── .env.sqlite.dev ├── .gitignore ├── README.md ├── add_env_var.sh ├── frontend_guidelines.md ├── grafana │ └── provisioning │ │ ├── datasources │ │ └── automatic.yml │ │ └── plugins │ │ └── grafana-oncall-app-provisioning.yaml ├── helm-local.yml ├── kind-config.yaml ├── kind.yml ├── prometheus.yml └── scripts │ ├── .isort.cfg │ ├── generate-fake-data │ ├── README.md │ ├── main.py │ └── requirements.txt │ └── restart_backend_plugin.sh ├── docker-compose-developer.yml ├── docker-compose-mysql-rabbitmq.yml ├── docker-compose.yml ├── docs ├── .markdownlint.json ├── Makefile ├── README.md ├── docs.mk ├── img │ ├── GH_discussions.png │ ├── architecture_diagram.png │ ├── logo.png │ ├── screenshot.png │ ├── screenshot_mobile.png │ └── slack.png ├── make-docs ├── sources │ ├── _index.md │ ├── configure │ │ ├── _index.md │ │ ├── escalation-chains-and-routes │ │ │ └── index.md │ │ ├── integrations │ │ │ ├── _index.md │ │ │ ├── integration-management │ │ │ │ └── index.md │ │ │ ├── labels │ │ │ │ └── index.md │ │ │ ├── outgoing-webhooks │ │ │ │ └── index.md │ │ │ └── references │ │ │ │ ├── _index.md │ │ │ │ ├── alertmanager │ │ │ │ └── index.md │ │ │ │ ├── amazon-sns │ │ │ │ └── index.md │ │ │ │ ├── appdynamics │ │ │ │ └── index.md │ │ │ │ ├── datadog │ │ │ │ └── index.md │ │ │ │ ├── elastalert │ │ │ │ └── index.md │ │ │ │ ├── fabric │ │ │ │ └── index.md │ │ │ │ ├── grafana-alerting │ │ │ │ └── index.md │ │ │ │ ├── inbound-email │ │ │ │ └── index.md │ │ │ │ ├── jira │ │ │ │ └── index.md │ │ │ │ ├── kapacitor │ │ │ │ └── index.md │ │ │ │ ├── manual │ │ │ │ └── index.md │ │ │ │ ├── newrelic │ │ │ │ └── index.md │ │ │ │ ├── pingdom │ │ │ │ └── index.md │ │ │ │ ├── prtg │ │ │ │ └── index.md │ │ │ │ ├── sentry │ │ │ │ └── index.md │ │ │ │ ├── servicenow │ │ │ │ └── index.md │ │ │ │ ├── slack │ │ │ │ └── index.md │ │ │ │ ├── stackdriver │ │ │ │ └── index.md │ │ │ │ ├── uptimerobot │ │ │ │ └── index.md │ │ │ │ ├── webhook │ │ │ │ └── index.md │ │ │ │ ├── zabbix │ │ │ │ └── index.md │ │ │ │ └── zendesk │ │ │ │ └── index.md │ │ ├── jinja2-templating │ │ │ ├── _index.md │ │ │ └── advanced-templates │ │ │ │ └── index.md │ │ └── live-call-routing │ │ │ └── index.md │ ├── intro │ │ └── _index.md │ ├── manage │ │ ├── _index.md │ │ ├── insights-and-metrics │ │ │ └── index.md │ │ ├── mobile-app │ │ │ ├── _index.md │ │ │ ├── alert-groups-feed │ │ │ │ └── index.md │ │ │ ├── installation-and-setup │ │ │ │ └── index.md │ │ │ ├── on-call-status-and-shifts │ │ │ │ └── index.md │ │ │ └── push-notifications │ │ │ │ └── index.md │ │ ├── notify │ │ │ ├── _index.md │ │ │ ├── mattermost │ │ │ │ └── index.md │ │ │ ├── ms-teams │ │ │ │ └── index.md │ │ │ ├── phone-calls-sms │ │ │ │ └── index.md │ │ │ ├── slack │ │ │ │ └── index.md │ │ │ ├── telegram │ │ │ │ └── index.md │ │ │ └── webhook │ │ │ │ └── index.md │ │ ├── on-call-schedules │ │ │ ├── _index.md │ │ │ ├── api-terraform-schedule │ │ │ │ └── index.md │ │ │ ├── ical-schedules │ │ │ │ └── index.md │ │ │ ├── shift-swaps │ │ │ │ └── index.md │ │ │ └── web-schedule │ │ │ │ └── index.md │ │ └── user-and-team-management │ │ │ ├── _index.md │ │ │ └── convert_plugin_json_rbac_roles_into_markdown_table.py │ ├── mobile-app │ │ ├── on-call-status-and-shifts │ │ │ └── _index.md │ │ └── schedules │ │ │ └── _index.md │ ├── oncall-api-reference │ │ ├── _index.md │ │ ├── alertgroups.md │ │ ├── alerts.md │ │ ├── escalation.md │ │ ├── escalation_chains.md │ │ ├── escalation_policies.md │ │ ├── integrations.md │ │ ├── on_call_shifts.md │ │ ├── organizations.md │ │ ├── outgoing_webhooks.md │ │ ├── personal_notification_rules.md │ │ ├── resolution_notes.md │ │ ├── routes.md │ │ ├── schedules.md │ │ ├── shift_swaps.md │ │ ├── slack_channels.md │ │ ├── teams.md │ │ ├── user_groups.md │ │ └── users.md │ └── set-up │ │ ├── _index.md │ │ ├── get-started │ │ └── index.md │ │ ├── migration-from-other-tools │ │ └── index.md │ │ └── open-source │ │ └── index.md └── variables.mk ├── engine ├── .dockerignore ├── .gitignore ├── Dockerfile ├── apps │ ├── __init__.py │ ├── alerts │ │ ├── __init__.py │ │ ├── constants.py │ │ ├── escalation_snapshot │ │ │ ├── __init__.py │ │ │ ├── escalation_snapshot_mixin.py │ │ │ ├── serializers │ │ │ │ ├── __init__.py │ │ │ │ ├── channel_filter_snapshot.py │ │ │ │ ├── escalation_chain_snapshot.py │ │ │ │ ├── escalation_policy_snapshot.py │ │ │ │ └── escalation_snapshot.py │ │ │ ├── snapshot_classes │ │ │ │ ├── __init__.py │ │ │ │ ├── channel_filter_snapshot.py │ │ │ │ ├── escalation_chain_snapshot.py │ │ │ │ ├── escalation_policy_snapshot.py │ │ │ │ └── escalation_snapshot.py │ │ │ └── utils.py │ │ ├── grafana_alerting_sync_manager │ │ │ ├── __init__.py │ │ │ └── grafana_alerting_sync.py │ │ ├── incident_appearance │ │ │ ├── __init__.py │ │ │ ├── renderers │ │ │ │ ├── __init__.py │ │ │ │ ├── base_renderer.py │ │ │ │ ├── classic_markdown_renderer.py │ │ │ │ ├── constants.py │ │ │ │ ├── phone_call_renderer.py │ │ │ │ ├── slack_renderer.py │ │ │ │ ├── sms_renderer.py │ │ │ │ ├── telegram_renderer.py │ │ │ │ └── web_renderer.py │ │ │ └── templaters │ │ │ │ ├── __init__.py │ │ │ │ ├── alert_templater.py │ │ │ │ ├── classic_markdown_templater.py │ │ │ │ ├── phone_call_templater.py │ │ │ │ ├── slack_templater.py │ │ │ │ ├── sms_templater.py │ │ │ │ ├── telegram_templater.py │ │ │ │ └── web_templater.py │ │ ├── incident_log_builder │ │ │ ├── __init__.py │ │ │ └── incident_log_builder.py │ │ ├── integration_options_mixin.py │ │ ├── migrations │ │ │ ├── 0001_squashed_initial.py │ │ │ ├── 0002_squashed_initial.py │ │ │ ├── 0003_grafanaalertingcontactpoint_datasource_uid.py │ │ │ ├── 0004_auto_20220711_1106.py │ │ │ ├── 0005_alertgroup_cached_render_for_web.py │ │ │ ├── 0006_alertgroup_alerts_aler_channel_ee84a7_idx.py │ │ │ ├── 0007_populate_web_title_cache.py │ │ │ ├── 0008_alter_alertgrouplogrecord_type.py │ │ │ ├── 0009_alertreceivechannel_web_templates_modified_at.py │ │ │ ├── 0010_channelfilter_filtering_term_type.py │ │ │ ├── 0011_auto_20230329_1617.py │ │ │ ├── 0012_alertreceivechannel_description_short.py │ │ │ ├── 0012_auto_20230406_1010.py │ │ │ ├── 0013_merge_20230418_0336.py │ │ │ ├── 0014_alertreceivechannel_restricted_at.py │ │ │ ├── 0015_auto_20230508_1641.py │ │ │ ├── 0016_auto_20230523_1355.py │ │ │ ├── 0017_alertgroup_is_restricted.py │ │ │ ├── 0018_remove_alertreceivechannel_integration_slack_channel_id.py │ │ │ ├── 0019_auto_20230705_1619.py │ │ │ ├── 0020_auto_20230711_1532.py │ │ │ ├── 0021_alter_alertgroup_started_at.py │ │ │ ├── 0022_alter_alertgroup_manual_severity.py │ │ │ ├── 0023_auto_20230718_0952.py │ │ │ ├── 0024_auto_20230718_0953.py │ │ │ ├── 0025_auto_20230718_1042.py │ │ │ ├── 0026_auto_20230719_1010.py │ │ │ ├── 0027_remove_alertreceivechannel_restricted_at_from_state.py │ │ │ ├── 0028_drop_alertreceivechannel_restricted_at.py │ │ │ ├── 0029_auto_20230728_0802.py │ │ │ ├── 0030_auto_20230731_0341.py │ │ │ ├── 0031_auto_20230831_1445.py │ │ │ ├── 0032_remove_alertgroup_slack_message_state.py │ │ │ ├── 0033_alertgrouplogrecord_action_source.py │ │ │ ├── 0034_alter_resolutionnote_source.py │ │ │ ├── 0035_alter_alertreceivechannel_maintenance_author.py │ │ │ ├── 0036_alertgroup_grafana_incident_id.py │ │ │ ├── 0037_remove_alertgroup_is_restricted_state.py │ │ │ ├── 0038_remove_alertgroup_is_restricted_db.py │ │ │ ├── 0039_remove_alertreceivechannel_unique_integration_name.py │ │ │ ├── 0040_alertreceivechannel_alert_group_labels_custom_and_more.py │ │ │ ├── 0041_alertreceivechannel_unique_direct_paging_integration_per_team.py │ │ │ ├── 0042_alertgroup_received_at.py │ │ │ ├── 0043_remove_alertgroup_alerts_aler_channel_81aeec_idx_and_more.py │ │ │ ├── 0044_alertreceivechannel_alertmanager_v2_backup_templates_and_more.py │ │ │ ├── 0045_escalationpolicy_notify_to_team_members_and_more.py │ │ │ ├── 0046_alertreceivechannelconnection.py │ │ │ ├── 0047_alertreceivechannel_additional_settings.py │ │ │ ├── 0048_alertgroupexternalid.py │ │ │ ├── 0049_alter_alertgrouplogrecord_action_source.py │ │ │ ├── 0050_alter_alertgrouplogrecord_type.py │ │ │ ├── 0051_remove_escalationpolicy_custom_button_trigger.py │ │ │ ├── 0052_alter_channelfilter_filtering_term_type.py │ │ │ ├── 0053_channelfilter_filtering_labels.py │ │ │ ├── 0054_usernotificationbundle_bundlednotification_and_more.py │ │ │ ├── 0055_alter_bundlednotification_alert_group.py │ │ │ ├── 0056_remove_alertgroup_slack_log_message_state.py │ │ │ ├── 0057_remove_alertgroup_slack_log_message_db.py │ │ │ ├── 0058_alter_alertgroup_reason_to_skip_escalation.py │ │ │ ├── 0059_escalationpolicy_severity_and_more.py │ │ │ ├── 0060_relatedincident.py │ │ │ ├── 0061_alter_alertgroup_resolved_by_alert.py │ │ │ ├── 0062_rename_slack_channel_id_channelfilter__slack_channel_id_and_more.py │ │ │ ├── 0063_migrate_channelfilter_slack_channel_id.py │ │ │ ├── 0064_migrate_resolutionnoteslackmessage_slack_channel_id.py │ │ │ ├── 0065_alertreceivechannel_service_account.py │ │ │ ├── 0066_remove_channelfilter__slack_channel_id_and_more.py │ │ │ ├── 0067_remove_channelfilter__slack_channel_id_state.py │ │ │ ├── 0068_remove_resolutionnoteslackmessage__slack_channel_id_state.py │ │ │ ├── 0069_remove_channelfilter__slack_channel_id_db.py │ │ │ ├── 0070_remove_resolutionnoteslackmessage__slack_channel_id_db.py │ │ │ ├── 0071_migrate_labels.py │ │ │ ├── 0072_upsert_direct_paging_integration_routes.py │ │ │ ├── 0073_update_direct_paging_integration_non_default_routes.py │ │ │ ├── 0074_alter_escalationpolicy_step.py │ │ │ ├── 0075_alter_alertgrouplogrecord_action_source.py │ │ │ └── __init__.py │ │ ├── models │ │ │ ├── __init__.py │ │ │ ├── alert.py │ │ │ ├── alert_group.py │ │ │ ├── alert_group_counter.py │ │ │ ├── alert_group_log_record.py │ │ │ ├── alert_manager_models.py │ │ │ ├── alert_receive_channel.py │ │ │ ├── alert_receive_channel_connection.py │ │ │ ├── channel_filter.py │ │ │ ├── custom_button.py │ │ │ ├── escalation_chain.py │ │ │ ├── escalation_policy.py │ │ │ ├── grafana_alerting_contact_point.py │ │ │ ├── invitation.py │ │ │ ├── maintainable_object.py │ │ │ ├── related_incident.py │ │ │ ├── resolution_note.py │ │ │ ├── user_has_notification.py │ │ │ └── user_notification_bundle.py │ │ ├── paging.py │ │ ├── representative.py │ │ ├── signals.py │ │ ├── tasks │ │ │ ├── __init__.py │ │ │ ├── acknowledge_reminder.py │ │ │ ├── alert_group_web_title_cache.py │ │ │ ├── call_ack_url.py │ │ │ ├── check_escalation_finished.py │ │ │ ├── custom_webhook_result.py │ │ │ ├── declare_incident.py │ │ │ ├── delete_alert_group.py │ │ │ ├── distribute_alert.py │ │ │ ├── escalate_alert_group.py │ │ │ ├── invite_user_to_join_incident.py │ │ │ ├── maintenance.py │ │ │ ├── notify_all.py │ │ │ ├── notify_group.py │ │ │ ├── notify_ical_schedule_shift.py │ │ │ ├── notify_user.py │ │ │ ├── resolve_alert_group_by_source_if_needed.py │ │ │ ├── resolve_by_last_step.py │ │ │ ├── send_alert_group_signal.py │ │ │ ├── send_update_log_report_signal.py │ │ │ ├── send_update_resolution_note_signal.py │ │ │ ├── sync_grafana_alerting_contact_points.py │ │ │ ├── task_logger.py │ │ │ ├── unsilence.py │ │ │ └── wipe.py │ │ ├── tests │ │ │ ├── __init__.py │ │ │ ├── conftest.py │ │ │ ├── factories.py │ │ │ ├── test_acknowledge_reminder.py │ │ │ ├── test_alert.py │ │ │ ├── test_alert_group.py │ │ │ ├── test_alert_group_log_record.py │ │ │ ├── test_alert_group_renderer.py │ │ │ ├── test_alert_manager.py │ │ │ ├── test_alert_receiver_channel.py │ │ │ ├── test_channel_filter.py │ │ │ ├── test_check_escalation_finished_task.py │ │ │ ├── test_custom_webhook_result.py │ │ │ ├── test_default_templates.py │ │ │ ├── test_escalation_chain.py │ │ │ ├── test_escalation_policy_snapshot.py │ │ │ ├── test_escalation_snapshot.py │ │ │ ├── test_escalation_snapshot_mixin.py │ │ │ ├── test_grafana_alerting_sync.py │ │ │ ├── test_incident_log_builder.py │ │ │ ├── test_maintenance.py │ │ │ ├── test_notify_all.py │ │ │ ├── test_notify_group.py │ │ │ ├── test_notify_ical_schedule_shift.py │ │ │ ├── test_notify_user.py │ │ │ ├── test_paging.py │ │ │ ├── test_related_incident.py │ │ │ ├── test_representative.py │ │ │ └── test_silence.py │ │ └── utils.py │ ├── api │ │ ├── __init__.py │ │ ├── alert_group_table_columns.py │ │ ├── errors.py │ │ ├── label_filtering.py │ │ ├── permissions.py │ │ ├── serializers │ │ │ ├── __init__.py │ │ │ ├── alert.py │ │ │ ├── alert_group.py │ │ │ ├── alert_group_escalation_snapshot.py │ │ │ ├── alert_group_table_settings.py │ │ │ ├── alert_receive_channel.py │ │ │ ├── alert_receive_channel_connection.py │ │ │ ├── alerts_field_cache_buster_mixin.py │ │ │ ├── channel_filter.py │ │ │ ├── custom_serializers.py │ │ │ ├── direct_paging.py │ │ │ ├── escalation_chain.py │ │ │ ├── escalation_policy.py │ │ │ ├── integration_heartbeat.py │ │ │ ├── labels.py │ │ │ ├── live_setting.py │ │ │ ├── on_call_shifts.py │ │ │ ├── organization.py │ │ │ ├── organization_slack_settings.py │ │ │ ├── public_api_token.py │ │ │ ├── resolution_note.py │ │ │ ├── schedule_base.py │ │ │ ├── schedule_calendar.py │ │ │ ├── schedule_ical.py │ │ │ ├── schedule_polymorphic.py │ │ │ ├── schedule_reminder.py │ │ │ ├── schedule_web.py │ │ │ ├── shift_swap.py │ │ │ ├── slack_channel.py │ │ │ ├── slack_user_identity.py │ │ │ ├── team.py │ │ │ ├── telegram.py │ │ │ ├── user.py │ │ │ ├── user_group.py │ │ │ ├── user_notification_policy.py │ │ │ └── webhook.py │ │ ├── tests │ │ │ ├── __init__.py │ │ │ ├── conftest.py │ │ │ ├── test_alert_group.py │ │ │ ├── test_alert_group_escalation_snapshot.py │ │ │ ├── test_alert_group_table_settings.py │ │ │ ├── test_alert_receive_channel.py │ │ │ ├── test_alert_receive_channel_template.py │ │ │ ├── test_auth.py │ │ │ ├── test_channel_filter.py │ │ │ ├── test_direct_paging.py │ │ │ ├── test_escalation_chain.py │ │ │ ├── test_escalation_policy.py │ │ │ ├── test_features.py │ │ │ ├── test_integration_heartbeat.py │ │ │ ├── test_labels.py │ │ │ ├── test_live_settings.py │ │ │ ├── test_oncall_shift.py │ │ │ ├── test_openapi_schema.py │ │ │ ├── test_organization.py │ │ │ ├── test_permissions.py │ │ │ ├── test_preview_template_options.py │ │ │ ├── test_public_api_tokens.py │ │ │ ├── test_resolution_note.py │ │ │ ├── test_route_regex_debugger.py │ │ │ ├── test_schedule_export.py │ │ │ ├── test_schedules.py │ │ │ ├── test_set_org_default_slack_channel.py │ │ │ ├── test_shift_swaps.py │ │ │ ├── test_slack_channels.py │ │ │ ├── test_slack_team_settings.py │ │ │ ├── test_team.py │ │ │ ├── test_telegram_channel.py │ │ │ ├── test_user.py │ │ │ ├── test_user_groups.py │ │ │ ├── test_user_notification_policy.py │ │ │ ├── test_user_schedule_export.py │ │ │ ├── test_webhook_presets.py │ │ │ └── test_webhooks.py │ │ ├── throttlers │ │ │ ├── __init__.py │ │ │ ├── demo_alert_throttler.py │ │ │ ├── phone_verification_throttler.py │ │ │ └── test_call_throttler.py │ │ ├── urls.py │ │ └── views │ │ │ ├── __init__.py │ │ │ ├── alert_group.py │ │ │ ├── alert_group_table_settings.py │ │ │ ├── alert_receive_channel.py │ │ │ ├── alert_receive_channel_template.py │ │ │ ├── alerts.py │ │ │ ├── auth.py │ │ │ ├── channel_filter.py │ │ │ ├── direct_paging.py │ │ │ ├── escalation_chain.py │ │ │ ├── escalation_policy.py │ │ │ ├── features.py │ │ │ ├── integration_heartbeat.py │ │ │ ├── labels.py │ │ │ ├── live_setting.py │ │ │ ├── on_call_shifts.py │ │ │ ├── organization.py │ │ │ ├── preview_template_options.py │ │ │ ├── public_api_tokens.py │ │ │ ├── resolution_note.py │ │ │ ├── route_regex_debugger.py │ │ │ ├── schedule.py │ │ │ ├── shift_swap.py │ │ │ ├── slack_channel.py │ │ │ ├── slack_team_settings.py │ │ │ ├── team.py │ │ │ ├── telegram_channels.py │ │ │ ├── user.py │ │ │ ├── user_group.py │ │ │ ├── user_notification_policy.py │ │ │ └── webhooks.py │ ├── api_for_grafana_incident │ │ ├── __init__.py │ │ ├── apps.py │ │ ├── serializers.py │ │ ├── tests │ │ │ ├── __init__.py │ │ │ └── test_views.py │ │ ├── urls.py │ │ └── views.py │ ├── auth_token │ │ ├── __init__.py │ │ ├── auth.py │ │ ├── constants.py │ │ ├── crypto.py │ │ ├── exceptions.py │ │ ├── grafana │ │ │ ├── __init__.py │ │ │ └── grafana_auth_token.py │ │ ├── migrations │ │ │ ├── 0001_squashed_initial.py │ │ │ ├── 0002_squashed_initial.py │ │ │ ├── 0003_auto_20221121_1610.py │ │ │ ├── 0004_alter_pluginauthtoken_organization.py │ │ │ ├── 0005_integrationauthtoken.py │ │ │ ├── 0006_googleoauth2token.py │ │ │ ├── 0007_serviceaccounttoken.py │ │ │ ├── 0008_mattermostauthtoken.py │ │ │ └── __init__.py │ │ ├── models │ │ │ ├── __init__.py │ │ │ ├── api_auth_token.py │ │ │ ├── base_auth_token.py │ │ │ ├── google_oauth2_token.py │ │ │ ├── integration_backsync_auth_token.py │ │ │ ├── mattermost_auth_token.py │ │ │ ├── plugin_auth_token.py │ │ │ ├── schedule_export_auth_token.py │ │ │ ├── service_account_token.py │ │ │ ├── slack_auth_token.py │ │ │ └── user_schedule_export_auth_token.py │ │ └── tests │ │ │ ├── __init__.py │ │ │ ├── helpers.py │ │ │ ├── test_crypto.py │ │ │ ├── test_grafana_auth.py │ │ │ ├── test_integration_backsync_auth_token.py │ │ │ └── test_plugin_auth.py │ ├── base │ │ ├── __init__.py │ │ ├── messaging.py │ │ ├── migrations │ │ │ ├── 0001_squashed_initial.py │ │ │ ├── 0002_squashed_initial.py │ │ │ ├── 0003_delete_organizationlogrecord.py │ │ │ ├── 0004_auto_20230616_1510.py │ │ │ ├── 0005_drop_unused_dynamic_settings.py │ │ │ └── __init__.py │ │ ├── models │ │ │ ├── __init__.py │ │ │ ├── dynamic_setting.py │ │ │ ├── failed_to_invoke_celery_task.py │ │ │ ├── live_setting.py │ │ │ ├── user_notification_policy.py │ │ │ └── user_notification_policy_log_record.py │ │ ├── tasks.py │ │ ├── tests │ │ │ ├── __init__.py │ │ │ ├── factories.py │ │ │ ├── messaging_backend.py │ │ │ ├── test_live_settings.py │ │ │ ├── test_messaging.py │ │ │ ├── test_user_notification_policy.py │ │ │ └── test_user_notification_policy_log_record.py │ │ └── utils.py │ ├── chatops_proxy │ │ ├── __init__.py │ │ ├── client.py │ │ ├── events │ │ │ ├── __init__.py │ │ │ ├── handlers.py │ │ │ ├── root_handler.py │ │ │ ├── signature.py │ │ │ └── types.py │ │ ├── register_oncall_tenant.py │ │ ├── tasks.py │ │ ├── tests │ │ │ ├── __init__.py │ │ │ └── test_events.py │ │ ├── urls.py │ │ ├── utils.py │ │ └── views.py │ ├── email │ │ ├── __init__.py │ │ ├── alert_rendering.py │ │ ├── backend.py │ │ ├── inbound.py │ │ ├── migrations │ │ │ ├── 0001_initial.py │ │ │ ├── 0002_alter_emailmessage_receiver.py │ │ │ └── __init__.py │ │ ├── models.py │ │ ├── tasks.py │ │ ├── templates │ │ │ └── email_notification.html │ │ ├── tests │ │ │ ├── __init__.py │ │ │ ├── factories.py │ │ │ ├── test_inbound_email.py │ │ │ └── test_notify_user.py │ │ └── validate_amazon_sns_message.py │ ├── exotel │ │ ├── __init__.py │ │ ├── migrations │ │ │ ├── 0001_initial.py │ │ │ └── __init__.py │ │ ├── models │ │ │ ├── __init__.py │ │ │ └── phone_call.py │ │ ├── phone_provider.py │ │ ├── status_callback.py │ │ ├── tests │ │ │ ├── __init__.py │ │ │ └── test_exotel_provider.py │ │ ├── urls.py │ │ └── views.py │ ├── google │ │ ├── __init__.py │ │ ├── client.py │ │ ├── constants.py │ │ ├── migrations │ │ │ ├── 0001_initial.py │ │ │ └── __init__.py │ │ ├── models │ │ │ ├── __init__.py │ │ │ └── google_oauth2_user.py │ │ ├── tasks.py │ │ ├── tests │ │ │ ├── __init__.py │ │ │ ├── factories.py │ │ │ ├── test_sync_out_of_office_calendar_events_for_user.py │ │ │ └── test_utils.py │ │ ├── types.py │ │ └── utils.py │ ├── grafana_plugin │ │ ├── __init__.py │ │ ├── apps.py │ │ ├── helpers │ │ │ ├── __init__.py │ │ │ ├── client.py │ │ │ └── gcom.py │ │ ├── serializers │ │ │ └── sync_data.py │ │ ├── sync_data.py │ │ ├── tasks │ │ │ ├── __init__.py │ │ │ ├── sync.py │ │ │ └── sync_v2.py │ │ ├── tests │ │ │ ├── test_app_config.py │ │ │ ├── test_gcom.py │ │ │ ├── test_gcom_api_client.py │ │ │ ├── test_grafana_api_client.py │ │ │ ├── test_install.py │ │ │ ├── test_install_v2.py │ │ │ ├── test_self_hosted_install.py │ │ │ ├── test_status.py │ │ │ ├── test_sync.py │ │ │ ├── test_sync_v2.py │ │ │ └── test_ui_url_builder.py │ │ ├── ui_url_builder.py │ │ ├── urls.py │ │ └── views │ │ │ ├── __init__.py │ │ │ ├── install.py │ │ │ ├── install_v2.py │ │ │ ├── recaptcha.py │ │ │ ├── self_hosted_install.py │ │ │ ├── status.py │ │ │ ├── status_v2.py │ │ │ ├── sync_organization.py │ │ │ └── sync_v2.py │ ├── heartbeat │ │ ├── __init__.py │ │ ├── migrations │ │ │ ├── 0001_squashed_initial.py │ │ │ ├── 0002_delete_heartbeat.py │ │ │ ├── 0003_remove_integrationheartbeat_actual_check_up_task_id_and_more.py │ │ │ └── __init__.py │ │ ├── models.py │ │ ├── tasks.py │ │ └── tests │ │ │ ├── __init__.py │ │ │ ├── factories.py │ │ │ └── test_integration_heartbeat.py │ ├── integrations │ │ ├── __init__.py │ │ ├── legacy_prefix.py │ │ ├── metadata │ │ │ ├── __init__.py │ │ │ └── heartbeat │ │ │ │ ├── __init__.py │ │ │ │ ├── _heartbeat_text_creator.py │ │ │ │ ├── alertmanager.py │ │ │ │ ├── elastalert.py │ │ │ │ ├── formatted_webhook.py │ │ │ │ ├── grafana.py │ │ │ │ ├── legacy_alertmanager.py │ │ │ │ ├── prtg.py │ │ │ │ ├── webhook.py │ │ │ │ └── zabbix.py │ │ ├── middlewares.py │ │ ├── mixins │ │ │ ├── __init__.py │ │ │ ├── alert_channel_defining_mixin.py │ │ │ ├── browsable_instruction_mixin.py │ │ │ └── ratelimit_mixin.py │ │ ├── tasks.py │ │ ├── templates │ │ │ ├── heartbeat_link.html │ │ │ └── integration_link.html │ │ ├── tests │ │ │ ├── __init__.py │ │ │ ├── test_heartbeat_metadata.py │ │ │ ├── test_integration_backsync.py │ │ │ ├── test_legacy_alertmanager.py │ │ │ ├── test_ratelimit.py │ │ │ ├── test_tasks.py │ │ │ └── test_views.py │ │ ├── throttlers │ │ │ ├── __init__.py │ │ │ └── integration_backsync_throttler.py │ │ ├── urls.py │ │ └── views.py │ ├── labels │ │ ├── __init__.py │ │ ├── alert_group_labels.py │ │ ├── client.py │ │ ├── migrations │ │ │ ├── 0001_initial.py │ │ │ ├── 0002_alertgroupassociatedlabel_and_more.py │ │ │ ├── 0003_alertreceivechannelassociatedlabel_inherit.py │ │ │ ├── 0004_webhookassociatedlabel.py │ │ │ ├── 0005_labelkeycache_prescribed_labelvaluecache_prescribed.py │ │ │ ├── 0006_remove_alertreceivechannelassociatedlabel_inheritable_state.py │ │ │ ├── 0007_remove_alertreceivechannelassociatedlabel_inheritable_db.py │ │ │ └── __init__.py │ │ ├── models.py │ │ ├── tasks.py │ │ ├── tests │ │ │ ├── __init__.py │ │ │ ├── factories.py │ │ │ ├── test_add_service_label.py │ │ │ ├── test_alert_group.py │ │ │ ├── test_labels.py │ │ │ └── test_labels_cache.py │ │ ├── types.py │ │ └── utils.py │ ├── mattermost │ │ ├── __init__.py │ │ ├── alert_group_representative.py │ │ ├── alert_rendering.py │ │ ├── apps.py │ │ ├── auth.py │ │ ├── backend.py │ │ ├── client.py │ │ ├── events │ │ │ ├── __init__.py │ │ │ ├── alert_group_actions_handler.py │ │ │ ├── event_handler.py │ │ │ ├── event_manager.py │ │ │ └── types.py │ │ ├── exceptions.py │ │ ├── migrations │ │ │ ├── 0001_initial.py │ │ │ └── __init__.py │ │ ├── models │ │ │ ├── __init__.py │ │ │ ├── channel.py │ │ │ ├── message.py │ │ │ └── user.py │ │ ├── serializers.py │ │ ├── signals.py │ │ ├── tasks.py │ │ ├── tests │ │ │ ├── __init__.py │ │ │ ├── conftest.py │ │ │ ├── events │ │ │ │ ├── __init__.py │ │ │ │ └── test_alert_group_action_handler.py │ │ │ ├── factories.py │ │ │ ├── models │ │ │ │ └── test_channel.py │ │ │ ├── test_alert_rendering.py │ │ │ ├── test_backend.py │ │ │ ├── test_mattermost_channel.py │ │ │ ├── test_mattermost_client.py │ │ │ ├── test_mattermost_event.py │ │ │ ├── test_representative.py │ │ │ ├── test_tasks.py │ │ │ └── test_utils.py │ │ ├── urls.py │ │ ├── utils.py │ │ └── views.py │ ├── metrics_exporter │ │ ├── __init__.py │ │ ├── apps.py │ │ ├── constants.py │ │ ├── helpers.py │ │ ├── metrics_cache_manager.py │ │ ├── metrics_collectors.py │ │ ├── signals.py │ │ ├── tasks.py │ │ ├── tests │ │ │ ├── __init__.py │ │ │ ├── conftest.py │ │ │ ├── test_calculation_metrics.py │ │ │ ├── test_metrics_collectors.py │ │ │ ├── test_update_metrics_cache.py │ │ │ └── test_views.py │ │ ├── urls.py │ │ └── views.py │ ├── mobile_app │ │ ├── __init__.py │ │ ├── alert_rendering.py │ │ ├── auth.py │ │ ├── backend.py │ │ ├── demo_push.py │ │ ├── exceptions.py │ │ ├── fcm_relay.py │ │ ├── migrations │ │ │ ├── 0001_initial.py │ │ │ ├── 0002_alter_mobileappauthtoken_user.py │ │ │ ├── 0003_mobileappusersettings.py │ │ │ ├── 0004_auto_20230425_1033.py │ │ │ ├── 0005_mobileappusersettings_important_notification_volume_override.py │ │ │ ├── 0006_auto_20230512_0902.py │ │ │ ├── 0007_alter_mobileappusersettings_info_notifications_enabled.py │ │ │ ├── 0008_mobileappusersettings_locale.py │ │ │ ├── 0009_fcmdevice.py │ │ │ ├── 0010_mobileappusersettings_time_zone.py │ │ │ ├── 0011_alter_mobileappusersettings_going_oncall_notification_timing.py │ │ │ └── __init__.py │ │ ├── models.py │ │ ├── serializers.py │ │ ├── tasks │ │ │ ├── __init__.py │ │ │ ├── going_oncall_notification.py │ │ │ ├── new_alert_group.py │ │ │ └── new_shift_swap_request.py │ │ ├── tests │ │ │ ├── __init__.py │ │ │ ├── tasks │ │ │ │ ├── __init__.py │ │ │ │ ├── test_going_oncall_notification.py │ │ │ │ ├── test_new_alert_group.py │ │ │ │ └── test_new_shift_swap_request.py │ │ │ ├── test_alert_rendering.py │ │ │ ├── test_demo_push.py │ │ │ ├── test_fcm_device_model.py │ │ │ ├── test_fcm_endpoint.py │ │ │ ├── test_fcm_relay.py │ │ │ ├── test_mobile_app_auth_token.py │ │ │ ├── test_mobile_app_gateway.py │ │ │ ├── test_user_settings.py │ │ │ └── test_utils.py │ │ ├── types.py │ │ ├── urls.py │ │ ├── utils.py │ │ └── views.py │ ├── oss_installation │ │ ├── __init__.py │ │ ├── cloud_heartbeat.py │ │ ├── constants.py │ │ ├── migrations │ │ │ ├── 0001_squashed_initial.py │ │ │ └── __init__.py │ │ ├── models │ │ │ ├── __init__.py │ │ │ ├── cloud_connector.py │ │ │ ├── cloud_heartbeat.py │ │ │ ├── cloud_user_identity.py │ │ │ └── oss_installation.py │ │ ├── serializers │ │ │ ├── __init__.py │ │ │ └── cloud_user.py │ │ ├── tasks.py │ │ ├── tests │ │ │ ├── __init__.py │ │ │ └── test_views.py │ │ ├── urls.py │ │ ├── usage_stats.py │ │ ├── utils.py │ │ └── views │ │ │ ├── __init__.py │ │ │ ├── cloud_connection.py │ │ │ ├── cloud_heartbeat.py │ │ │ └── cloud_users.py │ ├── phone_notifications │ │ ├── __init__.py │ │ ├── exceptions.py │ │ ├── migrations │ │ │ ├── 0001_initial.py │ │ │ ├── 0002_bannedphonenumber.py │ │ │ ├── 0003_smsrecord_represents_bundle_uuid.py │ │ │ └── __init__.py │ │ ├── models │ │ │ ├── __init__.py │ │ │ ├── banned_phone_number.py │ │ │ ├── phone_call.py │ │ │ └── sms.py │ │ ├── phone_backend.py │ │ ├── phone_provider.py │ │ ├── simple_phone_provider.py │ │ └── tests │ │ │ ├── __init__.py │ │ │ ├── factories.py │ │ │ ├── mock_phone_provider.py │ │ │ ├── test_banned_phone_number.py │ │ │ ├── test_phone_backend_call.py │ │ │ ├── test_phone_backend_oss_relay.py │ │ │ ├── test_phone_backend_phone_verification.py │ │ │ └── test_phone_backend_sms.py │ ├── public_api │ │ ├── __init__.py │ │ ├── constants.py │ │ ├── custom_renderers.py │ │ ├── helpers.py │ │ ├── serializers │ │ │ ├── __init__.py │ │ │ ├── action.py │ │ │ ├── alert_groups.py │ │ │ ├── alerts.py │ │ │ ├── escalation.py │ │ │ ├── escalation_chains.py │ │ │ ├── escalation_policies.py │ │ │ ├── integrations.py │ │ │ ├── integtration_heartbeat.py │ │ │ ├── maintenance.py │ │ │ ├── on_call_shifts.py │ │ │ ├── organizations.py │ │ │ ├── personal_notification_rules.py │ │ │ ├── resolution_notes.py │ │ │ ├── routes.py │ │ │ ├── schedules_base.py │ │ │ ├── schedules_calendar.py │ │ │ ├── schedules_ical.py │ │ │ ├── schedules_polymorphic.py │ │ │ ├── schedules_web.py │ │ │ ├── slack_channel.py │ │ │ ├── teams.py │ │ │ ├── user_groups.py │ │ │ ├── users.py │ │ │ └── webhooks.py │ │ ├── tests │ │ │ ├── __init__.py │ │ │ ├── test_alert_groups.py │ │ │ ├── test_alerts.py │ │ │ ├── test_custom_actions.py │ │ │ ├── test_escalation.py │ │ │ ├── test_escalation_chain.py │ │ │ ├── test_escalation_policies.py │ │ │ ├── test_integrations.py │ │ │ ├── test_maintenance.py │ │ │ ├── test_on_call_shifts.py │ │ │ ├── test_personal_notification_rules.py │ │ │ ├── test_ratelimit.py │ │ │ ├── test_rbac_permissions.py │ │ │ ├── test_resolution_notes.py │ │ │ ├── test_routes.py │ │ │ ├── test_schedule_export.py │ │ │ ├── test_schedules.py │ │ │ ├── test_shift_swap.py │ │ │ ├── test_slack_channels.py │ │ │ ├── test_teams.py │ │ │ ├── test_user_groups.py │ │ │ ├── test_users.py │ │ │ └── test_webhooks.py │ │ ├── tf_sync.py │ │ ├── throttlers │ │ │ ├── __init__.py │ │ │ ├── info_throttler.py │ │ │ ├── phone_notification_throttler.py │ │ │ └── user_throttle.py │ │ ├── urls.py │ │ └── views │ │ │ ├── __init__.py │ │ │ ├── action.py │ │ │ ├── alert_groups.py │ │ │ ├── alerts.py │ │ │ ├── escalation.py │ │ │ ├── escalation_chains.py │ │ │ ├── escalation_policies.py │ │ │ ├── info.py │ │ │ ├── integrations.py │ │ │ ├── maintaiable_object_mixin.py │ │ │ ├── on_call_shifts.py │ │ │ ├── organizations.py │ │ │ ├── personal_notifications.py │ │ │ ├── phone_notifications.py │ │ │ ├── resolution_notes.py │ │ │ ├── routes.py │ │ │ ├── schedules.py │ │ │ ├── shift_swap.py │ │ │ ├── slack_channels.py │ │ │ ├── teams.py │ │ │ ├── user_groups.py │ │ │ ├── users.py │ │ │ └── webhooks.py │ ├── schedules │ │ ├── __init__.py │ │ ├── constants.py │ │ ├── exceptions.py │ │ ├── ical_events │ │ │ ├── __init__.py │ │ │ ├── adapter │ │ │ │ ├── __init__.py │ │ │ │ ├── amixr_recurring_ical_events_adapter.py │ │ │ │ └── recurring_ical_events_adapter.py │ │ │ └── proxy │ │ │ │ ├── __init__.py │ │ │ │ └── ical_proxy.py │ │ ├── ical_utils.py │ │ ├── migrations │ │ │ ├── 0001_squashed_initial.py │ │ │ ├── 0002_squashed_initial.py │ │ │ ├── 0003_alter_customoncallshift_frequency.py │ │ │ ├── 0004_customoncallshift_until.py │ │ │ ├── 0005_auto_20220704_1947.py │ │ │ ├── 0006_customoncallshift_rotation_start.py │ │ │ ├── 0007_customoncallshift_updated_shift.py │ │ │ ├── 0008_auto_20221201_0809.py │ │ │ ├── 0009_oncallschedulecalendar_enable_web_overrides.py │ │ │ ├── 0010_fix_polymorphic_delete_related.py │ │ │ ├── 0011_oncallschedule_cached_ical_final_schedule.py │ │ │ ├── 0012_auto_20230502_1259.py │ │ │ ├── 0013_auto_20230517_0510.py │ │ │ ├── 0014_shiftswaprequest.py │ │ │ ├── 0015_shiftswaprequest_slack_message.py │ │ │ ├── 0016_alter_shiftswaprequest_created_at.py │ │ │ ├── 0017_alter_oncallschedule_polymorphic_ctype.py │ │ │ ├── 0018_oncallschedule_slack_channel.py │ │ │ ├── 0019_auto_20241021_1735.py │ │ │ ├── 0020_remove_oncallschedule_channel_state.py │ │ │ ├── 0021_remove_oncallschedule_channel_db.py │ │ │ └── __init__.py │ │ ├── models │ │ │ ├── __init__.py │ │ │ ├── custom_on_call_shift.py │ │ │ ├── on_call_schedule.py │ │ │ └── shift_swap_request.py │ │ ├── tasks │ │ │ ├── __init__.py │ │ │ ├── check_gaps_and_empty_shifts.py │ │ │ ├── drop_cached_ical.py │ │ │ ├── notify_about_empty_shifts_in_schedule.py │ │ │ ├── notify_about_gaps_in_schedule.py │ │ │ ├── refresh_ical_files.py │ │ │ └── shift_swaps │ │ │ │ ├── __init__.py │ │ │ │ ├── notify_when_taken.py │ │ │ │ ├── slack_followups.py │ │ │ │ └── slack_messages.py │ │ └── tests │ │ │ ├── __init__.py │ │ │ ├── calendars │ │ │ ├── calendar_with_all_day_event.ics │ │ │ ├── calendar_with_edited_recurring_events.ics │ │ │ ├── calendar_with_recurring_event.ics │ │ │ ├── modified_recurring_event.ics │ │ │ ├── override_email_case_sensitivity.ics │ │ │ └── quality.ics │ │ │ ├── conftest.py │ │ │ ├── custom_shift_test_cases.py │ │ │ ├── factories.py │ │ │ ├── tasks │ │ │ ├── __init__.py │ │ │ ├── shift_swaps │ │ │ │ ├── __init__.py │ │ │ │ ├── test_notify_when_taken.py │ │ │ │ ├── test_slack_followups.py │ │ │ │ └── test_slack_messages.py │ │ │ ├── test_drop_cached_ical.py │ │ │ └── test_refresh_ical_files.py │ │ │ ├── test_amixr_users_in_ical.py │ │ │ ├── test_check_gaps_and_empty_shifts.py │ │ │ ├── test_custom_on_call_shift.py │ │ │ ├── test_ical_proxy.py │ │ │ ├── test_ical_utils.py │ │ │ ├── test_notify_about_empty_shifts_in_schedule.py │ │ │ ├── test_notify_about_gaps_in_schedule.py │ │ │ ├── test_on_call_schedule.py │ │ │ ├── test_quality_score.py │ │ │ └── test_shift_swap_request.py │ ├── slack │ │ ├── 0013_remove_slackmessage__channel_id_db.py │ │ ├── 0014_remove_slackmessage_organization_db.py │ │ ├── __init__.py │ │ ├── alert_group_slack_service.py │ │ ├── chatops_proxy_routing.py │ │ ├── client.py │ │ ├── constants.py │ │ ├── errors.py │ │ ├── installation.py │ │ ├── migrations │ │ │ ├── 0001_squashed_initial.py │ │ │ ├── 0002_squashed_initial.py │ │ │ ├── 0003_delete_slackactionrecord.py │ │ │ ├── 0004_auto_20230913_1020.py │ │ │ ├── 0005_slackteamidentity__unified_slack_app_installed.py │ │ │ ├── 0006_rename_channel_id_slackmessage__channel_id_and_more.py │ │ │ ├── 0007_migrate_slackmessage_channel_id.py │ │ │ ├── 0008_remove_slackmessage_active_update_task_id_state.py │ │ │ ├── 0009_drop_orphaned_messages_and_fill_in_missing_team_identity_values.py │ │ │ ├── 0010_remove_slackmessage_active_update_task_id_db.py │ │ │ ├── 0011_remove_slackmessage__channel_id_state.py │ │ │ ├── 0012_remove_slackmessage_organization_state.py │ │ │ └── __init__.py │ │ ├── models │ │ │ ├── __init__.py │ │ │ ├── slack_channel.py │ │ │ ├── slack_message.py │ │ │ ├── slack_team_identity.py │ │ │ ├── slack_user_identity.py │ │ │ └── slack_usergroup.py │ │ ├── representatives │ │ │ ├── __init__.py │ │ │ ├── alert_group_representative.py │ │ │ └── user_representative.py │ │ ├── scenarios │ │ │ ├── __init__.py │ │ │ ├── alertgroup_appearance.py │ │ │ ├── alertgroup_timeline.py │ │ │ ├── declare_incident.py │ │ │ ├── distribute_alerts.py │ │ │ ├── escalation_delivery.py │ │ │ ├── invited_to_channel.py │ │ │ ├── manage_responders.py │ │ │ ├── notification_delivery.py │ │ │ ├── notified_user_not_in_channel.py │ │ │ ├── onboarding.py │ │ │ ├── paging.py │ │ │ ├── profile_update.py │ │ │ ├── resolution_note.py │ │ │ ├── scenario_step.py │ │ │ ├── schedules.py │ │ │ ├── shift_swap_requests.py │ │ │ ├── slack_channel.py │ │ │ ├── slack_channel_integration.py │ │ │ ├── slack_renderer.py │ │ │ ├── slack_usergroup.py │ │ │ └── step_mixins.py │ │ ├── slack_formatter.py │ │ ├── slash_command.py │ │ ├── tasks.py │ │ ├── tests │ │ │ ├── __init__.py │ │ │ ├── conftest.py │ │ │ ├── factories.py │ │ │ ├── scenario_steps │ │ │ │ ├── __init__.py │ │ │ │ ├── test_alert_group_actions.py │ │ │ │ ├── test_distribute_alerts.py │ │ │ │ ├── test_manage_responders.py │ │ │ │ ├── test_paging.py │ │ │ │ ├── test_resolution_note.py │ │ │ │ ├── test_shift_swap_requests.py │ │ │ │ ├── test_slack_channel.py │ │ │ │ ├── test_slack_channel_integration.py │ │ │ │ └── test_slack_usergroup_steps.py │ │ │ ├── tasks │ │ │ │ ├── __init__.py │ │ │ │ ├── test_send_message_to_thread_if_bot_not_in_channel.py │ │ │ │ └── test_update_alert_group_slack_message.py │ │ │ ├── test_installation.py │ │ │ ├── test_interactive_api_endpoint.py │ │ │ ├── test_parse_slack_usernames.py │ │ │ ├── test_populate_slack_channels.py │ │ │ ├── test_reset_slack.py │ │ │ ├── test_slack_client.py │ │ │ ├── test_slack_formatter.py │ │ │ ├── test_slack_message.py │ │ │ ├── test_slack_renderer.py │ │ │ ├── test_slash_command.py │ │ │ ├── test_user_group.py │ │ │ └── test_utils.py │ │ ├── types │ │ │ ├── __init__.py │ │ │ ├── block_elements.py │ │ │ ├── blocks.py │ │ │ ├── common.py │ │ │ ├── composition_objects.py │ │ │ ├── interaction_payloads │ │ │ │ ├── __init__.py │ │ │ │ ├── block_actions.py │ │ │ │ ├── dialog_submission.py │ │ │ │ ├── interactive_messages.py │ │ │ │ ├── shortcuts.py │ │ │ │ ├── slash_command.py │ │ │ │ └── view_submission.py │ │ │ ├── scenario_routes.py │ │ │ └── views.py │ │ ├── urls.py │ │ ├── utils.py │ │ └── views.py │ ├── social_auth │ │ ├── __init__.py │ │ ├── backends.py │ │ ├── exceptions.py │ │ ├── live_setting_django_strategy.py │ │ ├── middlewares.py │ │ ├── pipeline │ │ │ ├── __init__.py │ │ │ ├── common.py │ │ │ ├── google.py │ │ │ ├── mattermost.py │ │ │ └── slack.py │ │ └── types.py │ ├── telegram │ │ ├── __init__.py │ │ ├── alert_group_representative.py │ │ ├── apps.py │ │ ├── client.py │ │ ├── decorators.py │ │ ├── exceptions.py │ │ ├── migrations │ │ │ ├── 0001_squashed_initial.py │ │ │ ├── 0002_alter_telegrammessage_message_type.py │ │ │ └── __init__.py │ │ ├── models │ │ │ ├── __init__.py │ │ │ ├── connectors │ │ │ │ ├── __init__.py │ │ │ │ ├── channel.py │ │ │ │ └── personal.py │ │ │ ├── message.py │ │ │ └── verification │ │ │ │ ├── __init__.py │ │ │ │ ├── channel.py │ │ │ │ └── personal.py │ │ ├── renderers │ │ │ ├── __init__.py │ │ │ ├── keyboard.py │ │ │ └── message.py │ │ ├── signals.py │ │ ├── tasks.py │ │ ├── tests │ │ │ ├── __init__.py │ │ │ ├── factories.py │ │ │ ├── test_button_update_handler.py │ │ │ ├── test_channel_connector.py │ │ │ ├── test_keyboard_renderer.py │ │ │ ├── test_message_renderer.py │ │ │ ├── test_models.py │ │ │ ├── test_personal_connector.py │ │ │ ├── test_tasks.py │ │ │ ├── test_update_handlers.py │ │ │ └── test_webhook.py │ │ ├── updates │ │ │ ├── __init__.py │ │ │ ├── update_handlers │ │ │ │ ├── __init__.py │ │ │ │ ├── button_press.py │ │ │ │ ├── channel_to_group_forward.py │ │ │ │ ├── start_message.py │ │ │ │ ├── update_handler.py │ │ │ │ └── verification │ │ │ │ │ ├── __init__.py │ │ │ │ │ ├── channel.py │ │ │ │ │ └── personal.py │ │ │ └── update_manager.py │ │ ├── urls.py │ │ ├── utils.py │ │ └── views.py │ ├── twilioapp │ │ ├── __init__.py │ │ ├── gather.py │ │ ├── migrations │ │ │ ├── 0001_squashed_initial.py │ │ │ ├── 0002_auto_20220604_1008.py │ │ │ ├── 0003_auto_20230408_0711.py │ │ │ ├── 0004_twiliophonecall_twiliosms.py │ │ │ ├── 0005_twilioaccount_twiliophonecallsender_twiliosmssender_twilioverificationsender.py │ │ │ ├── 0006_auto_20230601_0807.py │ │ │ ├── 0007_delete_twiliologrecord.py │ │ │ ├── 0008_alter_twiliophonecallsender_account_and_more.py │ │ │ └── __init__.py │ │ ├── models │ │ │ ├── __init__.py │ │ │ ├── twilio_phone_call.py │ │ │ ├── twilio_sender.py │ │ │ └── twilio_sms.py │ │ ├── phone_provider.py │ │ ├── status_callback.py │ │ ├── tests │ │ │ ├── __init__.py │ │ │ ├── conftest.py │ │ │ ├── factories.py │ │ │ ├── test_phone_calls.py │ │ │ ├── test_senders.py │ │ │ ├── test_sms_message.py │ │ │ └── test_twilio_provider.py │ │ ├── urls.py │ │ └── views.py │ ├── user_management │ │ ├── __init__.py │ │ ├── apps.py │ │ ├── constants.py │ │ ├── exceptions.py │ │ ├── middlewares.py │ │ ├── migrations │ │ │ ├── 0001_squashed_initial.py │ │ │ ├── 0002_auto_20220705_1214.py │ │ │ ├── 0003_user_hide_phone_number.py │ │ │ ├── 0004_auto_20221025_0316.py │ │ │ ├── 0005_rbac_permissions.py │ │ │ ├── 0006_organization_uuid.py │ │ │ ├── 0007_organization_deleted_at.py │ │ │ ├── 0008_organization_is_grafana_incident_enabled.py │ │ │ ├── 0009_organization_cluster_slug.py │ │ │ ├── 0010_team_is_sharing_resources_to_all.py │ │ │ ├── 0011_auto_20230411_1358.py │ │ │ ├── 0012_auto_20230711_1554.py │ │ │ ├── 0013_alter_organization_acknowledge_remind_timeout.py │ │ │ ├── 0014_auto_20230728_0802.py │ │ │ ├── 0015_auto_20230926_2203.py │ │ │ ├── 0016_alter_user_role.py │ │ │ ├── 0017_alter_organization_maintenance_author.py │ │ │ ├── 0018_auto_20231115_1206.py │ │ │ ├── 0019_organization_grafana_incident_backend_url.py │ │ │ ├── 0020_organization_is_grafana_labels_enabled.py │ │ │ ├── 0021_user_google_calendar_settings.py │ │ │ ├── 0022_alter_team_unique_together.py │ │ │ ├── 0023_organization_is_grafana_irm_enabled.py │ │ │ ├── 0024_organization_direct_paging_prefer_important_policy.py │ │ │ ├── 0025_organization_default_slack_channel.py │ │ │ ├── 0026_auto_20241017_1919.py │ │ │ ├── 0027_serviceaccount.py │ │ │ ├── 0028_remove_organization_general_log_channel_id_state.py │ │ │ ├── 0029_remove_organization_general_log_channel_id_db.py │ │ │ └── __init__.py │ │ ├── models │ │ │ ├── __init__.py │ │ │ ├── organization.py │ │ │ ├── region.py │ │ │ ├── service_account.py │ │ │ ├── team.py │ │ │ └── user.py │ │ ├── signals.py │ │ ├── subscription_strategy │ │ │ ├── __init__.py │ │ │ ├── base_subsription_strategy.py │ │ │ └── free_public_beta_subscription_strategy.py │ │ ├── sync.py │ │ ├── tests │ │ │ ├── __init__.py │ │ │ ├── factories.py │ │ │ ├── test_free_public_beta_subcription_strategy.py │ │ │ ├── test_organization.py │ │ │ ├── test_region.py │ │ │ ├── test_sync.py │ │ │ ├── test_team.py │ │ │ └── test_user.py │ │ ├── types.py │ │ └── user_representative.py │ ├── webhooks │ │ ├── __init__.py │ │ ├── apps.py │ │ ├── backend.py │ │ ├── listeners.py │ │ ├── migrations │ │ │ ├── 0001_initial.py │ │ │ ├── 0002_auto_20230320_1604.py │ │ │ ├── 0003_auto_20230412_0006.py │ │ │ ├── 0004_auto_20230418_0109.py │ │ │ ├── 0005_webhook_is_legacy.py │ │ │ ├── 0006_auto_20230426_1631.py │ │ │ ├── 0007_webhookresponse_event_data.py │ │ │ ├── 0008_auto_20230712_1613.py │ │ │ ├── 0009_alter_webhook_authorization_header.py │ │ │ ├── 0010_alter_webhook_trigger_type.py │ │ │ ├── 0011_auto_20230920_1813.py │ │ │ ├── 0012_alter_webhook_team.py │ │ │ ├── 0013_alter_webhook_trigger_type_and_more.py │ │ │ ├── 0014_webhook_filtered_integrations.py │ │ │ ├── 0015_webhook_is_from_connected_integration.py │ │ │ ├── 0016_auto_20240402_1341.py │ │ │ ├── 0017_alter_webhook_trigger_type_and_more.py │ │ │ ├── 0018_alter_webhook_trigger_type_and_more.py │ │ │ └── __init__.py │ │ ├── models │ │ │ ├── __init__.py │ │ │ └── webhook.py │ │ ├── presets │ │ │ ├── __init__.py │ │ │ ├── advanced.py │ │ │ ├── preset.py │ │ │ ├── preset_options.py │ │ │ └── simple.py │ │ ├── signals.py │ │ ├── tasks │ │ │ ├── __init__.py │ │ │ ├── alert_group_status.py │ │ │ ├── notify_user.py │ │ │ └── trigger_webhook.py │ │ ├── tests │ │ │ ├── __init__.py │ │ │ ├── factories.py │ │ │ ├── test_alert_group_status_change.py │ │ │ ├── test_backend.py │ │ │ ├── test_notify_user.py │ │ │ ├── test_trigger_webhook.py │ │ │ ├── test_webhook.py │ │ │ └── test_webhook_presets.py │ │ └── utils.py │ └── zvonok │ │ ├── __init__.py │ │ ├── migrations │ │ ├── 0001_initial.py │ │ ├── 0002_alter_zvonokphonecall_phone_call_record.py │ │ └── __init__.py │ │ ├── models │ │ ├── __init__.py │ │ └── phone_call.py │ │ ├── phone_provider.py │ │ ├── status_callback.py │ │ ├── tests │ │ ├── __init__.py │ │ └── test_zvonok_provider.py │ │ ├── urls.py │ │ └── views.py ├── celery_with_exporter.sh ├── common │ ├── __init__.py │ ├── api_helpers │ │ ├── __init__.py │ │ ├── custom_fields.py │ │ ├── custom_rate_scoped_throttler.py │ │ ├── custom_ratelimit.py │ │ ├── errors.py │ │ ├── exceptions.py │ │ ├── filters.py │ │ ├── mixins.py │ │ ├── optional_slash_router.py │ │ ├── paginators.py │ │ ├── serializers.py │ │ └── utils.py │ ├── cache.py │ ├── cloud_auth_api │ │ ├── __init__.py │ │ ├── client.py │ │ └── tests │ │ │ ├── __init__.py │ │ │ └── test_client.py │ ├── constants │ │ ├── __init__.py │ │ ├── plugin_ids.py │ │ └── slack_auth.py │ ├── custom_celery_tasks │ │ ├── __init__.py │ │ ├── create_alert_base_task.py │ │ ├── dedicated_queue_retry_task.py │ │ ├── log_exception_on_failure_task.py │ │ ├── safe_to_broker_outage_task.py │ │ └── tests │ │ │ ├── __init__.py │ │ │ ├── test_dedicated_queue_retry_task.py │ │ │ └── test_log_exception_on_failure_task.py │ ├── database.py │ ├── exceptions │ │ ├── __init__.py │ │ └── exceptions.py │ ├── incident_api │ │ ├── __init__.py │ │ ├── client.py │ │ └── tests │ │ │ ├── __init__.py │ │ │ └── test_client.py │ ├── insight_log │ │ ├── __init__.py │ │ ├── chatops_insight_logs.py │ │ ├── insight_logs_enabled_check.py │ │ ├── maintenance_insight_log.py │ │ └── resource_insight_logs.py │ ├── jinja_templater │ │ ├── __init__.py │ │ ├── apply_jinja_template.py │ │ ├── filters.py │ │ └── jinja_template_env.py │ ├── l10n.py │ ├── migrations │ │ ├── __init__.py │ │ └── remove_field.py │ ├── oncall_gateway │ │ └── tasks.py │ ├── ordered_model │ │ ├── __init__.py │ │ ├── ordered_model.py │ │ ├── serializer.py │ │ └── viewset.py │ ├── public_primary_keys.py │ ├── recaptcha │ │ ├── __init__.py │ │ └── recaptcha_v3.py │ ├── tests │ │ ├── __init__.py │ │ ├── test_apply_jinja_template.py │ │ ├── test_cache.py │ │ ├── test_create_engine_url.py │ │ ├── test_custom_fields.py │ │ ├── test_database.py │ │ ├── test_l10n.py │ │ ├── test_markup.py │ │ ├── test_ordered_model.py │ │ ├── test_recaptcha.py │ │ ├── test_regex_replace.py │ │ ├── test_task_queue_assignment.py │ │ ├── test_timezones.py │ │ ├── test_urlize.py │ │ └── test_viewset_actions.py │ ├── timezones.py │ └── utils.py ├── config_integrations │ ├── README.md │ ├── alertmanager.py │ ├── direct_paging.py │ ├── elastalert.py │ ├── formatted_webhook.py │ ├── grafana.py │ ├── grafana_alerting.py │ ├── inbound_email.py │ ├── kapacitor.py │ ├── legacy_alertmanager.py │ ├── legacy_grafana_alerting.py │ ├── maintenance.py │ ├── manual.py │ ├── webhook.py │ └── zabbix.py ├── conftest.py ├── engine │ ├── __init__.py │ ├── celery.py │ ├── included_path.py │ ├── integrations_urls.py │ ├── management │ │ └── commands │ │ │ ├── continue_escalation.py │ │ │ ├── create_sqlite_db.py │ │ │ ├── remove_field.py │ │ │ ├── restart_acknowledge_reminder.py │ │ │ ├── start_celery.py │ │ │ └── start_telegram_polling.py │ ├── middlewares.py │ ├── schema.py │ ├── tests │ │ ├── test_maintenance_mode.py │ │ └── test_views.py │ ├── urls.py │ ├── views.py │ └── wsgi.py ├── grpcio-1.64.1-cp312-cp312-linux_aarch64.whl ├── manage.py ├── pyproject.toml ├── requirements-dev.in ├── requirements-dev.txt ├── requirements.in ├── requirements.txt ├── settings │ ├── __init__.py │ ├── base.py │ ├── celery_task_routes.py │ ├── ci_test.py │ ├── dev.py │ ├── helm.py │ ├── hobby.py │ └── prod_without_db.py ├── static │ └── images │ │ └── resolution_note.gif ├── tox.ini ├── type_stubs │ └── icalendar │ │ ├── __init__.pyi │ │ ├── cal.pyi │ │ ├── caselessdict.pyi │ │ ├── cli.pyi │ │ ├── compat.pyi │ │ ├── parser.pyi │ │ ├── parser_tools.pyi │ │ ├── prop.pyi │ │ ├── timezone_cache.pyi │ │ ├── tools.pyi │ │ └── windows_to_olson.pyi ├── uwsgi.ini └── wait_for_test_mysql_start.sh ├── grafana-plugin ├── .bra.toml ├── .config │ ├── .eslintrc │ ├── .prettierrc.js │ ├── jest-setup.js │ ├── jest.config.js │ ├── jest │ │ ├── mocks │ │ │ └── react-inlinesvg.tsx │ │ └── utils.js │ ├── tsconfig.json │ ├── types │ │ └── custom.d.ts │ └── webpack │ │ ├── constants.ts │ │ ├── utils.ts │ │ └── webpack.config.ts ├── .dockerignore ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .prettierrc.js ├── .stylelintrc ├── CHANGELOG.md ├── Dockerfile.dev ├── LICENSE ├── Magefile.go ├── README.md ├── e2e-tests │ ├── .auth │ │ └── .gitignore │ ├── .eslintrc │ ├── alerts │ │ ├── directPaging.test.ts │ │ └── onCallSchedule.test.ts │ ├── escalationChains │ │ ├── escalationPolicy.test.ts │ │ └── searching.test.ts │ ├── fixtures.ts │ ├── globalSetup.ts │ ├── insights │ │ └── insights.test.ts │ ├── integrations │ │ ├── heartbeat.test.ts │ │ ├── integrationsTable.test.ts │ │ ├── maintenanceMode.test.ts │ │ └── uniqueIntegrationNames.test.ts │ ├── outgoingWebhooks │ │ ├── advancedWebhook.test.ts │ │ └── simpleWebhook.test.ts │ ├── pluginInitialization │ │ ├── configuration.test.ts │ │ └── initialization.test.ts │ ├── schedules │ │ ├── addOverride.test.ts │ │ ├── addRotation.test.ts │ │ ├── quality.test.ts │ │ ├── scheduleDetails.test.ts │ │ ├── scheduleView.test.ts │ │ ├── schedulesList.test.ts │ │ └── timezones.test.ts │ ├── settings │ │ └── tabs.test.ts │ ├── users │ │ ├── personalWebhook.test.ts │ │ ├── userProfile.googleConnectionTab.test.ts │ │ ├── userProfile.test.ts │ │ └── usersActions.test.ts │ └── utils │ │ ├── alertGroup.ts │ │ ├── clients │ │ └── grafana.ts │ │ ├── constants.ts │ │ ├── escalationChain.ts │ │ ├── forms.ts │ │ ├── grafanaProfile.ts │ │ ├── integrations.ts │ │ ├── modals.ts │ │ ├── navigation.ts │ │ ├── outgoingWebhooks.ts │ │ ├── phone.ts │ │ ├── schedule.ts │ │ ├── userSettings.ts │ │ └── users.ts ├── go.mod ├── go.sum ├── jest.config.js ├── jest.setup.ts ├── knip.json ├── package.json ├── pkg │ ├── main.go │ └── plugin │ │ ├── app.go │ │ ├── debug.go │ │ ├── errors.go │ │ ├── install.go │ │ ├── permissions.go │ │ ├── proxy.go │ │ ├── resources.go │ │ ├── resources_test.go │ │ ├── settings.go │ │ ├── status.go │ │ ├── sync.go │ │ ├── teams.go │ │ └── users.go ├── playwright.config.ts ├── pnpm-lock.yaml ├── src │ ├── PluginPage.tsx │ ├── app-types.ts │ ├── assets │ │ ├── img │ │ │ ├── ElastAlert.svg │ │ │ ├── HeartBeatMonitoring.png │ │ │ ├── PagerDuty.png │ │ │ ├── ServiceNow.png │ │ │ ├── apple-logo.svg │ │ │ ├── error.svg │ │ │ ├── github_star.svg │ │ │ ├── grafana-legacy-alerting-icon.svg │ │ │ ├── grafana_icon.svg │ │ │ ├── inbound-email.png │ │ │ ├── integration-logos.png │ │ │ ├── logo.svg │ │ │ ├── play-store-logo.svg │ │ │ ├── qr-code.png │ │ │ ├── screenshot.png │ │ │ └── slack_instructions.png │ │ └── style │ │ │ ├── global.css │ │ │ └── grafanaGlobalStyles.css │ ├── components │ │ ├── AlertTemplates │ │ │ ├── AlertTemplatesForm.config.ts │ │ │ └── CommonAlertTemplatesForm.config.ts │ │ ├── Avatar │ │ │ ├── Avatar.styles.ts │ │ │ ├── Avatar.test.tsx │ │ │ └── Avatar.tsx │ │ ├── Button │ │ │ └── Button.tsx │ │ ├── CardButton │ │ │ ├── CardButton.styles.ts │ │ │ ├── CardButton.test.tsx │ │ │ └── CardButton.tsx │ │ ├── CheatSheet │ │ │ ├── CheatSheet.config.ts │ │ │ ├── CheatSheet.styles.ts │ │ │ └── CheatSheet.tsx │ │ ├── Collapse │ │ │ ├── Collapse.styles.ts │ │ │ ├── Collapse.test.tsx │ │ │ └── Collapse.tsx │ │ ├── CollapsibleTreeView │ │ │ ├── CollapsibleTreeView.styles.ts │ │ │ └── CollapsibleTreeView.tsx │ │ ├── CopyToClipboardIcon │ │ │ └── CopyToClipboardIcon.tsx │ │ ├── CursorPagination │ │ │ └── CursorPagination.tsx │ │ ├── ExtensionLinkMenu │ │ │ ├── ExtensionLinkDropdown.tsx │ │ │ └── ExtensionLinkMenu.tsx │ │ ├── FullPageError │ │ │ └── FullPageError.tsx │ │ ├── GBlock │ │ │ ├── Block.styles.ts │ │ │ └── Block.tsx │ │ ├── GList │ │ │ ├── GList.styles.ts │ │ │ └── GList.tsx │ │ ├── GTable │ │ │ └── GTable.tsx │ │ ├── HamburgerContextMenu │ │ │ └── HamburgerContextMenu.tsx │ │ ├── HamburgerMenuIcon │ │ │ └── HamburgerMenuIcon.tsx │ │ ├── HowTheIntegrationWorks │ │ │ └── HowTheIntegrationWorks.tsx │ │ ├── IntegrationContactPoint │ │ │ └── IntegrationContactPoint.tsx │ │ ├── IntegrationHowToConnect │ │ │ └── IntegrationHowToConnect.tsx │ │ ├── IntegrationInputField │ │ │ ├── IntegrationInputField.styles.ts │ │ │ └── IntegrationInputField.tsx │ │ ├── IntegrationLogo │ │ │ ├── IntegrationLogo.config.ts │ │ │ ├── IntegrationLogo.tsx │ │ │ └── IntegrationLogoWithTitle.tsx │ │ ├── IntegrationSendDemoAlertModal │ │ │ └── IntegrationSendDemoAlertModal.tsx │ │ ├── Integrations │ │ │ ├── IntegrationBlock.styles.ts │ │ │ ├── IntegrationBlock.tsx │ │ │ ├── IntegrationBlockItem.tsx │ │ │ ├── IntegrationTag.tsx │ │ │ └── IntegrationTemplateBlock.tsx │ │ ├── LabelsFilter │ │ │ └── LabelsFilter.tsx │ │ ├── LabelsTooltipBadge │ │ │ └── LabelsTooltipBadge.tsx │ │ ├── ManualAlertGroup │ │ │ └── ManualAlertGroup.tsx │ │ ├── Modal │ │ │ └── Modal.tsx │ │ ├── MonacoEditor │ │ │ ├── MonacoEditor.config.ts │ │ │ ├── MonacoEditor.tsx │ │ │ └── jinja2.ts │ │ ├── NewScheduleSelector │ │ │ └── NewScheduleSelector.tsx │ │ ├── NonExistentUserName │ │ │ └── NonExistentUserName.tsx │ │ ├── PageErrorHandlingWrapper │ │ │ ├── PageErrorHandlingWrapper.helpers.tsx │ │ │ └── PageErrorHandlingWrapper.tsx │ │ ├── PluginBridge │ │ │ ├── PluginBridge.tsx │ │ │ └── PluginService.ts │ │ ├── PluginLink │ │ │ └── PluginLink.tsx │ │ ├── Policy │ │ │ ├── DragHandle.tsx │ │ │ ├── EscalationPolicy.styles.ts │ │ │ ├── EscalationPolicy.tsx │ │ │ ├── NotificationPolicy.tsx │ │ │ ├── Policy.consts.ts │ │ │ ├── Policy.styles.ts │ │ │ └── PolicyNote.tsx │ │ ├── RenderConditionally │ │ │ └── RenderConditionally.tsx │ │ ├── ScheduleBorderedAvatar │ │ │ └── ScheduleBorderedAvatar.tsx │ │ ├── ScheduleFilters │ │ │ ├── ScheduleFilters.tsx │ │ │ └── ScheduleFilters.types.ts │ │ ├── ScheduleQuality │ │ │ ├── ScheduleQuality.styles.ts │ │ │ └── ScheduleQuality.tsx │ │ ├── ScheduleQualityDetails │ │ │ ├── ScheduleQualityDetails.styles.ts │ │ │ ├── ScheduleQualityDetails.tsx │ │ │ ├── ScheduleQualityProgressBar.styles.ts │ │ │ ├── ScheduleQualityProgressBar.test.tsx │ │ │ └── ScheduleQualityProgressBar.tsx │ │ ├── SortableList │ │ │ └── SortableList.tsx │ │ ├── SourceCode │ │ │ ├── SourceCode.test.tsx │ │ │ └── SourceCode.tsx │ │ ├── Tabs │ │ │ └── Tabs.tsx │ │ ├── Tag │ │ │ └── Tag.tsx │ │ ├── Text │ │ │ ├── Text.styles.ts │ │ │ └── Text.tsx │ │ ├── TextEllipsisTooltip │ │ │ └── TextEllipsisTooltip.tsx │ │ ├── TimeRange │ │ │ └── TimeRange.tsx │ │ ├── Timeline │ │ │ ├── Timeline.styles.ts │ │ │ ├── Timeline.tsx │ │ │ └── TimelineItem.tsx │ │ ├── TooltipBadge │ │ │ ├── TooltipBadge.styles.ts │ │ │ └── TooltipBadge.tsx │ │ ├── Tutorial │ │ │ ├── Tutorial.tsx │ │ │ ├── Tutorial.types.ts │ │ │ └── icons │ │ │ │ ├── bell-icon.svg │ │ │ │ ├── calendar-icon.svg │ │ │ │ ├── chat-icon.svg │ │ │ │ ├── escalation-icon.svg │ │ │ │ └── integration-icon.svg │ │ ├── Unauthorized │ │ │ └── Unauthorized.tsx │ │ ├── UserGroups │ │ │ ├── UserGroups.helpers.ts │ │ │ ├── UserGroups.styles.ts │ │ │ ├── UserGroups.tsx │ │ │ └── UserGroups.types.ts │ │ ├── UsersFilters │ │ │ └── UsersFilters.tsx │ │ ├── VerticalTabsBar │ │ │ └── VerticalTabsBar.tsx │ │ ├── Webhooks │ │ │ ├── WebhookLastEventDetails.tsx │ │ │ ├── WebhookLastEventTimestamp.tsx │ │ │ ├── WebhookName.tsx │ │ │ └── WebhookStatusCodeBadge.tsx │ │ ├── WithConfirm │ │ │ └── WithConfirm.tsx │ │ ├── WithContextMenu │ │ │ └── WithContextMenu.tsx │ │ └── WorkingHours │ │ │ ├── WorkingHours.config.ts │ │ │ ├── WorkingHours.helpers.test.ts │ │ │ ├── WorkingHours.helpers.ts │ │ │ └── WorkingHours.tsx │ ├── containers │ │ ├── AddResponders │ │ │ ├── AddResponders.helpers.ts │ │ │ ├── AddResponders.styles.ts │ │ │ ├── AddResponders.tsx │ │ │ ├── AddResponders.types.ts │ │ │ └── parts │ │ │ │ ├── AddRespondersPopup │ │ │ │ ├── AddRespondersPopup.styles.ts │ │ │ │ └── AddRespondersPopup.tsx │ │ │ │ ├── NotificationPoliciesSelect │ │ │ │ └── NotificationPoliciesSelect.tsx │ │ │ │ ├── TeamResponder │ │ │ │ ├── TeamResponder.test.tsx │ │ │ │ └── TeamResponder.tsx │ │ │ │ └── UserResponder │ │ │ │ ├── UserResponder.test.tsx │ │ │ │ └── UserResponder.tsx │ │ ├── AlertRules │ │ │ ├── AlertRules.tsx │ │ │ └── parts │ │ │ │ └── connectors │ │ │ │ ├── Connectors.styles.ts │ │ │ │ ├── MSTeamsConnector.tsx │ │ │ │ ├── MattermostConnector.tsx │ │ │ │ ├── SlackConnector.tsx │ │ │ │ └── TelegramConnector.tsx │ │ ├── Alerts │ │ │ └── Alerts.tsx │ │ ├── ApiTokenSettings │ │ │ ├── ApiTokenForm.styles.ts │ │ │ ├── ApiTokenForm.tsx │ │ │ └── ApiTokenSettings.tsx │ │ ├── AttachIncidentForm │ │ │ └── AttachIncidentForm.tsx │ │ ├── ColumnsSelector │ │ │ ├── ColumnsSelector.styles.ts │ │ │ └── ColumnsSelector.tsx │ │ ├── ColumnsSelectorWrapper │ │ │ ├── ColumnsModal.tsx │ │ │ ├── ColumnsSelectorWrapper.styles.ts │ │ │ └── ColumnsSelectorWrapper.tsx │ │ ├── DefaultPageLayout │ │ │ ├── DefaultPageLayout.helpers.tsx │ │ │ ├── DefaultPageLayout.tsx │ │ │ ├── DefaultPageLayout.types.ts │ │ │ └── helper.ts │ │ ├── EditRegexpRouteTemplateModal │ │ │ └── EditRegexpRouteTemplateModal.tsx │ │ ├── EscalationChainCard │ │ │ └── EscalationChainCard.tsx │ │ ├── EscalationChainForm │ │ │ └── EscalationChainForm.tsx │ │ ├── EscalationChainSteps │ │ │ └── EscalationChainSteps.tsx │ │ ├── GSelect │ │ │ └── GSelect.tsx │ │ ├── GrafanaTeamSelect │ │ │ └── GrafanaTeamSelect.tsx │ │ ├── IncidentsFilters │ │ │ └── IncidentFilters.types.ts │ │ ├── IntegrationContainers │ │ │ ├── CollapsedIntegrationRouteDisplay │ │ │ │ └── CollapsedIntegrationRouteDisplay.tsx │ │ │ ├── ExpandedIntegrationRouteDisplay │ │ │ │ ├── ExpandedIntegrationRouteDisplay.styles.ts │ │ │ │ └── ExpandedIntegrationRouteDisplay.tsx │ │ │ ├── IntegrationCommonTemplatesList.config.ts │ │ │ ├── IntegrationHeartbeatForm │ │ │ │ └── IntegrationHeartbeatForm.tsx │ │ │ ├── IntegrationTemplatesList.config.ts │ │ │ ├── IntegrationTemplatesList.tsx │ │ │ └── RouteHeading.tsx │ │ ├── IntegrationForm │ │ │ ├── IntegrationForm.helpers.ts │ │ │ ├── IntegrationForm.styles.ts │ │ │ ├── IntegrationForm.tsx │ │ │ ├── IntegrationFormContainer.styles.ts │ │ │ └── IntegrationFormContainer.tsx │ │ ├── IntegrationLabelsForm │ │ │ ├── IntegrationLabelsForm.helpers.test.ts │ │ │ ├── IntegrationLabelsForm.helpers.ts │ │ │ └── IntegrationLabelsForm.tsx │ │ ├── IntegrationTemplate │ │ │ ├── IntegrationTemplate.styles.ts │ │ │ └── IntegrationTemplate.tsx │ │ ├── Labels │ │ │ ├── Labels.tsx │ │ │ └── LabelsFilter.tsx │ │ ├── MSTeams │ │ │ └── MSTeamsInstructions.tsx │ │ ├── MSTeamsIntegrationButton │ │ │ └── MSTeamsIntegrationButton.tsx │ │ ├── MaintenanceForm │ │ │ └── MaintenanceForm.tsx │ │ ├── MattermostIntegrationButton │ │ │ └── MattermostIntegrationButton.tsx │ │ ├── MobileAppConnection │ │ │ ├── MobileAppConnection.styles.ts │ │ │ ├── MobileAppConnection.test.tsx │ │ │ ├── MobileAppConnection.tsx │ │ │ ├── MobileAppConnectionTab.tsx │ │ │ └── parts │ │ │ │ ├── DisconnectButton │ │ │ │ ├── DisconnectButton.test.tsx │ │ │ │ └── DisconnectButton.tsx │ │ │ │ ├── DownloadIcons │ │ │ │ └── DownloadIcons.tsx │ │ │ │ ├── LinkLoginButton │ │ │ │ └── LinkLoginButton.tsx │ │ │ │ └── QRCode │ │ │ │ └── QRCode.tsx │ │ ├── OutgoingWebhookForm │ │ │ ├── CommonWebhookPresetIcons.config.tsx │ │ │ ├── OutgoingWebhookForm.styles.ts │ │ │ ├── OutgoingWebhookForm.tsx │ │ │ ├── OutgoingWebhookForm.types.ts │ │ │ ├── OutgoingWebhookFormFields.tsx │ │ │ ├── WebhookPresetBlocks.tsx │ │ │ └── WebhookPresetIcons.config.tsx │ │ ├── OutgoingWebhookStatus │ │ │ └── OutgoingWebhookStatus.tsx │ │ ├── PersonalNotificationSettings │ │ │ ├── PersonalNotificationSettings.helpers.ts │ │ │ ├── PersonalNotificationSettings.tsx │ │ │ └── img │ │ │ │ └── default-step.png │ │ ├── PluginConfigPage │ │ │ └── PluginConfigPage.tsx │ │ ├── PluginInitializer │ │ │ └── PluginInitializer.tsx │ │ ├── RemoteFilters │ │ │ ├── RemoteFilters.helpers.ts │ │ │ ├── RemoteFilters.tsx │ │ │ ├── RemoteFilters.types.ts │ │ │ └── TimeRangePickerWrapper.tsx │ │ ├── RemoteSelect │ │ │ └── RemoteSelect.tsx │ │ ├── Rotation │ │ │ ├── Rotation.styles.ts │ │ │ └── Rotation.tsx │ │ ├── RotationForm │ │ │ ├── RotationForm.helpers.test.ts │ │ │ ├── RotationForm.helpers.ts │ │ │ ├── RotationForm.styles.ts │ │ │ ├── RotationForm.tsx │ │ │ ├── RotationForm.types.ts │ │ │ ├── ScheduleOverrideForm.tsx │ │ │ ├── ShiftSwapForm.tsx │ │ │ └── parts │ │ │ │ ├── DateTimePicker.tsx │ │ │ │ ├── DaysSelector.tsx │ │ │ │ ├── DeletionModal.tsx │ │ │ │ ├── TimeUnitSelector.tsx │ │ │ │ └── UserItem.tsx │ │ ├── Rotations │ │ │ ├── Animations.styles.ts │ │ │ ├── Rotations.config.ts │ │ │ ├── Rotations.helpers.ts │ │ │ ├── Rotations.styles.ts │ │ │ ├── Rotations.tsx │ │ │ ├── ScheduleFinal.tsx │ │ │ ├── ScheduleOverrides.tsx │ │ │ └── SchedulePersonal.tsx │ │ ├── RouteLabelsDisplay │ │ │ └── RouteLabelsDisplay.tsx │ │ ├── ScheduleForm │ │ │ ├── ScheduleForm.helpers.ts │ │ │ └── ScheduleForm.tsx │ │ ├── ScheduleIcalLink │ │ │ └── ScheduleIcalLink.tsx │ │ ├── ScheduleSlot │ │ │ ├── ScheduleSlot.helpers.ts │ │ │ └── ScheduleSlot.tsx │ │ ├── ServiceNowConfigDrawer │ │ │ ├── CompleteServiceNowConfigModal.tsx │ │ │ ├── ServiceNow.styles.ts │ │ │ ├── ServiceNowAuthSection.tsx │ │ │ ├── ServiceNowConfig.helpers.ts │ │ │ ├── ServiceNowConfigDrawer.tsx │ │ │ ├── ServiceNowStatusSection.tsx │ │ │ └── ServiceNowTokenSection.tsx │ │ ├── TeamName │ │ │ └── TeamName.tsx │ │ ├── TeamsList │ │ │ └── TeamsList.tsx │ │ ├── TelegramIntegrationButton │ │ │ └── TelegramIntegrationButton.tsx │ │ ├── TemplatePreview │ │ │ ├── TemplatePreview.styles.ts │ │ │ └── TemplatePreview.tsx │ │ ├── TemplateResult │ │ │ └── TemplateResult.tsx │ │ ├── TemplatesAlertGroupsList │ │ │ ├── TemplatesAlertGroupsList.styles.ts │ │ │ └── TemplatesAlertGroupsList.tsx │ │ ├── TimelineMarks │ │ │ ├── TimelineMarks.styles.ts │ │ │ └── TimelineMarks.tsx │ │ ├── UserDisplay │ │ │ └── UserDisplayWithAvatar.tsx │ │ ├── UserSettings │ │ │ ├── UserSettings.tsx │ │ │ ├── UserSettings.types.ts │ │ │ └── parts │ │ │ │ ├── UserSettingsParts.tsx │ │ │ │ ├── connectors │ │ │ │ ├── Connectors.tsx │ │ │ │ ├── GoogleConnector.tsx │ │ │ │ ├── ICalConnector.tsx │ │ │ │ ├── MSTeamsConnector.tsx │ │ │ │ ├── MattermostConnector.tsx │ │ │ │ ├── MobileAppConnector.tsx │ │ │ │ ├── PersonalWebhookConnector.tsx │ │ │ │ ├── PhoneConnector.tsx │ │ │ │ ├── SlackConnector.tsx │ │ │ │ └── TelegramConnector.tsx │ │ │ │ └── tabs │ │ │ │ ├── CloudPhoneSettings │ │ │ │ └── CloudPhoneSettings.tsx │ │ │ │ ├── GoogleCalendar │ │ │ │ └── GoogleCalendar.tsx │ │ │ │ ├── MSTeamsInfo │ │ │ │ └── MSTeamsInfo.tsx │ │ │ │ ├── MattermostInfo │ │ │ │ └── MattermostInfo.tsx │ │ │ │ ├── NotificationSettingsTab.tsx │ │ │ │ ├── PersonalWebhookInfo │ │ │ │ └── PersonalWebhookInfo.tsx │ │ │ │ ├── PhoneVerification │ │ │ │ └── PhoneVerification.tsx │ │ │ │ ├── SlackTab │ │ │ │ └── SlackTab.tsx │ │ │ │ ├── TelegramInfo │ │ │ │ └── TelegramInfo.tsx │ │ │ │ └── UserInfoTab │ │ │ │ └── UserInfoTab.tsx │ │ ├── UserTimezoneSelect │ │ │ └── UserTimezoneSelect.tsx │ │ ├── UserTooltip │ │ │ └── UserTooltip.tsx │ │ ├── UsersTimezones │ │ │ ├── ScheduleUserDetails │ │ │ │ ├── ScheduleUserDetails.tsx │ │ │ │ └── img │ │ │ │ │ └── line.svg │ │ │ ├── UserTimezones.styles.ts │ │ │ ├── UsersTimezones.helpers.ts │ │ │ └── UsersTimezones.tsx │ │ ├── WebhooksTemplateEditor │ │ │ └── WebhooksTemplateEditor.tsx │ │ └── WithPermissionControl │ │ │ ├── WithPermissionControlDisplay.tsx │ │ │ └── WithPermissionControlTooltip.tsx │ ├── dashboards │ │ └── oncall_metrics_dashboard.json │ ├── helpers │ │ ├── DOM.ts │ │ ├── LocationHelper.ts │ │ ├── async.test.ts │ │ ├── async.ts │ │ ├── authorization │ │ │ ├── authorization.test.ts │ │ │ └── authorization.ts │ │ ├── consts.ts │ │ ├── datetime.test.ts │ │ ├── datetime.ts │ │ ├── decorators.ts │ │ ├── faro.test.tsx │ │ ├── faro.ts │ │ ├── helpers.test.ts │ │ ├── helpers.ts │ │ ├── hoc.tsx │ │ ├── hooks.tsx │ │ ├── loadJs.ts │ │ ├── localStorage.ts │ │ ├── sanitize.ts │ │ ├── string.ts │ │ ├── styles.ts │ │ ├── types.ts │ │ └── url.ts │ ├── icons │ │ ├── GoogleCalendarLogo.tsx │ │ ├── GoogleLogo.tsx │ │ ├── Icons.tsx │ │ ├── MSTeamsLogo.tsx │ │ └── MachineLearningLogo.tsx │ ├── index.d.ts │ ├── jest │ │ ├── grafanaMock.ts │ │ ├── matchMedia.ts │ │ ├── openapiFetchMock.ts │ │ ├── styleMock.ts │ │ ├── svgTransform.ts │ │ └── utils.ts │ ├── models │ │ ├── alert_receive_channel │ │ │ ├── alert_receive_channel.helpers.ts │ │ │ ├── alert_receive_channel.ts │ │ │ └── alert_receive_channel.types.ts │ │ ├── alert_receive_channel_connected_channels │ │ │ └── alert_receive_channel_connected_channels.ts │ │ ├── alert_receive_channel_filters │ │ │ └── alert_receive_channel_filters.ts │ │ ├── alert_receive_channel_webhooks │ │ │ └── alert_receive_channel_webhooks.ts │ │ ├── alert_templates │ │ │ └── alert_templates.ts │ │ ├── alertgroup │ │ │ ├── alertgroup.helpers.ts │ │ │ ├── alertgroup.ts │ │ │ └── alertgroup.types.ts │ │ ├── api_token │ │ │ ├── api_token.ts │ │ │ └── api_token.types.ts │ │ ├── base_store.ts │ │ ├── channel │ │ │ └── channel.ts │ │ ├── channel_filter │ │ │ └── channel_filter.types.ts │ │ ├── cloud │ │ │ ├── cloud.ts │ │ │ └── cloud.types.ts │ │ ├── direct_paging │ │ │ ├── direct_paging.test.ts │ │ │ ├── direct_paging.ts │ │ │ └── direct_paging.types.ts │ │ ├── escalation_chain │ │ │ ├── escalation_chain.ts │ │ │ └── escalation_chain.types.ts │ │ ├── escalation_policy │ │ │ ├── escalation_policy.helpers.ts │ │ │ ├── escalation_policy.ts │ │ │ └── escalation_policy.types.ts │ │ ├── filters │ │ │ ├── filters.helpers.ts │ │ │ ├── filters.ts │ │ │ └── filters.types.ts │ │ ├── global_setting │ │ │ ├── global_setting.ts │ │ │ └── global_setting.types.ts │ │ ├── grafana_team │ │ │ ├── grafana_team.ts │ │ │ └── grafana_team.types.ts │ │ ├── heartbeat │ │ │ ├── heartbeat.ts │ │ │ └── heartbeat.types.ts │ │ ├── label │ │ │ ├── label.helpers.ts │ │ │ ├── label.ts │ │ │ └── label.types.ts │ │ ├── loader │ │ │ ├── action-keys.ts │ │ │ ├── loader.helpers.ts │ │ │ └── loader.ts │ │ ├── mattermost │ │ │ ├── mattermost.ts │ │ │ ├── mattermost.types.ts │ │ │ └── mattermost_channel.ts │ │ ├── msteams_channel │ │ │ ├── msteams_channel.ts │ │ │ └── msteams_channel.types.ts │ │ ├── notification_policy │ │ │ └── notification_policy.ts │ │ ├── organization │ │ │ ├── organization.ts │ │ │ └── organization.types.ts │ │ ├── outgoing_webhook │ │ │ ├── outgoing_webhook.ts │ │ │ └── outgoing_webhook.types.ts │ │ ├── plugin │ │ │ ├── plugin.helper.ts │ │ │ └── plugin.ts │ │ ├── resolution_note │ │ │ ├── resolution_note.ts │ │ │ └── resolution_note.types.ts │ │ ├── schedule │ │ │ ├── schedule.helpers.test.ts │ │ │ ├── schedule.helpers.ts │ │ │ ├── schedule.ts │ │ │ └── schedule.types.ts │ │ ├── slack │ │ │ ├── slack.ts │ │ │ └── slack.types.ts │ │ ├── slack_channel │ │ │ ├── slack_channel.config.ts │ │ │ ├── slack_channel.helpers.ts │ │ │ ├── slack_channel.ts │ │ │ └── slack_channel.types.ts │ │ ├── telegram_channel │ │ │ ├── telegram_channel.ts │ │ │ └── telegram_channel.types.ts │ │ ├── timezone │ │ │ ├── timezone.helpers.ts │ │ │ ├── timezone.ts │ │ │ └── timezone.types.ts │ │ ├── user │ │ │ ├── user.helpers.tsx │ │ │ └── user.ts │ │ └── user_group │ │ │ ├── user_group.ts │ │ │ └── user_group.types.ts │ ├── module.ts │ ├── navbar │ │ ├── Header │ │ │ ├── Header.styles.ts │ │ │ └── Header.tsx │ │ ├── LegacyNavHeading.tsx │ │ └── LegacyNavTabsBar.tsx │ ├── network │ │ ├── grafana-api │ │ │ ├── api.types.d.ts │ │ │ └── http-client.ts │ │ ├── network.ts │ │ └── oncall-api │ │ │ ├── api.types.d.ts │ │ │ ├── autogenerated-api.types.d.ts │ │ │ ├── http-client.test.ts │ │ │ ├── http-client.ts │ │ │ └── types-generator │ │ │ ├── custom-schemas.ts │ │ │ ├── generate-types.ts │ │ │ ├── package.json │ │ │ └── tsconfig.json │ ├── pages │ │ ├── NoMatch.tsx │ │ ├── escalation-chains │ │ │ ├── EscalationChains.styles.ts │ │ │ └── EscalationChains.tsx │ │ ├── incident │ │ │ ├── Incident.helpers.tsx │ │ │ ├── Incident.styles.ts │ │ │ └── Incident.tsx │ │ ├── incidents │ │ │ ├── Incidents.styles.ts │ │ │ ├── Incidents.tsx │ │ │ └── parts │ │ │ │ ├── IncidentDropdown.styles.ts │ │ │ │ ├── IncidentDropdown.tsx │ │ │ │ ├── IncidentSilenceModal.tsx │ │ │ │ └── SilenceSelect.tsx │ │ ├── insights │ │ │ ├── Insights.hooks.ts │ │ │ ├── Insights.tsx │ │ │ ├── Insights.types.ts │ │ │ ├── scenes │ │ │ │ ├── AlertGroupsByIntegration.tsx │ │ │ │ ├── AlertGroupsByTeam.tsx │ │ │ │ ├── MTTRAverageStat.tsx │ │ │ │ ├── MTTRByIntegration.tsx │ │ │ │ ├── MTTRByTeam.tsx │ │ │ │ ├── MTTRChangedTimeseries.tsx │ │ │ │ ├── NewAlertGroups.tsx │ │ │ │ ├── NewAlertGroupsNotificationsTable.tsx │ │ │ │ ├── NewAlertGroupsNotificationsTimeseries.tsx │ │ │ │ └── NewAlertGroupsTimeseries.tsx │ │ │ └── variables.ts │ │ ├── integration │ │ │ ├── CommonIntegration.helper.ts │ │ │ ├── Integration.helper.ts │ │ │ ├── Integration.hooks.ts │ │ │ ├── Integration.styles.ts │ │ │ ├── Integration.tsx │ │ │ ├── IntegrationActions.tsx │ │ │ ├── IntegrationCommon.config.ts │ │ │ └── OutgoingTab │ │ │ │ ├── ConnectIntegrationModal.tsx │ │ │ │ ├── ConnectedIntegrationsTable.tsx │ │ │ │ ├── NewOutgoingWebhookDrawerContent.tsx │ │ │ │ ├── OtherIntegrations.tsx │ │ │ │ ├── OutgoingTab.hooks.ts │ │ │ │ ├── OutgoingTab.styles.ts │ │ │ │ ├── OutgoingTab.tsx │ │ │ │ ├── OutgoingTab.types.ts │ │ │ │ ├── OutgoingWebhookDetailsDrawerTabs.tsx │ │ │ │ ├── OutgoingWebhookFormFields.tsx │ │ │ │ └── OutgoingWebhooksTable.tsx │ │ ├── integrations │ │ │ ├── Integrations.styles.tsx │ │ │ └── Integrations.tsx │ │ ├── outgoing_webhooks │ │ │ ├── OutgoingWebhooks.tsx │ │ │ └── OutgoingWebhooks.types.ts │ │ ├── pages.tsx │ │ ├── schedule │ │ │ ├── Schedule.helpers.test.ts │ │ │ ├── Schedule.helpers.ts │ │ │ ├── Schedule.styles.ts │ │ │ └── Schedule.tsx │ │ ├── schedules │ │ │ ├── Schedules.styles.ts │ │ │ └── Schedules.tsx │ │ ├── settings │ │ │ ├── SettingsPage.tsx │ │ │ ├── SettingsPage.types.ts │ │ │ └── tabs │ │ │ │ ├── ChatOps │ │ │ │ ├── ChatOps.helpers.ts │ │ │ │ ├── ChatOps.tsx │ │ │ │ └── tabs │ │ │ │ │ ├── MSTeamsSettings │ │ │ │ │ └── MSTeamsSettings.tsx │ │ │ │ │ ├── MattermostSettings │ │ │ │ │ └── MattermostSettings.tsx │ │ │ │ │ ├── SlackSettings │ │ │ │ │ ├── SlackSettings.styles.ts │ │ │ │ │ └── SlackSettings.tsx │ │ │ │ │ └── TelegramSettings │ │ │ │ │ └── TelegramSettings.tsx │ │ │ │ ├── Cloud │ │ │ │ ├── CloudPage.styles.ts │ │ │ │ └── CloudPage.tsx │ │ │ │ ├── LiveSettings │ │ │ │ ├── LiveSettings.config.ts │ │ │ │ ├── LiveSettings.helpers.ts │ │ │ │ └── LiveSettingsPage.tsx │ │ │ │ ├── MainSettings │ │ │ │ └── MainSettings.tsx │ │ │ │ └── TeamsSettings │ │ │ │ └── TeamsSettings.tsx │ │ └── users │ │ │ ├── Users.styles.ts │ │ │ └── Users.tsx │ ├── plugin.json │ ├── plugin │ │ ├── GrafanaPluginRootPage.helpers.test.tsx │ │ ├── GrafanaPluginRootPage.helpers.tsx │ │ ├── GrafanaPluginRootPage.tsx │ │ └── dayjs.ts │ ├── state │ │ ├── features.ts │ │ ├── helpers.ts │ │ ├── rootBaseStore │ │ │ └── RootBaseStore.ts │ │ ├── rootStore.ts │ │ ├── types.ts │ │ ├── useStore.ts │ │ └── withStore.tsx │ ├── styles │ │ └── utils.styles.ts │ └── version.ts ├── tools │ └── eslint-rules │ │ └── no-relative-import-paths.js ├── tsconfig.json └── webpack.config.ts ├── helm ├── README.md ├── cr.yaml ├── ct.yaml ├── kind.yml ├── local_image.yml ├── oncall │ ├── .helmignore │ ├── Chart.yaml │ ├── README.md │ ├── charts │ │ ├── cert-manager-v1.8.0.tgz │ │ ├── grafana-8.4.6.tgz │ │ ├── ingress-nginx-4.1.4.tgz │ │ ├── mariadb-12.2.5.tgz │ │ ├── postgresql-11.9.10.tgz │ │ ├── prometheus-25.8.2.tgz │ │ ├── rabbitmq-12.0.0.tgz │ │ └── redis-16.13.2.tgz │ ├── templates │ │ ├── NOTES.txt │ │ ├── _env.tpl │ │ ├── _helpers.tpl │ │ ├── celery │ │ │ ├── _helpers.tpl │ │ │ └── deployment.yaml │ │ ├── cert-issuer.yaml │ │ ├── engine │ │ │ ├── _helpers-engine.tpl │ │ │ ├── deployment.yaml │ │ │ ├── job-migrate.yaml │ │ │ ├── service-external.yaml │ │ │ └── service-internal.yaml │ │ ├── ingress-regular.yaml │ │ ├── integrations │ │ │ ├── _helpers.tpl │ │ │ ├── deployment.yaml │ │ │ ├── service-external.yaml │ │ │ └── service-internal.yaml │ │ ├── plugin-provisioning.yaml │ │ ├── secrets.yaml │ │ ├── serviceaccount.yaml │ │ ├── telegram-polling │ │ │ ├── _helpers.tpl │ │ │ └── deployment.yaml │ │ └── ui │ │ │ ├── _helpers.tpl │ │ │ └── deployment.yaml │ ├── tests │ │ ├── __snapshot__ │ │ │ ├── affinity_deployments_test.yaml.snap │ │ │ ├── dev_mode_db_migrate_job_volumes_test.yaml.snap │ │ │ ├── integrations_deployment_test.yaml.snap │ │ │ ├── nodeselector_deployments_test.yaml.snap │ │ │ ├── telegram_polling_deployment_test.yaml.snap │ │ │ ├── tolerations_deployments_test.yaml.snap │ │ │ ├── topology_deployments_test.yaml.snap │ │ │ ├── ui_test.yaml.snap │ │ │ └── wait_for_db_test.yaml.snap │ │ ├── affinity_deployments_test.yaml │ │ ├── broker_secret_test.yaml │ │ ├── dev_mode_volumes_test.yaml │ │ ├── extra_containers_celery_test.yaml │ │ ├── extra_containers_engine_test.yaml │ │ ├── extra_env_test.yaml │ │ ├── extra_volume_mounts_test.yaml │ │ ├── image_deployments_test.yaml │ │ ├── image_pull_secrets_test.yaml │ │ ├── integrations_deployment_test.yaml │ │ ├── migrate_annotations_test.yaml │ │ ├── migrate_extra_containers_test.yaml │ │ ├── migrate_resources.yaml │ │ ├── mysql_env_test.yaml │ │ ├── mysql_password_env_test.yaml │ │ ├── nodeselector_deployments_test.yaml │ │ ├── podlabels_test.yaml │ │ ├── postgres_env_test.yaml │ │ ├── postgres_password_env_test.yaml │ │ ├── priority_class_deployments_test.yaml │ │ ├── rabbitmq_env_test.yaml │ │ ├── redis_env_test.yaml │ │ ├── redis_password_env_test.yaml │ │ ├── security_context_deployments_test.yaml │ │ ├── service_account_deployments_test.yaml │ │ ├── service_account_test.yaml │ │ ├── smtp_env_test.yaml │ │ ├── telegram_env_test.yaml │ │ ├── telegram_polling_deployment_test.yaml │ │ ├── tolerations_deployments_test.yaml │ │ ├── topology_deployments_test.yaml │ │ ├── twilio_auth_env_test.yaml │ │ ├── ui_test.yaml │ │ ├── uwsgi_env_test.yaml │ │ └── wait_for_db_test.yaml │ └── values.yaml └── simple.yml ├── terraform ├── README.md └── examples │ ├── basic.tf │ ├── routes.tf │ └── shift_schedule.tf └── tools ├── migrators ├── .gitignore ├── .isort.cfg ├── Dockerfile ├── README.md ├── add_users_to_grafana.py ├── lib │ ├── __init__.py │ ├── base_config.py │ ├── common │ │ ├── __init__.py │ │ ├── report.py │ │ └── resources │ │ │ ├── __init__.py │ │ │ ├── teams.py │ │ │ └── users.py │ ├── constants.py │ ├── grafana │ │ ├── __init__.py │ │ ├── api_client.py │ │ └── service_model_client.py │ ├── network.py │ ├── oncall │ │ ├── __init__.py │ │ ├── api_client.py │ │ └── types.py │ ├── opsgenie │ │ ├── api_client.py │ │ ├── config.py │ │ ├── migrate.py │ │ ├── report.py │ │ └── resources │ │ │ ├── escalation_policies.py │ │ │ ├── integrations.py │ │ │ ├── notification_rules.py │ │ │ ├── schedules.py │ │ │ └── users.py │ ├── pagerduty │ │ ├── __init__.py │ │ ├── config.py │ │ ├── migrate.py │ │ ├── report.py │ │ └── resources │ │ │ ├── __init__.py │ │ │ ├── business_service.py │ │ │ ├── escalation_policies.py │ │ │ ├── integrations.py │ │ │ ├── notification_rules.py │ │ │ ├── rulesets.py │ │ │ ├── schedules.py │ │ │ ├── services.py │ │ │ └── users.py │ ├── session.py │ ├── splunk │ │ ├── __init__.py │ │ ├── api_client.py │ │ ├── config.py │ │ ├── migrate.py │ │ ├── report.py │ │ ├── resources │ │ │ ├── __init__.py │ │ │ ├── escalation_policies.py │ │ │ ├── paging_policies.py │ │ │ └── schedules.py │ │ └── types.py │ ├── tests │ │ ├── __init__.py │ │ ├── common │ │ │ ├── __init__.py │ │ │ └── resources │ │ │ │ ├── __init__.py │ │ │ │ └── test_user.py │ │ ├── opsgenie │ │ │ ├── __init__.py │ │ │ ├── resources │ │ │ │ ├── __init__.py │ │ │ │ ├── test_escalation_policies.py │ │ │ │ ├── test_integrations.py │ │ │ │ ├── test_notification_rules.py │ │ │ │ ├── test_schedules.py │ │ │ │ └── test_users.py │ │ │ └── test_report.py │ │ ├── pagerduty │ │ │ ├── __init__.py │ │ │ ├── resources │ │ │ │ ├── __init__.py │ │ │ │ ├── test_escalation_policies.py │ │ │ │ ├── test_integrations.py │ │ │ │ ├── test_notification_rules.py │ │ │ │ ├── test_schedules.py │ │ │ │ ├── test_services.py │ │ │ │ └── test_users.py │ │ │ ├── test_matching.py │ │ │ └── test_migrate.py │ │ ├── splunk │ │ │ ├── __init__.py │ │ │ └── resources │ │ │ │ ├── __init__.py │ │ │ │ ├── test_escalation_policies.py │ │ │ │ ├── test_paging_policies.py │ │ │ │ └── test_schedules.py │ │ ├── test_add_users_to_grafana.py │ │ ├── test_session.py │ │ └── test_utils.py │ └── utils.py ├── main.py ├── pytest.ini ├── requirements.in └── requirements.txt ├── scripts ├── discord_webhooks.py ├── mattermost_webhooks.py ├── oncall_hours_report.py ├── oncall_reports.py ├── page_each_user.py ├── readme.md ├── shift_shifts.py └── swap_requests_workday.py └── twilio ├── basic_flow.json └── flow_with_routes.json /.dockerignore: -------------------------------------------------------------------------------- 1 | # Ignore everything 2 | * 3 | 4 | # Allow directories 5 | !/engine 6 | !/grafana-plugin -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.whl filter=lfs diff=lfs merge=lfs -text 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | -------------------------------------------------------------------------------- /.github/helm-ci.yml: -------------------------------------------------------------------------------- 1 | env: 2 | - name: GRAFANA_CLOUD_NOTIFICATIONS_ENABLED 3 | value: "False" 4 | - name: FEATURE_PROMETHEUS_EXPORTER_ENABLED 5 | value: "True" 6 | # enabled to be able to test docker.host.internal in the webhook e2e tests 7 | - name: DANGEROUS_WEBHOOKS_ENABLED 8 | value: "True" 9 | - name: FEATURE_LABELS_ENABLED_FOR_ALL 10 | value: "True" 11 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | changelog: 2 | exclude: 3 | labels: 4 | - release:ignore 5 | categories: 6 | - title: Breaking Changes 🛠 7 | labels: 8 | - release:breaking-change 9 | - title: Exciting New Features 🎉 10 | labels: 11 | - release:enhancement 12 | - title: Other Changes 13 | labels: 14 | - release:patch 15 | -------------------------------------------------------------------------------- /.github/workflows/on-issue-closed.yml: -------------------------------------------------------------------------------- 1 | name: On issue closed 2 | 3 | on: 4 | issues: 5 | types: 6 | - closed 7 | 8 | jobs: 9 | remove-needs-triage-label: 10 | name: Remove "needs triage" label 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: Remove "needs triage" label 15 | uses: actions-ecosystem/action-remove-labels@2ce5d41b4b6aa8503e285553f75ed56e0a40bae0 #v1.3.0 16 | with: 17 | labels: needs triage 18 | -------------------------------------------------------------------------------- /.github/workflows/update-make-docs.yml: -------------------------------------------------------------------------------- 1 | name: Update make docs procedure 2 | 3 | on: 4 | schedule: 5 | - cron: "0 7 * * 1-5" 6 | workflow_dispatch: 7 | 8 | jobs: 9 | main: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | - uses: grafana/writers-toolkit/update-make-docs@f65819d6a412b752c0e0263375215f049507b0e6 #update-make-docs/v1.3.0 14 | with: 15 | pr_options: --label "release:ignore" 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | venv 3 | .python-version 4 | 5 | .vscode 6 | *.http 7 | .idea 8 | .DS_Store 9 | .env 10 | 11 | pnpm.lock 12 | node_modules 13 | 14 | test-results -------------------------------------------------------------------------------- /.markdownlint.json: -------------------------------------------------------------------------------- 1 | { 2 | "default": true, 3 | "MD013": { 4 | "line_length": "120", 5 | "code_blocks": false, 6 | "tables": false 7 | }, 8 | "MD024": { 9 | "siblings_only": true 10 | }, 11 | "MD033": { 12 | "allowed_elements": ["img"] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.markdownlintignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | venv 3 | .python-version 4 | 5 | .vscode 6 | .idea 7 | .DS_Store 8 | .env 9 | 10 | CHANGELOG.md 11 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 20.16.0 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | **/*.md -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | overrides: [ 2 | { 3 | files: ["*.yml", "*.yaml"], 4 | options: { 5 | singleQuote: false, 6 | }, 7 | }, 8 | ]; 9 | -------------------------------------------------------------------------------- /.tilt/backend/Tiltfile: -------------------------------------------------------------------------------- 1 | label = "OnCall.Backend" 2 | 3 | k8s_resource( 4 | workload="celery", 5 | resource_deps=["mariadb", "redis-master"], 6 | labels=[label], 7 | ) 8 | 9 | k8s_resource( 10 | workload="engine", 11 | port_forwards=8080, 12 | resource_deps=["mariadb", "redis-master"], 13 | labels=[label], 14 | ) 15 | 16 | k8s_resource(workload="engine-migrate", labels=[label]) -------------------------------------------------------------------------------- /.tilt/deps/Tiltfile: -------------------------------------------------------------------------------- 1 | label = "OnCall.Deps" 2 | 3 | k8s_resource(workload="redis-master", labels=[label]) 4 | 5 | k8s_resource(workload="prometheus-server", labels=[label]) 6 | 7 | k8s_resource( 8 | workload="mariadb", 9 | port_forwards='3307:3306', # : 10 | labels=[label], 11 | ) -------------------------------------------------------------------------------- /.yamllint.yml: -------------------------------------------------------------------------------- 1 | extends: default 2 | 3 | ignore: | 4 | helm/oncall/templates/**/*.yaml 5 | pnpm-lock.yaml 6 | 7 | rules: 8 | line-length: 9 | max: 120 10 | 11 | comments: 12 | min-spaces-from-content: 1 13 | 14 | document-start: disable 15 | -------------------------------------------------------------------------------- /dev/.env.mysql.dev: -------------------------------------------------------------------------------- 1 | DATABASE_USER=root 2 | DATABASE_NAME=oncall_local_dev 3 | DATABASE_PASSWORD=empty 4 | DATABASE_HOST=mysql 5 | DATABASE_PORT=3306 6 | 7 | # specific for the grafana container 8 | GF_DATABASE_TYPE=mysql 9 | GF_DATABASE_HOST=mysql:3306 10 | GF_DATABASE_USER=root 11 | GF_DATABASE_PASSWORD=empty 12 | GF_DATABASE_SSL_MODE=disable 13 | -------------------------------------------------------------------------------- /dev/.env.postgres.dev: -------------------------------------------------------------------------------- 1 | DATABASE_TYPE=postgresql 2 | DATABASE_NAME=oncall_local_dev 3 | DATABASE_USER=postgres 4 | DATABASE_PASSWORD=empty 5 | DATABASE_HOST=postgres 6 | DATABASE_PORT=5432 7 | 8 | # specific for the grafana container 9 | GF_DATABASE_TYPE=postgres 10 | GF_DATABASE_HOST=postgres:5432 11 | GF_DATABASE_NAME=grafana 12 | GF_DATABASE_USER=postgres 13 | GF_DATABASE_PASSWORD=empty 14 | -------------------------------------------------------------------------------- /dev/.env.sqlite.dev: -------------------------------------------------------------------------------- 1 | DATABASE_TYPE=sqlite3 2 | DATABASE_NAME=/var/lib/oncall/oncall.db 3 | -------------------------------------------------------------------------------- /dev/grafana/provisioning/datasources/automatic.yml: -------------------------------------------------------------------------------- 1 | apiVersion: 1 2 | 3 | datasources: 4 | - name: Prometheus 5 | type: prometheus 6 | access: proxy 7 | url: http://host.docker.internal:9090 8 | jsonData: 9 | httpMethod: POST 10 | manageAlerts: true 11 | prometheusType: Prometheus 12 | -------------------------------------------------------------------------------- /dev/grafana/provisioning/plugins/grafana-oncall-app-provisioning.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: 1 2 | apps: 3 | - type: grafana-oncall-app 4 | name: grafana-oncall-app 5 | jsonData: 6 | stackId: 5 7 | orgId: 100 8 | onCallApiUrl: $ONCALL_API_URL 9 | -------------------------------------------------------------------------------- /dev/kind-config.yaml: -------------------------------------------------------------------------------- 1 | # Based on https://github.com/grafana/ops-devenv 2 | apiVersion: ctlptl.dev/v1alpha1 3 | kind: Cluster 4 | product: kind 5 | registry: ctlptl-registry 6 | kindV1Alpha4Cluster: 7 | nodes: 8 | - role: control-plane 9 | image: kindest/node:v1.27.11 10 | -------------------------------------------------------------------------------- /dev/prometheus.yml: -------------------------------------------------------------------------------- 1 | global: 2 | scrape_interval: 15s 3 | evaluation_interval: 15s 4 | 5 | scrape_configs: 6 | - job_name: prometheus 7 | static_configs: 8 | - targets: ["host.docker.internal:8080"] 9 | -------------------------------------------------------------------------------- /dev/scripts/.isort.cfg: -------------------------------------------------------------------------------- 1 | [settings] 2 | profile=black 3 | -------------------------------------------------------------------------------- /dev/scripts/generate-fake-data/requirements.txt: -------------------------------------------------------------------------------- 1 | aiohttp==3.10.11 2 | Faker==16.4.0 3 | tqdm==4.66.3 4 | -------------------------------------------------------------------------------- /docs/.markdownlint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../.markdownlint.json", 3 | "MD013": { 4 | "line_length": "160" 5 | }, 6 | "MD025": false, 7 | "MD033": false, 8 | "MD036": false, 9 | "MD052": false, 10 | "MD053": false 11 | } 12 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | .ONESHELL: 2 | .DELETE_ON_ERROR: 3 | export SHELL := bash 4 | export SHELLOPTS := pipefail:errexit 5 | MAKEFLAGS += --warn-undefined-variables 6 | MAKEFLAGS += --no-builtin-rule 7 | 8 | include docs.mk 9 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Grafana Cloud Documentation 2 | 3 | Source for documentation at 4 | 5 | ## Preview the website 6 | 7 | Run `make docs`. This launches a preview of the website with the current grafana docs at 8 | `http://localhost:3002/docs/oncall/latest/` which will refresh automatically when changes are made to 9 | content in the `sources` directory. 10 | Make sure Docker is running. 11 | 12 | ## Image support 13 | 14 | See [the documentation in writers toolkit about how to store images](https://grafana.com/docs/writers-toolkit/writing-guide/image-guidelines/#where-to-store-media-assets) 15 | -------------------------------------------------------------------------------- /docs/img/GH_discussions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/docs/img/GH_discussions.png -------------------------------------------------------------------------------- /docs/img/architecture_diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/docs/img/architecture_diagram.png -------------------------------------------------------------------------------- /docs/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/docs/img/logo.png -------------------------------------------------------------------------------- /docs/img/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/docs/img/screenshot.png -------------------------------------------------------------------------------- /docs/img/screenshot_mobile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/docs/img/screenshot_mobile.png -------------------------------------------------------------------------------- /docs/img/slack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/docs/img/slack.png -------------------------------------------------------------------------------- /docs/variables.mk: -------------------------------------------------------------------------------- 1 | # List of projects to provide to the make-docs script. 2 | PROJECTS := oncall 3 | -------------------------------------------------------------------------------- /engine/.dockerignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | .pytest_cache 3 | *.pyc 4 | celerybeat-schedule 5 | *.db 6 | ./extensions 7 | 8 | .DS_Store 9 | -------------------------------------------------------------------------------- /engine/.gitignore: -------------------------------------------------------------------------------- 1 | requirements-enterprise*.txt 2 | extensions/ 3 | uwsgi-local.ini 4 | celerybeat-schedule 5 | *.db 6 | gcp_service_account.json 7 | -------------------------------------------------------------------------------- /engine/apps/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/engine/apps/__init__.py -------------------------------------------------------------------------------- /engine/apps/alerts/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/engine/apps/alerts/__init__.py -------------------------------------------------------------------------------- /engine/apps/alerts/escalation_snapshot/__init__.py: -------------------------------------------------------------------------------- 1 | from .escalation_snapshot_mixin import EscalationSnapshotMixin # noqa: F401 2 | -------------------------------------------------------------------------------- /engine/apps/alerts/escalation_snapshot/serializers/__init__.py: -------------------------------------------------------------------------------- 1 | from .channel_filter_snapshot import ChannelFilterSnapshotSerializer # noqa: F401 2 | from .escalation_chain_snapshot import EscalationChainSnapshotSerializer # noqa: F401 3 | from .escalation_policy_snapshot import EscalationPolicySnapshotSerializer # noqa: F401 4 | from .escalation_snapshot import EscalationSnapshotSerializer # noqa: F401 5 | -------------------------------------------------------------------------------- /engine/apps/alerts/escalation_snapshot/serializers/escalation_chain_snapshot.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | 3 | from apps.alerts.models.escalation_chain import EscalationChain 4 | 5 | 6 | class EscalationChainSnapshotSerializer(serializers.ModelSerializer): 7 | id = serializers.IntegerField() 8 | 9 | class Meta: 10 | model = EscalationChain 11 | fields = [ 12 | "id", 13 | "name", 14 | ] 15 | -------------------------------------------------------------------------------- /engine/apps/alerts/escalation_snapshot/snapshot_classes/__init__.py: -------------------------------------------------------------------------------- 1 | from .channel_filter_snapshot import ChannelFilterSnapshot # noqa: F401 2 | from .escalation_chain_snapshot import EscalationChainSnapshot # noqa: F401 3 | from .escalation_policy_snapshot import EscalationPolicySnapshot # noqa: F401 4 | from .escalation_snapshot import EscalationSnapshot # noqa: F401 5 | -------------------------------------------------------------------------------- /engine/apps/alerts/escalation_snapshot/snapshot_classes/escalation_chain_snapshot.py: -------------------------------------------------------------------------------- 1 | from apps.alerts.escalation_snapshot.serializers import EscalationChainSnapshotSerializer 2 | 3 | 4 | class EscalationChainSnapshot: 5 | __slots__ = ("id", "name") 6 | 7 | serializer = EscalationChainSnapshotSerializer 8 | 9 | def __init__(self, id, name): 10 | self.id = id 11 | self.name = name 12 | -------------------------------------------------------------------------------- /engine/apps/alerts/grafana_alerting_sync_manager/__init__.py: -------------------------------------------------------------------------------- 1 | from .grafana_alerting_sync import GrafanaAlertingSyncManager # noqa: F401 2 | -------------------------------------------------------------------------------- /engine/apps/alerts/incident_appearance/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/engine/apps/alerts/incident_appearance/__init__.py -------------------------------------------------------------------------------- /engine/apps/alerts/incident_appearance/renderers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/engine/apps/alerts/incident_appearance/renderers/__init__.py -------------------------------------------------------------------------------- /engine/apps/alerts/incident_appearance/renderers/constants.py: -------------------------------------------------------------------------------- 1 | DEFAULT_BACKUP_TITLE = "Incident" 2 | -------------------------------------------------------------------------------- /engine/apps/alerts/incident_appearance/templaters/__init__.py: -------------------------------------------------------------------------------- 1 | from .alert_templater import TemplateLoader # noqa: F401 2 | from .classic_markdown_templater import AlertClassicMarkdownTemplater # noqa: F401 3 | from .phone_call_templater import AlertPhoneCallTemplater # noqa: F401 4 | from .slack_templater import AlertSlackTemplater # noqa: F401 5 | from .sms_templater import AlertSmsTemplater # noqa: F401 6 | from .telegram_templater import AlertTelegramTemplater # noqa: F401 7 | from .web_templater import AlertWebTemplater # noqa: F401 8 | -------------------------------------------------------------------------------- /engine/apps/alerts/incident_log_builder/__init__.py: -------------------------------------------------------------------------------- 1 | from .incident_log_builder import IncidentLogBuilder # noqa: F401 2 | -------------------------------------------------------------------------------- /engine/apps/alerts/migrations/0009_alertreceivechannel_web_templates_modified_at.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.16 on 2023-01-27 07:41 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('alerts', '0008_alter_alertgrouplogrecord_type'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='alertreceivechannel', 15 | name='web_templates_modified_at', 16 | field=models.DateTimeField(blank=True, null=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /engine/apps/alerts/migrations/0012_alertreceivechannel_description_short.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.18 on 2023-04-17 01:46 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('alerts', '0011_auto_20230329_1617'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='alertreceivechannel', 15 | name='description_short', 16 | field=models.CharField(default=None, max_length=250, null=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /engine/apps/alerts/migrations/0013_merge_20230418_0336.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.18 on 2023-04-18 03:36 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('alerts', '0012_alertreceivechannel_description_short'), 10 | ('alerts', '0012_auto_20230406_1010'), 11 | ] 12 | 13 | operations = [ 14 | ] 15 | -------------------------------------------------------------------------------- /engine/apps/alerts/migrations/0014_alertreceivechannel_restricted_at.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.18 on 2023-04-18 05:03 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('alerts', '0013_merge_20230418_0336'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='alertreceivechannel', 15 | name='restricted_at', 16 | field=models.DateTimeField(default=None, null=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /engine/apps/alerts/migrations/0017_alertgroup_is_restricted.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.19 on 2023-06-19 12:40 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('alerts', '0016_auto_20230523_1355'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='alertgroup', 15 | name='is_restricted', 16 | field=models.BooleanField(default=False, null=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /engine/apps/alerts/migrations/0018_remove_alertreceivechannel_integration_slack_channel_id.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.19 on 2023-06-28 16:00 2 | 3 | from django.db import migrations 4 | import django_migration_linter as linter 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('alerts', '0017_alertgroup_is_restricted'), 11 | ] 12 | 13 | operations = [ 14 | linter.IgnoreMigration(), # This field is deprecated a long time ago. 15 | migrations.RemoveField( 16 | model_name='alertreceivechannel', 17 | name='integration_slack_channel_id', 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /engine/apps/alerts/migrations/0021_alter_alertgroup_started_at.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.20 on 2023-07-13 06:48 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('alerts', '0020_auto_20230711_1532'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='alertgroup', 15 | name='started_at', 16 | field=models.DateTimeField(auto_now_add=True, db_index=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /engine/apps/alerts/migrations/0027_remove_alertreceivechannel_restricted_at_from_state.py: -------------------------------------------------------------------------------- 1 | from django.db import migrations 2 | 3 | 4 | class Migration(migrations.Migration): 5 | 6 | dependencies = [ 7 | ("alerts", "0026_auto_20230719_1010"), 8 | ] 9 | 10 | operations = [ 11 | migrations.SeparateDatabaseAndState( 12 | state_operations=[ 13 | migrations.RemoveField( 14 | model_name="alertreceivechannel", 15 | name="restricted_at", 16 | ), 17 | ], 18 | database_operations=[], 19 | ) 20 | ] 21 | -------------------------------------------------------------------------------- /engine/apps/alerts/migrations/0032_remove_alertgroup_slack_message_state.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.20 on 2023-09-04 12:46 2 | 3 | import common.migrations.remove_field 4 | from django.db import migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('alerts', '0031_auto_20230831_1445'), 11 | ] 12 | 13 | operations = [ 14 | common.migrations.remove_field.RemoveFieldState( 15 | model_name='AlertGroup', 16 | name='slack_message', 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /engine/apps/alerts/migrations/0033_alertgrouplogrecord_action_source.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.20 on 2023-10-04 10:11 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('alerts', '0032_remove_alertgroup_slack_message_state'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='alertgrouplogrecord', 15 | name='action_source', 16 | field=models.SmallIntegerField(default=None, null=True, verbose_name=[(0, 'Slack'), (1, 'Web'), (2, 'Phone'), (3, 'Telegram'), (4, 'API')]), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /engine/apps/alerts/migrations/0034_alter_resolutionnote_source.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.20 on 2023-10-20 13:21 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('alerts', '0033_alertgrouplogrecord_action_source'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='resolutionnote', 15 | name='source', 16 | field=models.IntegerField(choices=[(0, 'Slack'), (1, 'Web'), (2, 'Mobile App')], default=None, null=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /engine/apps/alerts/migrations/0036_alertgroup_grafana_incident_id.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.6 on 2023-10-31 20:05 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('alerts', '0035_alter_alertreceivechannel_maintenance_author'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='alertgroup', 15 | name='grafana_incident_id', 16 | field=models.CharField(default=None, max_length=100, null=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /engine/apps/alerts/migrations/0037_remove_alertgroup_is_restricted_state.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.6 on 2023-11-03 23:02 2 | 3 | import common.migrations.remove_field 4 | from django.db import migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('alerts', '0036_alertgroup_grafana_incident_id'), 11 | ] 12 | 13 | operations = [ 14 | common.migrations.remove_field.RemoveFieldState( 15 | model_name='AlertGroup', 16 | name='is_restricted', 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /engine/apps/alerts/migrations/0039_remove_alertreceivechannel_unique_integration_name.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.7 on 2023-11-21 12:25 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('alerts', '0038_remove_alertgroup_is_restricted_db'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RemoveConstraint( 14 | model_name='alertreceivechannel', 15 | name='unique integration name', 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /engine/apps/alerts/migrations/0042_alertgroup_received_at.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.7 on 2023-12-05 18:06 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('alerts', '0041_alertreceivechannel_unique_direct_paging_integration_per_team'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='alertgroup', 15 | name='received_at', 16 | field=models.DateTimeField(blank=True, default=None, null=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /engine/apps/alerts/migrations/0047_alertreceivechannel_additional_settings.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.10 on 2024-03-07 18:10 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('alerts', '0046_alertreceivechannelconnection'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='alertreceivechannel', 15 | name='additional_settings', 16 | field=models.JSONField(default=None, null=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /engine/apps/alerts/migrations/0051_remove_escalationpolicy_custom_button_trigger.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.11 on 2024-04-02 13:54 2 | 3 | from django.db import migrations 4 | import django_migration_linter as linter 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('alerts', '0050_alter_alertgrouplogrecord_type'), 11 | ] 12 | 13 | operations = [ 14 | linter.IgnoreMigration(), 15 | migrations.RemoveField( 16 | model_name='escalationpolicy', 17 | name='custom_button_trigger', 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /engine/apps/alerts/migrations/0052_alter_channelfilter_filtering_term_type.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.11 on 2024-07-04 20:20 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('alerts', '0051_remove_escalationpolicy_custom_button_trigger'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='channelfilter', 15 | name='filtering_term_type', 16 | field=models.IntegerField(choices=[(0, 'regex'), (1, 'jinja2'), (2, 'labels')], default=0), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /engine/apps/alerts/migrations/0053_channelfilter_filtering_labels.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.11 on 2024-07-08 18:20 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('alerts', '0052_alter_channelfilter_filtering_term_type'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='channelfilter', 15 | name='filtering_labels', 16 | field=models.JSONField(default=None, null=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /engine/apps/alerts/migrations/0056_remove_alertgroup_slack_log_message_state.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.15 on 2024-08-19 21:54 2 | 3 | import common.migrations.remove_field 4 | from django.db import migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('alerts', '0055_alter_bundlednotification_alert_group'), 11 | ] 12 | 13 | operations = [ 14 | common.migrations.remove_field.RemoveFieldState( 15 | model_name='AlertGroup', 16 | name='slack_log_message', 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /engine/apps/alerts/migrations/0066_remove_channelfilter__slack_channel_id_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.16 on 2024-11-06 21:11 2 | 3 | from django.db import migrations 4 | import django_migration_linter as linter 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('alerts', '0065_alertreceivechannel_service_account'), 11 | ] 12 | 13 | operations = [ 14 | linter.IgnoreMigration(), 15 | migrations.DeleteModel( 16 | name='AlertGroupPostmortem', 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /engine/apps/alerts/migrations/0067_remove_channelfilter__slack_channel_id_state.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.16 on 2024-11-20 20:21 2 | 3 | import common.migrations.remove_field 4 | from django.db import migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('alerts', '0066_remove_channelfilter__slack_channel_id_and_more'), 11 | ] 12 | 13 | operations = [ 14 | common.migrations.remove_field.RemoveFieldState( 15 | model_name='channelfilter', 16 | name='_slack_channel_id', 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /engine/apps/alerts/migrations/0068_remove_resolutionnoteslackmessage__slack_channel_id_state.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.16 on 2024-11-20 20:23 2 | 3 | import common.migrations.remove_field 4 | from django.db import migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('alerts', '0067_remove_channelfilter__slack_channel_id_state'), 11 | ] 12 | 13 | operations = [ 14 | common.migrations.remove_field.RemoveFieldState( 15 | model_name='resolutionnoteslackmessage', 16 | name='_slack_channel_id', 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /engine/apps/alerts/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/engine/apps/alerts/migrations/__init__.py -------------------------------------------------------------------------------- /engine/apps/alerts/representative.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from abc import ABC, abstractmethod 3 | 4 | logger = logging.getLogger(__name__) 5 | 6 | 7 | class AlertGroupAbstractRepresentative(ABC): 8 | HANDLER_PREFIX = "on_" 9 | 10 | @abstractmethod 11 | def is_applicable(self): 12 | return None 13 | 14 | @staticmethod 15 | def get_handlers_map(): 16 | from apps.alerts.models import AlertGroupLogRecord 17 | 18 | return AlertGroupLogRecord.ACTIONS_TO_HANDLERS_MAP 19 | 20 | @classmethod 21 | def on_create_alert(cls, **kwargs): 22 | raise NotImplementedError 23 | -------------------------------------------------------------------------------- /engine/apps/alerts/tasks/call_ack_url.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/engine/apps/alerts/tasks/call_ack_url.py -------------------------------------------------------------------------------- /engine/apps/alerts/tasks/custom_webhook_result.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from django.conf import settings 4 | 5 | from common.custom_celery_tasks import shared_dedicated_queue_retry_task 6 | 7 | logger = logging.getLogger(__name__) 8 | 9 | 10 | @shared_dedicated_queue_retry_task( 11 | autoretry_for=(Exception,), retry_backoff=True, max_retries=1 if settings.DEBUG else None 12 | ) 13 | def custom_webhook_result(webhook_pk, alert_group_pk, escalation_policy_pk=None): 14 | from apps.webhooks.tasks import execute_webhook 15 | 16 | execute_webhook.apply_async((webhook_pk, alert_group_pk, None, escalation_policy_pk)) 17 | -------------------------------------------------------------------------------- /engine/apps/alerts/tasks/resolve_by_last_step.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | 3 | from common.custom_celery_tasks import shared_dedicated_queue_retry_task 4 | 5 | 6 | @shared_dedicated_queue_retry_task( 7 | autoretry_for=(Exception,), retry_backoff=True, max_retries=0 if settings.DEBUG else None 8 | ) 9 | def resolve_by_last_step_task(alert_group_pk): 10 | from apps.alerts.models import AlertGroup 11 | 12 | alert_group = AlertGroup.objects.get(pk=alert_group_pk) 13 | alert_group.resolve_by_last_step() 14 | -------------------------------------------------------------------------------- /engine/apps/alerts/tasks/sync_grafana_alerting_contact_points.py: -------------------------------------------------------------------------------- 1 | from common.custom_celery_tasks import shared_dedicated_queue_retry_task 2 | 3 | 4 | @shared_dedicated_queue_retry_task(autoretry_for=(Exception,), retry_backoff=True, max_retries=10) 5 | def disconnect_integration_from_alerting_contact_points(alert_receive_channel_id): 6 | from apps.alerts.models import AlertReceiveChannel 7 | 8 | alert_receive_channel = AlertReceiveChannel.objects_with_deleted.get(pk=alert_receive_channel_id) 9 | alert_receive_channel.grafana_alerting_sync_manager.disconnect_all_contact_points() 10 | -------------------------------------------------------------------------------- /engine/apps/alerts/tasks/task_logger.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from celery.utils.log import get_task_logger 4 | 5 | task_logger = get_task_logger(__name__) 6 | task_logger.setLevel(logging.DEBUG) 7 | -------------------------------------------------------------------------------- /engine/apps/alerts/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/engine/apps/alerts/tests/__init__.py -------------------------------------------------------------------------------- /engine/apps/alerts/tests/test_custom_webhook_result.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import call, patch 2 | 3 | import pytest 4 | 5 | from apps.alerts.tasks import custom_webhook_result 6 | 7 | 8 | @pytest.mark.django_db 9 | def test_custom_webhook_result_executes_webhook(): 10 | webhook_id = 42 11 | alert_group_id = 13 12 | escalation_policy_id = 11 13 | 14 | with patch("apps.webhooks.tasks.trigger_webhook.execute_webhook.apply_async") as mock_execute: 15 | custom_webhook_result(webhook_id, alert_group_id, escalation_policy_id) 16 | 17 | assert mock_execute.call_args == call((webhook_id, alert_group_id, None, escalation_policy_id)) 18 | -------------------------------------------------------------------------------- /engine/apps/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/engine/apps/api/__init__.py -------------------------------------------------------------------------------- /engine/apps/api/errors.py: -------------------------------------------------------------------------------- 1 | """errors contains business-logic error codes for internal api. 2 | 3 | It's expected that error codes will use 1000-9999 codes range, where first two digits are for entity: 4 | 11xx - AlertGroup, 12xx - AlertReceiveChannel, etc. 5 | 10xx are saved for non-entity related errors. 6 | """ 7 | # TODO: this package is WIP. It requires validation of code ranges. 8 | from enum import Enum, unique 9 | 10 | 11 | @unique 12 | class AlertGroupAPIError(Enum): 13 | """ 14 | Error codes for alert group. 15 | Range is 1100-1199 16 | """ 17 | 18 | RESOLUTION_NOTE_REQUIRED = 1101 19 | -------------------------------------------------------------------------------- /engine/apps/api/label_filtering.py: -------------------------------------------------------------------------------- 1 | from typing import List, Tuple 2 | 3 | 4 | def parse_label_query(label_query: List[str]) -> List[Tuple[str, str]]: 5 | """ 6 | parse_label_query returns list of key-value tuples from a list of "raw" labels – key-value pairs separated with ':'. 7 | """ 8 | kv_pairs = [] 9 | for label in label_query: 10 | label_data = label.split(":") 11 | # Check if label_data is a valid key-value label pair]: ["key1", "value1"] 12 | if len(label_data) != 2: 13 | continue 14 | kv_pairs.append((label_data[0], label_data[1])) 15 | return kv_pairs 16 | -------------------------------------------------------------------------------- /engine/apps/api/serializers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/engine/apps/api/serializers/__init__.py -------------------------------------------------------------------------------- /engine/apps/api/serializers/custom_serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | 3 | 4 | class DynamicFieldsModelSerializer(serializers.ModelSerializer): 5 | def __init__(self, *args, **kwargs): 6 | fields = kwargs.pop("fields", None) 7 | 8 | super(DynamicFieldsModelSerializer, self).__init__(*args, **kwargs) 9 | 10 | if fields is not None: 11 | allowed = set(fields) 12 | existing = set(self.fields) 13 | for field_name in existing - allowed: 14 | self.fields.pop(field_name) 15 | -------------------------------------------------------------------------------- /engine/apps/api/serializers/public_api_token.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | 3 | from apps.auth_token.models import ApiAuthToken 4 | 5 | 6 | class PublicApiTokenSerializer(serializers.ModelSerializer): 7 | class Meta: 8 | model = ApiAuthToken 9 | fields = [ 10 | "id", 11 | "name", 12 | "created_at", 13 | ] 14 | -------------------------------------------------------------------------------- /engine/apps/api/serializers/schedule_reminder.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | 3 | from apps.schedules.models import OnCallSchedule 4 | 5 | 6 | class ScheduleReminderSerializer(serializers.ModelSerializer): 7 | class Meta: 8 | model = OnCallSchedule 9 | fields = [ 10 | "id", 11 | "notify_oncall_shift_freq", 12 | "mention_oncall_start", 13 | "mention_oncall_next", 14 | "notify_empty_oncall", 15 | ] 16 | -------------------------------------------------------------------------------- /engine/apps/api/serializers/user_group.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | 3 | from apps.slack.models import SlackUserGroup 4 | 5 | 6 | class UserGroupSerializer(serializers.ModelSerializer): 7 | id = serializers.CharField(read_only=True, source="public_primary_key") 8 | 9 | class Meta: 10 | model = SlackUserGroup 11 | fields = ("id", "name", "handle") 12 | -------------------------------------------------------------------------------- /engine/apps/api/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/engine/apps/api/tests/__init__.py -------------------------------------------------------------------------------- /engine/apps/api/throttlers/__init__.py: -------------------------------------------------------------------------------- 1 | from .demo_alert_throttler import DemoAlertThrottler # noqa: F401 2 | from .phone_verification_throttler import ( # noqa: F401 3 | GetPhoneVerificationCodeThrottlerPerOrg, 4 | GetPhoneVerificationCodeThrottlerPerUser, 5 | VerifyPhoneNumberThrottlerPerOrg, 6 | VerifyPhoneNumberThrottlerPerUser, 7 | ) 8 | from .test_call_throttler import TestCallThrottler # noqa: F401 9 | -------------------------------------------------------------------------------- /engine/apps/api/throttlers/demo_alert_throttler.py: -------------------------------------------------------------------------------- 1 | from common.api_helpers.custom_rate_scoped_throttler import CustomRateUserThrottler 2 | 3 | 4 | class DemoAlertThrottler(CustomRateUserThrottler): 5 | scope = "send_demo_alert" 6 | rate = "30/m" 7 | -------------------------------------------------------------------------------- /engine/apps/api/views/__init__.py: -------------------------------------------------------------------------------- 1 | from .user_notification_policy import UserNotificationPolicyView # noqa: F401 2 | -------------------------------------------------------------------------------- /engine/apps/api_for_grafana_incident/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/engine/apps/api_for_grafana_incident/__init__.py -------------------------------------------------------------------------------- /engine/apps/api_for_grafana_incident/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ApiForGrafanaIncidentConfig(AppConfig): 5 | default_auto_field = "django.db.models.BigAutoField" 6 | name = "apps.api_for_grafana_incident" 7 | -------------------------------------------------------------------------------- /engine/apps/api_for_grafana_incident/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/engine/apps/api_for_grafana_incident/tests/__init__.py -------------------------------------------------------------------------------- /engine/apps/api_for_grafana_incident/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import include, path 2 | 3 | from common.api_helpers.optional_slash_router import OptionalSlashRouter 4 | 5 | from . import views 6 | 7 | app_name = "api_for_grafana_incident" 8 | 9 | 10 | router = OptionalSlashRouter() 11 | 12 | router.register(r"alert-groups", views.AlertGroupsView, basename="alert-groups") 13 | 14 | 15 | urlpatterns = [ 16 | path("", include(router.urls)), 17 | ] 18 | -------------------------------------------------------------------------------- /engine/apps/auth_token/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/engine/apps/auth_token/__init__.py -------------------------------------------------------------------------------- /engine/apps/auth_token/exceptions.py: -------------------------------------------------------------------------------- 1 | class InvalidToken(Exception): 2 | pass 3 | 4 | 5 | class ServiceAccountDoesNotExist(Exception): 6 | pass 7 | -------------------------------------------------------------------------------- /engine/apps/auth_token/grafana/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/engine/apps/auth_token/grafana/__init__.py -------------------------------------------------------------------------------- /engine/apps/auth_token/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/engine/apps/auth_token/migrations/__init__.py -------------------------------------------------------------------------------- /engine/apps/auth_token/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/engine/apps/auth_token/tests/__init__.py -------------------------------------------------------------------------------- /engine/apps/base/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/engine/apps/base/__init__.py -------------------------------------------------------------------------------- /engine/apps/base/migrations/0003_delete_organizationlogrecord.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.5 on 2022-08-23 12:03 2 | 3 | from django.db import migrations 4 | import django_migration_linter as linter 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('base', '0002_squashed_initial'), 11 | ] 12 | 13 | operations = [ 14 | linter.IgnoreMigration(), 15 | migrations.DeleteModel( 16 | name='OrganizationLogRecord', 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /engine/apps/base/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/engine/apps/base/migrations/__init__.py -------------------------------------------------------------------------------- /engine/apps/base/models/__init__.py: -------------------------------------------------------------------------------- 1 | from .dynamic_setting import DynamicSetting # noqa: F401 2 | from .failed_to_invoke_celery_task import FailedToInvokeCeleryTask # noqa: F401 3 | from .live_setting import LiveSetting # noqa: F401 4 | from .user_notification_policy import UserNotificationPolicy # noqa: F401 5 | from .user_notification_policy_log_record import UserNotificationPolicyLogRecord # noqa: F401 6 | -------------------------------------------------------------------------------- /engine/apps/base/models/dynamic_setting.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.db.models import JSONField 3 | 4 | 5 | class DynamicSetting(models.Model): 6 | name = models.CharField(max_length=100) 7 | boolean_value = models.BooleanField(null=True, default=None) 8 | numeric_value = models.IntegerField(null=True, default=None) 9 | json_value = JSONField(default=None, null=True, blank=True) 10 | 11 | class Meta: 12 | constraints = [models.UniqueConstraint(fields=["name"], name="unique_dynamic_setting_name")] 13 | 14 | def __str__(self): 15 | return self.name 16 | -------------------------------------------------------------------------------- /engine/apps/base/models/failed_to_invoke_celery_task.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | from engine.celery import app 4 | 5 | 6 | class FailedToInvokeCeleryTask(models.Model): 7 | name = models.CharField(max_length=500) 8 | parameters = models.JSONField() 9 | 10 | is_sent = models.BooleanField(default=False) 11 | 12 | def send(self): 13 | app.send_task( 14 | name=self.name, 15 | args=self.parameters.get("args", []), 16 | kwargs=self.parameters.get("kwargs", {}), 17 | **self.parameters.get("options", {}), 18 | ) 19 | -------------------------------------------------------------------------------- /engine/apps/base/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/engine/apps/base/tests/__init__.py -------------------------------------------------------------------------------- /engine/apps/base/tests/factories.py: -------------------------------------------------------------------------------- 1 | import factory 2 | 3 | from apps.base.models import LiveSetting, UserNotificationPolicy, UserNotificationPolicyLogRecord 4 | 5 | 6 | class UserNotificationPolicyFactory(factory.DjangoModelFactory): 7 | class Meta: 8 | model = UserNotificationPolicy 9 | 10 | 11 | class UserNotificationPolicyLogRecordFactory(factory.DjangoModelFactory): 12 | class Meta: 13 | model = UserNotificationPolicyLogRecord 14 | 15 | 16 | class LiveSettingFactory(factory.DjangoModelFactory): 17 | class Meta: 18 | model = LiveSetting 19 | -------------------------------------------------------------------------------- /engine/apps/base/tests/test_messaging.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from apps.base.messaging import get_messaging_backend_from_id, get_messaging_backends 4 | 5 | 6 | @pytest.mark.django_db 7 | def test_messaging_backends_enabled(settings): 8 | assert get_messaging_backends() != {} 9 | assert get_messaging_backend_from_id("TESTONLY") is not None 10 | -------------------------------------------------------------------------------- /engine/apps/chatops_proxy/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/engine/apps/chatops_proxy/__init__.py -------------------------------------------------------------------------------- /engine/apps/chatops_proxy/events/__init__.py: -------------------------------------------------------------------------------- 1 | from .root_handler import ChatopsEventsHandler # noqa 2 | -------------------------------------------------------------------------------- /engine/apps/chatops_proxy/events/types.py: -------------------------------------------------------------------------------- 1 | import typing 2 | 3 | INTEGRATION_INSTALLED_EVENT_TYPE = "integration_installed" 4 | INTEGRATION_UNINSTALLED_EVENT_TYPE = "integration_uninstalled" 5 | 6 | 7 | class Event(typing.TypedDict): 8 | event_type: str 9 | data: dict 10 | 11 | 12 | class IntegrationInstalledData(typing.TypedDict): 13 | oauth_installation_id: int 14 | provider_type: str 15 | stack_id: int 16 | grafana_user_id: int 17 | payload: dict 18 | 19 | 20 | class IntegrationUninstalledData(typing.TypedDict): 21 | provider_type: str 22 | stack_id: int 23 | grafana_user_id: int 24 | -------------------------------------------------------------------------------- /engine/apps/chatops_proxy/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/engine/apps/chatops_proxy/tests/__init__.py -------------------------------------------------------------------------------- /engine/apps/chatops_proxy/urls.py: -------------------------------------------------------------------------------- 1 | from common.api_helpers.optional_slash_router import optional_slash_path 2 | 3 | from .views import ChatopsEventsView 4 | 5 | app_name = "chatops-proxy" 6 | 7 | urlpatterns = [ 8 | optional_slash_path("events", ChatopsEventsView.as_view(), name="events"), 9 | ] 10 | -------------------------------------------------------------------------------- /engine/apps/email/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/engine/apps/email/__init__.py -------------------------------------------------------------------------------- /engine/apps/email/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/engine/apps/email/migrations/__init__.py -------------------------------------------------------------------------------- /engine/apps/email/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/engine/apps/email/tests/__init__.py -------------------------------------------------------------------------------- /engine/apps/email/tests/factories.py: -------------------------------------------------------------------------------- 1 | import factory 2 | 3 | from apps.email.models import EmailMessage 4 | 5 | 6 | class EmailMessageFactory(factory.DjangoModelFactory): 7 | class Meta: 8 | model = EmailMessage 9 | -------------------------------------------------------------------------------- /engine/apps/exotel/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/engine/apps/exotel/__init__.py -------------------------------------------------------------------------------- /engine/apps/exotel/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/engine/apps/exotel/migrations/__init__.py -------------------------------------------------------------------------------- /engine/apps/exotel/models/__init__.py: -------------------------------------------------------------------------------- 1 | from .phone_call import ExotelCallStatuses, ExotelPhoneCall # noqa: F401 2 | -------------------------------------------------------------------------------- /engine/apps/exotel/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/engine/apps/exotel/tests/__init__.py -------------------------------------------------------------------------------- /engine/apps/exotel/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from .views import CallStatusCallback 4 | 5 | app_name = "exotel" 6 | 7 | urlpatterns = [ 8 | path("call_status_events/", CallStatusCallback.as_view(), name="call_status_events"), 9 | ] 10 | -------------------------------------------------------------------------------- /engine/apps/google/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/engine/apps/google/__init__.py -------------------------------------------------------------------------------- /engine/apps/google/constants.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | 3 | EVENT_SUMMARY_IGNORE_KEYWORD = "#grafana-oncall-ignore" 4 | 5 | GOOGLE_CALENDAR_EVENT_DATETIME_FORMAT = "%Y-%m-%dT%H:%M:%S%z" 6 | """ 7 | https://stackoverflow.com/a/17159470/3902555 8 | """ 9 | 10 | DAYS_IN_FUTURE_TO_CONSIDER_OUT_OF_OFFICE_EVENTS = 30 11 | REQUIRED_OAUTH_SCOPES = settings.SOCIAL_AUTH_GOOGLE_OAUTH2_SCOPE 12 | -------------------------------------------------------------------------------- /engine/apps/google/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/engine/apps/google/migrations/__init__.py -------------------------------------------------------------------------------- /engine/apps/google/models/__init__.py: -------------------------------------------------------------------------------- 1 | from .google_oauth2_user import GoogleOAuth2User # noqa: F401 2 | -------------------------------------------------------------------------------- /engine/apps/google/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/engine/apps/google/tests/__init__.py -------------------------------------------------------------------------------- /engine/apps/google/tests/factories.py: -------------------------------------------------------------------------------- 1 | import factory 2 | 3 | from apps.google.models import GoogleOAuth2User 4 | from common.utils import UniqueFaker 5 | 6 | 7 | class GoogleOAuth2UserFactory(factory.DjangoModelFactory): 8 | google_user_id = UniqueFaker("pyint") 9 | access_token = factory.Faker("password") 10 | refresh_token = factory.Faker("password") 11 | oauth_scope = factory.Faker("word") 12 | 13 | class Meta: 14 | model = GoogleOAuth2User 15 | -------------------------------------------------------------------------------- /engine/apps/google/tests/test_utils.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from apps.google import utils 4 | 5 | SCOPES_ALWAYS_GRANTED = ( 6 | "openid https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email" 7 | ) 8 | 9 | 10 | @pytest.mark.parametrize( 11 | "granted_scopes,expected", 12 | ( 13 | (SCOPES_ALWAYS_GRANTED, False), 14 | (f"{SCOPES_ALWAYS_GRANTED} https://www.googleapis.com/auth/calendar.events.readonly", True), 15 | ), 16 | ) 17 | def test_user_granted_all_required_scopes(granted_scopes, expected): 18 | assert utils.user_granted_all_required_scopes(granted_scopes) == expected 19 | -------------------------------------------------------------------------------- /engine/apps/grafana_plugin/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/engine/apps/grafana_plugin/__init__.py -------------------------------------------------------------------------------- /engine/apps/grafana_plugin/helpers/__init__.py: -------------------------------------------------------------------------------- 1 | from .client import GcomAPIClient # noqa: F401 2 | from .client import GrafanaAPIClient # noqa: F401 3 | -------------------------------------------------------------------------------- /engine/apps/grafana_plugin/tasks/__init__.py: -------------------------------------------------------------------------------- 1 | from .sync import ( # noqa: F401 2 | start_sync_organizations, 3 | sync_organization_async, 4 | sync_team_members_for_organization_async, 5 | ) 6 | from .sync_v2 import sync_organizations_v2 # noqa: F401 7 | -------------------------------------------------------------------------------- /engine/apps/grafana_plugin/views/__init__.py: -------------------------------------------------------------------------------- 1 | from .install import InstallView # noqa: F401 2 | from .install_v2 import InstallV2View # noqa: F401 3 | from .recaptcha import RecaptchaView # noqa: F401 4 | from .self_hosted_install import SelfHostedInstallView # noqa: F401 5 | from .status import StatusView # noqa: F401 6 | from .status_v2 import StatusV2View # noqa: F401 7 | from .sync_organization import SyncOrganizationView # noqa: F401 8 | from .sync_v2 import SyncV2View # noqa: F401 9 | -------------------------------------------------------------------------------- /engine/apps/grafana_plugin/views/recaptcha.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from rest_framework.request import Request 3 | from rest_framework.response import Response 4 | from rest_framework.views import APIView 5 | 6 | from apps.auth_token.auth import PluginAuthentication 7 | 8 | 9 | class RecaptchaView(APIView): 10 | authentication_classes = (PluginAuthentication,) 11 | 12 | def get(self, request: Request) -> Response: 13 | return Response( 14 | data={ 15 | "recaptcha_site_key": settings.RECAPTCHA_V3_SITE_KEY, 16 | } 17 | ) 18 | -------------------------------------------------------------------------------- /engine/apps/heartbeat/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/engine/apps/heartbeat/__init__.py -------------------------------------------------------------------------------- /engine/apps/heartbeat/migrations/0002_delete_heartbeat.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.20 on 2023-07-14 11:36 2 | 3 | from django.db import migrations 4 | import django_migration_linter as linter 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('heartbeat', '0001_squashed_initial'), 11 | ] 12 | 13 | operations = [ 14 | linter.IgnoreMigration(), 15 | migrations.DeleteModel( 16 | name='HeartBeat', 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /engine/apps/heartbeat/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/engine/apps/heartbeat/migrations/__init__.py -------------------------------------------------------------------------------- /engine/apps/heartbeat/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/engine/apps/heartbeat/tests/__init__.py -------------------------------------------------------------------------------- /engine/apps/heartbeat/tests/factories.py: -------------------------------------------------------------------------------- 1 | import factory 2 | 3 | from apps.heartbeat.models import IntegrationHeartBeat 4 | 5 | 6 | class IntegrationHeartBeatFactory(factory.DjangoModelFactory): 7 | class Meta: 8 | model = IntegrationHeartBeat 9 | -------------------------------------------------------------------------------- /engine/apps/integrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/engine/apps/integrations/__init__.py -------------------------------------------------------------------------------- /engine/apps/integrations/legacy_prefix.py: -------------------------------------------------------------------------------- 1 | """ 2 | legacy_prefix.py provides utils to work with legacy integration types, which are prefixed with 'legacy_'. 3 | """ 4 | 5 | legacy_prefix = "legacy_" 6 | 7 | 8 | def has_legacy_prefix(integration_type: str) -> bool: 9 | return integration_type.startswith(legacy_prefix) 10 | 11 | 12 | def remove_legacy_prefix(integration_type: str) -> str: 13 | return integration_type.removeprefix(legacy_prefix) 14 | -------------------------------------------------------------------------------- /engine/apps/integrations/metadata/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/engine/apps/integrations/metadata/__init__.py -------------------------------------------------------------------------------- /engine/apps/integrations/middlewares.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from django.core.exceptions import PermissionDenied 4 | from django.http import HttpResponse 5 | from django.utils.deprecation import MiddlewareMixin 6 | from rest_framework import status 7 | 8 | logger = logging.getLogger(__name__) 9 | 10 | 11 | class IntegrationExceptionMiddleware(MiddlewareMixin): 12 | def process_exception(self, request, exception): 13 | if request.path.startswith("/integrations/v1") and isinstance(exception, PermissionDenied): 14 | return HttpResponse(exception, status=status.HTTP_403_FORBIDDEN) 15 | -------------------------------------------------------------------------------- /engine/apps/integrations/mixins/__init__.py: -------------------------------------------------------------------------------- 1 | from .alert_channel_defining_mixin import AlertChannelDefiningMixin # noqa: F401 2 | from .browsable_instruction_mixin import BrowsableInstructionMixin # noqa: F401 3 | from .ratelimit_mixin import ( # noqa: F401 4 | IntegrationHeartBeatRateLimitMixin, 5 | IntegrationRateLimitMixin, 6 | RateLimitMixin, 7 | is_ratelimit_ignored, 8 | ) 9 | -------------------------------------------------------------------------------- /engine/apps/integrations/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/engine/apps/integrations/tests/__init__.py -------------------------------------------------------------------------------- /engine/apps/integrations/throttlers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/engine/apps/integrations/throttlers/__init__.py -------------------------------------------------------------------------------- /engine/apps/integrations/throttlers/integration_backsync_throttler.py: -------------------------------------------------------------------------------- 1 | from rest_framework.throttling import SimpleRateThrottle 2 | 3 | 4 | class BacksyncRateThrottle(SimpleRateThrottle): 5 | """ 6 | Integration backsync rate limit 7 | """ 8 | 9 | scope = "backsync" 10 | rate = "300/m" 11 | 12 | def get_cache_key(self, request, view): 13 | ident = request.auth.alert_receive_channel.pk 14 | return self.cache_format % {"scope": self.scope, "ident": ident} 15 | -------------------------------------------------------------------------------- /engine/apps/labels/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/engine/apps/labels/__init__.py -------------------------------------------------------------------------------- /engine/apps/labels/migrations/0003_alertreceivechannelassociatedlabel_inherit.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.6 on 2023-11-09 10:42 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('labels', '0002_alertgroupassociatedlabel_and_more'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='alertreceivechannelassociatedlabel', 15 | name='inheritable', 16 | field=models.BooleanField(default=True, null=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /engine/apps/labels/migrations/0006_remove_alertreceivechannelassociatedlabel_inheritable_state.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.15 on 2024-11-26 13:37 2 | 3 | import common.migrations.remove_field 4 | from django.db import migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('labels', '0005_labelkeycache_prescribed_labelvaluecache_prescribed'), 11 | ] 12 | 13 | operations = [ 14 | common.migrations.remove_field.RemoveFieldState( 15 | model_name='AlertReceiveChannelAssociatedLabel', 16 | name='inheritable', 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /engine/apps/labels/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/engine/apps/labels/migrations/__init__.py -------------------------------------------------------------------------------- /engine/apps/labels/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/engine/apps/labels/tests/__init__.py -------------------------------------------------------------------------------- /engine/apps/mattermost/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/engine/apps/mattermost/__init__.py -------------------------------------------------------------------------------- /engine/apps/mattermost/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class MattermostConfig(AppConfig): 5 | name = "apps.mattermost" 6 | 7 | def ready(self) -> None: 8 | import apps.mattermost.signals # noqa: F401 9 | -------------------------------------------------------------------------------- /engine/apps/mattermost/events/__init__.py: -------------------------------------------------------------------------------- 1 | from .alert_group_actions_handler import AlertGroupActionHandler # noqa: F401 2 | -------------------------------------------------------------------------------- /engine/apps/mattermost/events/event_handler.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | 3 | from apps.mattermost.events.types import MattermostEvent 4 | from apps.user_management.models import User 5 | 6 | 7 | class MattermostEventHandler(ABC): 8 | def __init__(self, event: MattermostEvent, user: User): 9 | self.event: MattermostEvent = event 10 | self.user: User = user 11 | 12 | @abstractmethod 13 | def is_match(self) -> bool: 14 | pass 15 | 16 | @abstractmethod 17 | def process(self) -> None: 18 | pass 19 | -------------------------------------------------------------------------------- /engine/apps/mattermost/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/engine/apps/mattermost/migrations/__init__.py -------------------------------------------------------------------------------- /engine/apps/mattermost/models/__init__.py: -------------------------------------------------------------------------------- 1 | from .channel import MattermostChannel # noqa: F401 2 | from .message import MattermostMessage # noqa F401 3 | from .user import MattermostUser # noqa F401 4 | -------------------------------------------------------------------------------- /engine/apps/mattermost/signals.py: -------------------------------------------------------------------------------- 1 | from apps.alerts.signals import alert_create_signal, alert_group_action_triggered_signal 2 | from apps.mattermost.alert_group_representative import AlertGroupMattermostRepresentative 3 | 4 | alert_create_signal.connect(AlertGroupMattermostRepresentative.on_create_alert) 5 | alert_group_action_triggered_signal.connect(AlertGroupMattermostRepresentative.on_alert_group_action_triggered) 6 | -------------------------------------------------------------------------------- /engine/apps/mattermost/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/engine/apps/mattermost/tests/__init__.py -------------------------------------------------------------------------------- /engine/apps/mattermost/tests/events/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/engine/apps/mattermost/tests/events/__init__.py -------------------------------------------------------------------------------- /engine/apps/mattermost/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import include, path 2 | 3 | from common.api_helpers.optional_slash_router import OptionalSlashRouter, optional_slash_path 4 | 5 | from .views import MattermostChannelViewSet, MattermostEventView 6 | 7 | app_name = "mattermost" 8 | router = OptionalSlashRouter() 9 | router.register(r"channels", MattermostChannelViewSet, basename="channel") 10 | 11 | urlpatterns = [ 12 | path("", include(router.urls)), 13 | optional_slash_path("event", MattermostEventView.as_view(), name="incoming_mattermost_event"), 14 | ] 15 | -------------------------------------------------------------------------------- /engine/apps/metrics_exporter/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/engine/apps/metrics_exporter/__init__.py -------------------------------------------------------------------------------- /engine/apps/metrics_exporter/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class MetricsExporterConfig(AppConfig): 5 | name = "apps.metrics_exporter" 6 | 7 | def ready(self): 8 | from . import signals # noqa: F401 9 | -------------------------------------------------------------------------------- /engine/apps/metrics_exporter/signals.py: -------------------------------------------------------------------------------- 1 | from apps.alerts.constants import AlertGroupState 2 | from apps.alerts.signals import alert_group_created_signal 3 | 4 | 5 | def on_alert_group_created(**kwargs): 6 | alert_group = kwargs["alert_group"] 7 | if alert_group.is_maintenance_incident is True: 8 | return 9 | alert_group._update_metrics( 10 | organization_id=alert_group.channel.organization_id, previous_state=None, state=AlertGroupState.FIRING 11 | ) 12 | 13 | 14 | alert_group_created_signal.connect(on_alert_group_created) 15 | -------------------------------------------------------------------------------- /engine/apps/metrics_exporter/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/engine/apps/metrics_exporter/tests/__init__.py -------------------------------------------------------------------------------- /engine/apps/metrics_exporter/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from .views import MetricsExporterView 4 | 5 | urlpatterns = [ 6 | path("", MetricsExporterView.as_view(), name="metrics-exporter"), 7 | ] 8 | -------------------------------------------------------------------------------- /engine/apps/mobile_app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/engine/apps/mobile_app/__init__.py -------------------------------------------------------------------------------- /engine/apps/mobile_app/exceptions.py: -------------------------------------------------------------------------------- 1 | class DeviceNotSet(Exception): 2 | """ 3 | Indicates that user has no connected fcm device. 4 | Introduced only for test_push_notification handler. 5 | We should have generic test notifications system across all messaging backends. 6 | """ 7 | -------------------------------------------------------------------------------- /engine/apps/mobile_app/migrations/0005_mobileappusersettings_important_notification_volume_override.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.18 on 2023-05-08 16:52 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('mobile_app', '0004_auto_20230425_1033'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='mobileappusersettings', 15 | name='important_notification_volume_override', 16 | field=models.BooleanField(default=True, null=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /engine/apps/mobile_app/migrations/0008_mobileappusersettings_locale.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.19 on 2023-06-08 10:51 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('mobile_app', '0007_alter_mobileappusersettings_info_notifications_enabled'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='mobileappusersettings', 15 | name='locale', 16 | field=models.CharField(max_length=50, null=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /engine/apps/mobile_app/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/engine/apps/mobile_app/migrations/__init__.py -------------------------------------------------------------------------------- /engine/apps/mobile_app/tasks/__init__.py: -------------------------------------------------------------------------------- 1 | from .going_oncall_notification import ( # noqa:F401 2 | conditionally_send_going_oncall_push_notifications_for_all_schedules, 3 | conditionally_send_going_oncall_push_notifications_for_schedule, 4 | ) 5 | from .new_alert_group import notify_user_about_new_alert_group # noqa:F401 6 | from .new_shift_swap_request import ( # noqa:F401 7 | notify_beneficiary_about_taken_shift_swap_request, 8 | notify_shift_swap_request, 9 | notify_shift_swap_requests, 10 | notify_user_about_shift_swap_request, 11 | ) 12 | -------------------------------------------------------------------------------- /engine/apps/mobile_app/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/engine/apps/mobile_app/tests/__init__.py -------------------------------------------------------------------------------- /engine/apps/mobile_app/tests/tasks/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/engine/apps/mobile_app/tests/tasks/__init__.py -------------------------------------------------------------------------------- /engine/apps/mobile_app/tests/test_fcm_device_model.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from apps.mobile_app.models import FCMDevice 4 | 5 | 6 | @pytest.mark.django_db 7 | def test_get_active_device_for_user_works(make_organization_and_user): 8 | _, user = make_organization_and_user() 9 | FCMDevice.objects.create(user=user, registration_id="inactive_device_id", active=False) 10 | active_device = FCMDevice.objects.create(user=user, registration_id="active_device_id", active=True) 11 | 12 | assert FCMDevice.objects.filter(user=user).count() == 2 13 | assert FCMDevice.get_active_device_for_user(user) == active_device 14 | -------------------------------------------------------------------------------- /engine/apps/mobile_app/types.py: -------------------------------------------------------------------------------- 1 | import typing 2 | from enum import StrEnum 3 | 4 | 5 | class MessageType(StrEnum): 6 | DEFAULT = "oncall.message" 7 | IMPORTANT = "oncall.critical_message" 8 | INFO = "oncall.info" 9 | 10 | 11 | class Platform(StrEnum): 12 | ANDROID = "android" 13 | IOS = "ios" 14 | 15 | 16 | class FCMMessageData(typing.TypedDict): 17 | title: str 18 | subtitle: typing.Optional[str] 19 | body: typing.Optional[str] 20 | -------------------------------------------------------------------------------- /engine/apps/oss_installation/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/engine/apps/oss_installation/__init__.py -------------------------------------------------------------------------------- /engine/apps/oss_installation/constants.py: -------------------------------------------------------------------------------- 1 | from enum import IntEnum 2 | 3 | 4 | class CloudSyncStatus(IntEnum): 5 | NOT_SYNCED = 0 6 | SYNCED_USER_NOT_FOUND = 1 7 | SYNCED_PHONE_NOT_VERIFIED = 2 8 | SYNCED_PHONE_VERIFIED = 3 9 | -------------------------------------------------------------------------------- /engine/apps/oss_installation/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/engine/apps/oss_installation/migrations/__init__.py -------------------------------------------------------------------------------- /engine/apps/oss_installation/models/__init__.py: -------------------------------------------------------------------------------- 1 | from .cloud_connector import CloudConnector # noqa: F401 2 | from .cloud_heartbeat import CloudHeartbeat # noqa: F401 3 | from .cloud_user_identity import CloudUserIdentity # noqa: F401 4 | from .oss_installation import OssInstallation # noqa: F401 5 | -------------------------------------------------------------------------------- /engine/apps/oss_installation/models/cloud_user_identity.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class CloudUserIdentity(models.Model): 5 | phone_number_verified = models.BooleanField(default=False) 6 | cloud_id = models.CharField(max_length=20) 7 | email = models.EmailField() 8 | -------------------------------------------------------------------------------- /engine/apps/oss_installation/models/oss_installation.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import uuid 3 | 4 | from django.db import models 5 | 6 | logger = logging.getLogger(__name__) 7 | 8 | 9 | class OssInstallation(models.Model): 10 | """ 11 | OssInstallation is model to track installation of OSS OnCall version. 12 | """ 13 | 14 | installation_id = models.UUIDField(default=uuid.uuid4, editable=False) 15 | created_at = models.DateTimeField(auto_now=True) 16 | report_sent_at = models.DateTimeField(null=True, default=None) 17 | -------------------------------------------------------------------------------- /engine/apps/oss_installation/serializers/__init__.py: -------------------------------------------------------------------------------- 1 | from .cloud_user import CloudUserSerializer # noqa: F401 2 | -------------------------------------------------------------------------------- /engine/apps/oss_installation/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/engine/apps/oss_installation/tests/__init__.py -------------------------------------------------------------------------------- /engine/apps/oss_installation/views/__init__.py: -------------------------------------------------------------------------------- 1 | from .cloud_connection import CloudConnectionView # noqa: F401 2 | from .cloud_heartbeat import CloudHeartbeatView # noqa: F401 3 | from .cloud_users import CloudUsersView, CloudUserView # noqa: F401 4 | -------------------------------------------------------------------------------- /engine/apps/phone_notifications/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/engine/apps/phone_notifications/__init__.py -------------------------------------------------------------------------------- /engine/apps/phone_notifications/migrations/0003_smsrecord_represents_bundle_uuid.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.10 on 2024-07-03 08:15 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('phone_notifications', '0002_bannedphonenumber'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='smsrecord', 15 | name='represents_bundle_uuid', 16 | field=models.CharField(db_index=True, default=None, max_length=100, null=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /engine/apps/phone_notifications/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/engine/apps/phone_notifications/migrations/__init__.py -------------------------------------------------------------------------------- /engine/apps/phone_notifications/models/__init__.py: -------------------------------------------------------------------------------- 1 | from .phone_call import PhoneCallRecord, ProviderPhoneCall # noqa: F401 2 | from .sms import ProviderSMS, SMSRecord # noqa: F401 3 | -------------------------------------------------------------------------------- /engine/apps/phone_notifications/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/engine/apps/phone_notifications/tests/__init__.py -------------------------------------------------------------------------------- /engine/apps/phone_notifications/tests/factories.py: -------------------------------------------------------------------------------- 1 | import factory 2 | 3 | from apps.phone_notifications.models import PhoneCallRecord, SMSRecord 4 | 5 | 6 | class PhoneCallRecordFactory(factory.DjangoModelFactory): 7 | class Meta: 8 | model = PhoneCallRecord 9 | 10 | 11 | class SMSRecordFactory(factory.DjangoModelFactory): 12 | class Meta: 13 | model = SMSRecord 14 | -------------------------------------------------------------------------------- /engine/apps/public_api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/engine/apps/public_api/__init__.py -------------------------------------------------------------------------------- /engine/apps/public_api/constants.py: -------------------------------------------------------------------------------- 1 | from django.utils import dateparse 2 | 3 | VALID_DATE_FOR_DELETE_INCIDENT = dateparse.parse_date("2020-07-04") 4 | -------------------------------------------------------------------------------- /engine/apps/public_api/custom_renderers.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from rest_framework.renderers import BaseRenderer 4 | 5 | 6 | class CalendarRenderer(BaseRenderer): 7 | """ 8 | A basic customer renderer to set the format to text to remove escape characters 9 | on feed requests. 10 | """ 11 | 12 | media_type = "text/calendar" 13 | format = "txt" 14 | 15 | def render(self, data, accepted_media_type=None, renderer_context=None): 16 | if isinstance(data, bytes): 17 | return data 18 | error_response = json.dumps(data) 19 | return bytes(error_response.encode("utf-8")) 20 | -------------------------------------------------------------------------------- /engine/apps/public_api/serializers/escalation.py: -------------------------------------------------------------------------------- 1 | from apps.api.serializers.direct_paging import BasePagingSerializer 2 | 3 | 4 | class EscalationSerializer(BasePagingSerializer): 5 | """ 6 | Very similar to `apps.api.serializers.direct_paging.DirectPagingSerializer` except that 7 | there is no `grafana_incident_id` attribute 8 | """ 9 | -------------------------------------------------------------------------------- /engine/apps/public_api/serializers/integtration_heartbeat.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | 3 | from apps.heartbeat.models import IntegrationHeartBeat 4 | 5 | 6 | class IntegrationHeartBeatSerializer(serializers.ModelSerializer): 7 | class Meta: 8 | model = IntegrationHeartBeat 9 | fields = [ 10 | "link", 11 | ] 12 | -------------------------------------------------------------------------------- /engine/apps/public_api/serializers/organizations.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | 3 | from apps.user_management.models import Organization 4 | 5 | 6 | class OrganizationSerializer(serializers.ModelSerializer): 7 | id = serializers.ReadOnlyField(read_only=True, source="public_primary_key") 8 | 9 | class Meta: 10 | model = Organization 11 | fields = ["id"] 12 | read_only_fields = ["id"] 13 | -------------------------------------------------------------------------------- /engine/apps/public_api/serializers/slack_channel.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | 3 | from apps.slack.models import SlackChannel 4 | 5 | 6 | class SlackChannelSerializer(serializers.ModelSerializer): 7 | class Meta: 8 | model = SlackChannel 9 | fields = ["name", "slack_id"] 10 | -------------------------------------------------------------------------------- /engine/apps/public_api/serializers/teams.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | 3 | from apps.user_management.models import Team 4 | 5 | 6 | class TeamSerializer(serializers.ModelSerializer): 7 | id = serializers.CharField(read_only=True, source="public_primary_key") 8 | grafana_id = serializers.IntegerField(read_only=True, source="team_id") 9 | 10 | class Meta: 11 | model = Team 12 | fields = [ 13 | "id", 14 | "grafana_id", 15 | "name", 16 | "email", 17 | "avatar_url", 18 | ] 19 | -------------------------------------------------------------------------------- /engine/apps/public_api/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/engine/apps/public_api/tests/__init__.py -------------------------------------------------------------------------------- /engine/apps/public_api/throttlers/__init__.py: -------------------------------------------------------------------------------- 1 | from .info_throttler import InfoThrottler # noqa: F401 2 | from .phone_notification_throttler import PhoneNotificationThrottler # noqa: F401 3 | from .user_throttle import UserThrottle # noqa: F401 4 | -------------------------------------------------------------------------------- /engine/apps/public_api/throttlers/info_throttler.py: -------------------------------------------------------------------------------- 1 | from common.api_helpers.custom_rate_scoped_throttler import CustomRateUserThrottler 2 | 3 | 4 | class InfoThrottler(CustomRateUserThrottler): 5 | scope = "info" 6 | rate = "100/m" 7 | -------------------------------------------------------------------------------- /engine/apps/public_api/throttlers/phone_notification_throttler.py: -------------------------------------------------------------------------------- 1 | from common.api_helpers.custom_rate_scoped_throttler import CustomRateUserThrottler 2 | 3 | 4 | class PhoneNotificationThrottler(CustomRateUserThrottler): 5 | scope = "phone_notification" 6 | rate = "60/m" 7 | -------------------------------------------------------------------------------- /engine/apps/public_api/throttlers/user_throttle.py: -------------------------------------------------------------------------------- 1 | from common.api_helpers.custom_rate_scoped_throttler import CustomRateUserThrottler 2 | 3 | 4 | class UserThrottle(CustomRateUserThrottler): 5 | scope = "public_api" 6 | rate = "300/m" 7 | -------------------------------------------------------------------------------- /engine/apps/schedules/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/engine/apps/schedules/__init__.py -------------------------------------------------------------------------------- /engine/apps/schedules/exceptions.py: -------------------------------------------------------------------------------- 1 | class BeneficiaryCannotTakeOwnShiftSwapRequest(Exception): 2 | """ 3 | Raised when a beneficiary tries to 'take' their own shift swap request 4 | """ 5 | 6 | 7 | class ShiftSwapRequestNotOpenForTaking(Exception): 8 | """ 9 | Indicates that the shift swap request is not in a state which allows it to be assigned to a benefactor (aka "taken") 10 | """ 11 | -------------------------------------------------------------------------------- /engine/apps/schedules/ical_events/__init__.py: -------------------------------------------------------------------------------- 1 | from apps.schedules.ical_events.adapter.amixr_recurring_ical_events_adapter import AmixrRecurringIcalEventsAdapter 2 | from apps.schedules.ical_events.proxy.ical_proxy import IcalProxy 3 | 4 | adapter = AmixrRecurringIcalEventsAdapter() 5 | ical_events = IcalProxy(adapter) 6 | -------------------------------------------------------------------------------- /engine/apps/schedules/ical_events/adapter/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/engine/apps/schedules/ical_events/adapter/__init__.py -------------------------------------------------------------------------------- /engine/apps/schedules/ical_events/proxy/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/engine/apps/schedules/ical_events/proxy/__init__.py -------------------------------------------------------------------------------- /engine/apps/schedules/migrations/0004_customoncallshift_until.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.5 on 2022-06-28 13:07 2 | 3 | from django.db import migrations, models 4 | import django_migration_linter as linter 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('schedules', '0003_alter_customoncallshift_frequency'), 11 | ] 12 | 13 | operations = [ 14 | linter.IgnoreMigration(), 15 | migrations.AddField( 16 | model_name='customoncallshift', 17 | name='until', 18 | field=models.DateTimeField(default=None, null=True), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /engine/apps/schedules/migrations/0009_oncallschedulecalendar_enable_web_overrides.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.18 on 2023-03-08 18:29 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('schedules', '0008_auto_20221201_0809'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='oncallschedulecalendar', 15 | name='enable_web_overrides', 16 | field=models.BooleanField(default=False, null=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /engine/apps/schedules/migrations/0011_oncallschedule_cached_ical_final_schedule.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.18 on 2023-04-11 19:03 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('schedules', '0010_fix_polymorphic_delete_related'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='oncallschedule', 15 | name='cached_ical_final_schedule', 16 | field=models.TextField(default=None, null=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /engine/apps/schedules/migrations/0012_auto_20230502_1259.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.18 on 2023-05-02 12:59 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('schedules', '0011_oncallschedule_cached_ical_final_schedule'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterModelOptions( 14 | name='oncallschedule', 15 | options={'base_manager_name': 'objects'}, 16 | ), 17 | migrations.AlterUniqueTogether( 18 | name='oncallschedule', 19 | unique_together=set(), 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /engine/apps/schedules/migrations/0016_alter_shiftswaprequest_created_at.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.20 on 2023-08-01 18:16 2 | 3 | from django.db import migrations, models 4 | import django.utils.timezone 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('schedules', '0015_shiftswaprequest_slack_message'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='shiftswaprequest', 16 | name='created_at', 17 | field=models.DateTimeField(default=django.utils.timezone.now), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /engine/apps/schedules/migrations/0020_remove_oncallschedule_channel_state.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.16 on 2024-11-20 20:12 2 | 3 | import common.migrations.remove_field 4 | from django.db import migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('schedules', '0019_auto_20241021_1735'), 11 | ] 12 | 13 | operations = [ 14 | common.migrations.remove_field.RemoveFieldState( 15 | model_name='oncallschedule', 16 | name='channel', 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /engine/apps/schedules/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/engine/apps/schedules/migrations/__init__.py -------------------------------------------------------------------------------- /engine/apps/schedules/models/__init__.py: -------------------------------------------------------------------------------- 1 | from .custom_on_call_shift import CustomOnCallShift # noqa: F401 2 | from .on_call_schedule import ( # noqa: F401 3 | OnCallSchedule, 4 | OnCallScheduleCalendar, 5 | OnCallScheduleICal, 6 | OnCallScheduleWeb, 7 | ) 8 | from .shift_swap_request import ShiftSwapRequest # noqa: F401 9 | -------------------------------------------------------------------------------- /engine/apps/schedules/tasks/shift_swaps/__init__.py: -------------------------------------------------------------------------------- 1 | from .notify_when_taken import notify_beneficiary_about_taken_shift_swap_request # noqa: F401 2 | from .slack_followups import send_shift_swap_request_slack_followups # noqa: F401 3 | from .slack_messages import create_shift_swap_request_message, update_shift_swap_request_message # noqa: F401 4 | -------------------------------------------------------------------------------- /engine/apps/schedules/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/engine/apps/schedules/tests/__init__.py -------------------------------------------------------------------------------- /engine/apps/schedules/tests/calendars/override_email_case_sensitivity.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | PRODID:-//Google Inc//Google Calendar 70.9054//EN 3 | VERSION:2.0 4 | CALSCALE:GREGORIAN 5 | METHOD:PUBLISH 6 | X-WR-CALNAME:test 7 | X-WR-TIMEZONE:Europe/London 8 | BEGIN:VEVENT 9 | DTSTART:20230206T110000Z 10 | DTEND:20230206T120000Z 11 | DTSTAMP:20230206T112228Z 12 | UID:16m6o1qji0fdg49coeflc202ss@google.com 13 | CREATED:20230206T112217Z 14 | DESCRIPTION: 15 | LAST-MODIFIED:20230206T112217Z 16 | SEQUENCE:0 17 | STATUS:CONFIRMED 18 | SUMMARY:Test@TEST.test 19 | TRANSP:OPAQUE 20 | END:VEVENT 21 | END:VCALENDAR 22 | -------------------------------------------------------------------------------- /engine/apps/schedules/tests/conftest.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pytest 4 | from icalendar import Calendar 5 | 6 | CALENDARS_FOLDER = os.path.join(os.path.dirname(os.path.abspath(__file__)), "calendars") 7 | 8 | 9 | @pytest.fixture() 10 | def get_ical(): 11 | def _get_ical(calendar_name): 12 | path_to_calendar = os.path.join(CALENDARS_FOLDER, calendar_name) 13 | with open(path_to_calendar, "rb") as file: 14 | content = file.read() 15 | return Calendar.from_ical(content) 16 | 17 | return _get_ical 18 | -------------------------------------------------------------------------------- /engine/apps/schedules/tests/tasks/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/engine/apps/schedules/tests/tasks/__init__.py -------------------------------------------------------------------------------- /engine/apps/schedules/tests/tasks/shift_swaps/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/engine/apps/schedules/tests/tasks/shift_swaps/__init__.py -------------------------------------------------------------------------------- /engine/apps/slack/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/engine/apps/slack/__init__.py -------------------------------------------------------------------------------- /engine/apps/slack/constants.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from apps.slack.types import Block 4 | 5 | SLACK_BOT_ID = "USLACKBOT" 6 | SLACK_INVALID_AUTH_RESPONSE = "no_enough_permissions_to_retrieve" 7 | PLACEHOLDER = "Placeholder" 8 | 9 | SLACK_WRONG_TEAM_NAMES = [SLACK_INVALID_AUTH_RESPONSE, PLACEHOLDER] 10 | 11 | SLACK_RATE_LIMIT_TIMEOUT = datetime.timedelta(minutes=5) 12 | SLACK_RATE_LIMIT_DELAY = 10 13 | 14 | BLOCK_SECTION_TEXT_MAX_SIZE = 2800 15 | PRIVATE_METADATA_MAX_LENGTH = 3000 16 | 17 | DIVIDER: Block.Divider = {"type": "divider"} 18 | -------------------------------------------------------------------------------- /engine/apps/slack/migrations/0003_delete_slackactionrecord.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.19 on 2023-06-13 08:59 2 | 3 | from django.db import migrations 4 | import django_migration_linter as linter 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('slack', '0002_squashed_initial'), 11 | ] 12 | 13 | operations = [ 14 | linter.IgnoreMigration(), 15 | migrations.DeleteModel( 16 | name='SlackActionRecord', 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /engine/apps/slack/migrations/0005_slackteamidentity__unified_slack_app_installed.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.11 on 2024-07-16 16:11 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('slack', '0004_auto_20230913_1020'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='slackteamidentity', 15 | name='_unified_slack_app_installed', 16 | field=models.BooleanField(default=False, null=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /engine/apps/slack/migrations/0008_remove_slackmessage_active_update_task_id_state.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.16 on 2024-12-04 12:00 2 | 3 | import common.migrations.remove_field 4 | from django.db import migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('slack', '0007_migrate_slackmessage_channel_id'), 11 | ] 12 | 13 | operations = [ 14 | common.migrations.remove_field.RemoveFieldState( 15 | model_name='SlackMessage', 16 | name='active_update_task_id', 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /engine/apps/slack/migrations/0011_remove_slackmessage__channel_id_state.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.17 on 2024-12-06 17:05 2 | 3 | import common.migrations.remove_field 4 | from django.db import migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('slack', '0010_remove_slackmessage_active_update_task_id_db'), 11 | ] 12 | 13 | operations = [ 14 | common.migrations.remove_field.RemoveFieldState( 15 | model_name='SlackMessage', 16 | name='_channel_id', 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /engine/apps/slack/migrations/0012_remove_slackmessage_organization_state.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.17 on 2024-12-06 17:05 2 | 3 | import common.migrations.remove_field 4 | from django.db import migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('slack', '0011_remove_slackmessage__channel_id_state'), 11 | ] 12 | 13 | operations = [ 14 | common.migrations.remove_field.RemoveFieldState( 15 | model_name='SlackMessage', 16 | name='organization', 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /engine/apps/slack/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/engine/apps/slack/migrations/__init__.py -------------------------------------------------------------------------------- /engine/apps/slack/models/__init__.py: -------------------------------------------------------------------------------- 1 | from .slack_channel import SlackChannel # noqa: F401 2 | from .slack_message import SlackMessage # noqa: F401 3 | from .slack_team_identity import SlackTeamIdentity # noqa: F401 4 | from .slack_user_identity import SlackUserIdentity # noqa: F401 5 | from .slack_usergroup import SlackUserGroup # noqa: F401 6 | -------------------------------------------------------------------------------- /engine/apps/slack/representatives/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/engine/apps/slack/representatives/__init__.py -------------------------------------------------------------------------------- /engine/apps/slack/scenarios/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/engine/apps/slack/scenarios/__init__.py -------------------------------------------------------------------------------- /engine/apps/slack/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/engine/apps/slack/tests/__init__.py -------------------------------------------------------------------------------- /engine/apps/slack/tests/scenario_steps/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/engine/apps/slack/tests/scenario_steps/__init__.py -------------------------------------------------------------------------------- /engine/apps/slack/tests/tasks/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/engine/apps/slack/tests/tasks/__init__.py -------------------------------------------------------------------------------- /engine/apps/slack/types/interaction_payloads/__init__.py: -------------------------------------------------------------------------------- 1 | from .block_actions import BlockActionsPayload 2 | from .dialog_submission import DialogSubmissionPayload 3 | from .interactive_messages import InteractiveMessagesPayload 4 | from .shortcuts import MessageActionPayload 5 | from .slash_command import SlashCommandPayload 6 | from .view_submission import ViewSubmissionPayload 7 | 8 | EventPayload = ( 9 | BlockActionsPayload 10 | | DialogSubmissionPayload 11 | | InteractiveMessagesPayload 12 | | MessageActionPayload 13 | | SlashCommandPayload 14 | | ViewSubmissionPayload 15 | ) 16 | -------------------------------------------------------------------------------- /engine/apps/social_auth/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/engine/apps/social_auth/__init__.py -------------------------------------------------------------------------------- /engine/apps/social_auth/exceptions.py: -------------------------------------------------------------------------------- 1 | class InstallMultiRegionSlackException(Exception): 2 | pass 3 | 4 | 5 | class UserLoginOAuth2MattermostException(Exception): 6 | pass 7 | 8 | 9 | GOOGLE_AUTH_MISSING_GRANTED_SCOPE_ERROR = "missing_granted_scope" 10 | MATTERMOST_AUTH_FETCH_USER_ERROR = "failed_to_fetch_user" 11 | -------------------------------------------------------------------------------- /engine/apps/social_auth/pipeline/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/engine/apps/social_auth/pipeline/__init__.py -------------------------------------------------------------------------------- /engine/apps/social_auth/types.py: -------------------------------------------------------------------------------- 1 | import typing 2 | 3 | 4 | class GoogleOauth2Response(typing.TypedDict): 5 | sub: str 6 | scope: str 7 | access_token: str 8 | refresh_token: typing.Optional[str] 9 | """ 10 | NOTE: I think `refresh_token` is only included when the user initially grants access to our Google OAuth2 app 11 | on subsequent logins, the `refresh_token` is not included in the response, only `access_token` 12 | 13 | https://medium.com/starthinker/google-oauth-2-0-access-token-and-refresh-token-explained-cccf2fc0a6d9 14 | """ 15 | -------------------------------------------------------------------------------- /engine/apps/telegram/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/engine/apps/telegram/__init__.py -------------------------------------------------------------------------------- /engine/apps/telegram/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class TelegramConfig(AppConfig): 5 | name = "apps.telegram" 6 | 7 | def ready(self): 8 | import apps.telegram.signals # noqa: F401 9 | -------------------------------------------------------------------------------- /engine/apps/telegram/exceptions.py: -------------------------------------------------------------------------------- 1 | class AlertGroupTelegramMessageDoesNotExist(Exception): 2 | pass 3 | -------------------------------------------------------------------------------- /engine/apps/telegram/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/engine/apps/telegram/migrations/__init__.py -------------------------------------------------------------------------------- /engine/apps/telegram/models/__init__.py: -------------------------------------------------------------------------------- 1 | from .message import TelegramMessage # noqa: F401, isort: skip 2 | from .connectors.channel import TelegramToOrganizationConnector # noqa: F401 3 | from .connectors.personal import TelegramToUserConnector # noqa: F401 4 | from .verification.channel import TelegramChannelVerificationCode # noqa: F401 5 | from .verification.personal import TelegramVerificationCode # noqa: F401 6 | -------------------------------------------------------------------------------- /engine/apps/telegram/models/connectors/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/engine/apps/telegram/models/connectors/__init__.py -------------------------------------------------------------------------------- /engine/apps/telegram/models/verification/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/engine/apps/telegram/models/verification/__init__.py -------------------------------------------------------------------------------- /engine/apps/telegram/renderers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/engine/apps/telegram/renderers/__init__.py -------------------------------------------------------------------------------- /engine/apps/telegram/signals.py: -------------------------------------------------------------------------------- 1 | from apps.alerts.signals import ( 2 | alert_create_signal, 3 | alert_group_action_triggered_signal, 4 | alert_group_update_log_report_signal, 5 | ) 6 | from apps.telegram.alert_group_representative import AlertGroupTelegramRepresentative 7 | 8 | alert_create_signal.connect(AlertGroupTelegramRepresentative.on_create_alert) 9 | alert_group_action_triggered_signal.connect(AlertGroupTelegramRepresentative.on_alert_group_action_triggered) 10 | alert_group_update_log_report_signal.connect(AlertGroupTelegramRepresentative.on_alert_group_update_log_report) 11 | -------------------------------------------------------------------------------- /engine/apps/telegram/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/engine/apps/telegram/tests/__init__.py -------------------------------------------------------------------------------- /engine/apps/telegram/updates/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/engine/apps/telegram/updates/__init__.py -------------------------------------------------------------------------------- /engine/apps/telegram/updates/update_handlers/__init__.py: -------------------------------------------------------------------------------- 1 | from .update_handler import UpdateHandler # noqa: F401, isort: skip 2 | from .button_press import ButtonPressHandler # noqa: F401 3 | from .channel_to_group_forward import ChannelToGroupForwardHandler # noqa: F401 4 | from .start_message import StartMessageHandler # noqa: F401 5 | from .verification.channel import ChannelVerificationCodeHandler # noqa: F401 6 | from .verification.personal import PersonalVerificationCodeHandler # noqa: F401 7 | -------------------------------------------------------------------------------- /engine/apps/telegram/updates/update_handlers/update_handler.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | 3 | from telegram import Update 4 | 5 | 6 | class UpdateHandler(ABC): 7 | """ 8 | Update handler for Telegram update 9 | After making new handler by subclassing this abstract class, make sure to add it to __init__.py 10 | """ 11 | 12 | def __init__(self, update: Update): 13 | self.update = update 14 | 15 | @abstractmethod 16 | def matches(self) -> bool: 17 | pass 18 | 19 | @abstractmethod 20 | def process_update(self) -> None: 21 | pass 22 | -------------------------------------------------------------------------------- /engine/apps/telegram/updates/update_handlers/verification/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/engine/apps/telegram/updates/update_handlers/verification/__init__.py -------------------------------------------------------------------------------- /engine/apps/telegram/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from .views import WebHookView 4 | 5 | app_name = "telegram" 6 | 7 | urlpatterns = [ 8 | path("", WebHookView.as_view(), name="incoming_webhook"), 9 | ] 10 | -------------------------------------------------------------------------------- /engine/apps/telegram/views.py: -------------------------------------------------------------------------------- 1 | from rest_framework.response import Response 2 | from rest_framework.views import APIView 3 | 4 | from apps.telegram.updates.update_manager import UpdateManager 5 | 6 | 7 | class WebHookView(APIView): 8 | def get(self, request, format=None): 9 | return Response("hello") 10 | 11 | def post(self, request): 12 | UpdateManager.process_request(request) 13 | return Response(status=200) 14 | -------------------------------------------------------------------------------- /engine/apps/twilioapp/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/engine/apps/twilioapp/__init__.py -------------------------------------------------------------------------------- /engine/apps/twilioapp/migrations/0003_auto_20230408_0711.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.18 on 2023-04-08 07:11 2 | 3 | from django.db import migrations 4 | import django_migration_linter as linter 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('twilioapp', '0002_auto_20220604_1008'), 11 | ] 12 | 13 | state_operations = [ 14 | migrations.DeleteModel('PhoneCall'), 15 | migrations.DeleteModel('SMSMessage') 16 | ] 17 | 18 | operations = [ 19 | migrations.SeparateDatabaseAndState( 20 | state_operations=state_operations 21 | ) 22 | ] 23 | -------------------------------------------------------------------------------- /engine/apps/twilioapp/migrations/0007_delete_twiliologrecord.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.20 on 2023-07-12 05:32 2 | 3 | from django.db import migrations 4 | import django_migration_linter as linter 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('twilioapp', '0006_auto_20230601_0807'), 11 | ] 12 | 13 | operations = [ 14 | linter.IgnoreMigration(), 15 | migrations.DeleteModel( 16 | name='TwilioLogRecord', 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /engine/apps/twilioapp/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/engine/apps/twilioapp/migrations/__init__.py -------------------------------------------------------------------------------- /engine/apps/twilioapp/models/__init__.py: -------------------------------------------------------------------------------- 1 | from .twilio_phone_call import TwilioCallStatuses, TwilioPhoneCall # noqa: F401 2 | from .twilio_sender import ( # noqa: F401 3 | TwilioAccount, 4 | TwilioPhoneCallSender, 5 | TwilioSender, 6 | TwilioSmsSender, 7 | TwilioVerificationSender, 8 | ) 9 | from .twilio_sms import TwilioSMS, TwilioSMSstatuses # noqa: F401 10 | -------------------------------------------------------------------------------- /engine/apps/twilioapp/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/engine/apps/twilioapp/tests/__init__.py -------------------------------------------------------------------------------- /engine/apps/twilioapp/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from .views import CallStatusCallback, GatherView, HealthCheckView, SMSStatusCallback 4 | 5 | app_name = "twilioapp" 6 | 7 | urlpatterns = [ 8 | path("healthz", HealthCheckView.as_view()), 9 | path("gather/", GatherView.as_view(), name="gather"), 10 | path("sms_status_events/", SMSStatusCallback.as_view(), name="sms_status_events"), 11 | path("call_status_events/", CallStatusCallback.as_view(), name="call_status_events"), 12 | ] 13 | -------------------------------------------------------------------------------- /engine/apps/user_management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/engine/apps/user_management/__init__.py -------------------------------------------------------------------------------- /engine/apps/user_management/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | from django.db import models 3 | 4 | 5 | # enable a __lower field lookup for email fields 6 | # https://docs.djangoproject.com/en/4.1/howto/custom-lookups/#a-bilateral-transformer-example 7 | class LowerCase(models.Transform): 8 | lookup_name = "lower" 9 | function = "LOWER" 10 | 11 | 12 | class UserManagementConfig(AppConfig): 13 | name = "apps.user_management" 14 | 15 | def ready(self): 16 | models.EmailField.register_lookup(LowerCase) 17 | -------------------------------------------------------------------------------- /engine/apps/user_management/exceptions.py: -------------------------------------------------------------------------------- 1 | from .models import Organization 2 | 3 | 4 | class OrganizationDeletedException(Exception): 5 | def __init__(self, organization: Organization): 6 | self.organization = organization 7 | 8 | 9 | class OrganizationMovedException(Exception): 10 | def __init__(self, organization: Organization): 11 | self.organization = organization 12 | -------------------------------------------------------------------------------- /engine/apps/user_management/migrations/0003_user_hide_phone_number.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.5 on 2022-08-25 08:51 2 | 3 | from django.db import migrations, models 4 | import django_migration_linter as linter 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('user_management', '0002_auto_20220705_1214'), 11 | ] 12 | 13 | operations = [ 14 | linter.IgnoreMigration(), 15 | migrations.AddField( 16 | model_name='user', 17 | name='hide_phone_number', 18 | field=models.BooleanField(default=False), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /engine/apps/user_management/migrations/0007_organization_deleted_at.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.16 on 2023-01-04 05:06 2 | 3 | from django.db import migrations, models 4 | import django_migration_linter as linter 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('user_management', '0006_organization_uuid'), 11 | ] 12 | 13 | operations = [ 14 | linter.IgnoreMigration(), 15 | migrations.AddField( 16 | model_name='organization', 17 | name='deleted_at', 18 | field=models.DateTimeField(null=True), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /engine/apps/user_management/migrations/0009_organization_cluster_slug.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.18 on 2023-03-08 04:46 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('user_management', '0008_organization_is_grafana_incident_enabled'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='organization', 15 | name='cluster_slug', 16 | field=models.CharField(default=None, max_length=300, null=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /engine/apps/user_management/migrations/0016_alter_user_role.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.20 on 2023-10-18 18:10 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('user_management', '0015_auto_20230926_2203'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='user', 15 | name='role', 16 | field=models.PositiveSmallIntegerField(choices=[(0, 'ADMIN'), (1, 'EDITOR'), (2, 'VIEWER'), (3, 'NONE')]), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /engine/apps/user_management/migrations/0019_organization_grafana_incident_backend_url.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.7 on 2023-11-29 15:44 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('user_management', '0018_auto_20231115_1206'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='organization', 15 | name='grafana_incident_backend_url', 16 | field=models.CharField(default=None, max_length=300, null=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /engine/apps/user_management/migrations/0020_organization_is_grafana_labels_enabled.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.7 on 2024-01-30 07:17 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('user_management', '0019_organization_grafana_incident_backend_url'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='organization', 15 | name='is_grafana_labels_enabled', 16 | field=models.BooleanField(default=False, null=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /engine/apps/user_management/migrations/0021_user_google_calendar_settings.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.10 on 2024-03-18 22:13 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('user_management', '0020_organization_is_grafana_labels_enabled'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='user', 15 | name='google_calendar_settings', 16 | field=models.JSONField(default=None, null=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /engine/apps/user_management/migrations/0023_organization_is_grafana_irm_enabled.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.15 on 2024-10-08 14:57 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('user_management', '0022_alter_team_unique_together'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='organization', 15 | name='is_grafana_irm_enabled', 16 | field=models.BooleanField(default=False, null=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /engine/apps/user_management/migrations/0024_organization_direct_paging_prefer_important_policy.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.15 on 2024-10-18 16:50 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('user_management', '0023_organization_is_grafana_irm_enabled'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='organization', 15 | name='direct_paging_prefer_important_policy', 16 | field=models.BooleanField(default=False, null=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /engine/apps/user_management/migrations/0028_remove_organization_general_log_channel_id_state.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.16 on 2024-11-20 17:58 2 | 3 | import common.migrations.remove_field 4 | from django.db import migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('user_management', '0027_serviceaccount'), 11 | ] 12 | 13 | operations = [ 14 | common.migrations.remove_field.RemoveFieldState( 15 | model_name='organization', 16 | name='general_log_channel_id', 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /engine/apps/user_management/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/engine/apps/user_management/migrations/__init__.py -------------------------------------------------------------------------------- /engine/apps/user_management/models/__init__.py: -------------------------------------------------------------------------------- 1 | from .user import User # noqa: F401, isort: skip 2 | from .organization import Organization # noqa: F401 3 | from .region import Region # noqa: F401 4 | from .service_account import ServiceAccount, ServiceAccountUser # noqa: F401 5 | from .team import Team # noqa: F401 6 | -------------------------------------------------------------------------------- /engine/apps/user_management/signals.py: -------------------------------------------------------------------------------- 1 | import django.dispatch 2 | 3 | org_sync_signal = django.dispatch.Signal() 4 | -------------------------------------------------------------------------------- /engine/apps/user_management/subscription_strategy/__init__.py: -------------------------------------------------------------------------------- 1 | from .free_public_beta_subscription_strategy import FreePublicBetaSubscriptionStrategy # noqa: F401 2 | -------------------------------------------------------------------------------- /engine/apps/user_management/subscription_strategy/base_subsription_strategy.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | 3 | 4 | class BaseSubscriptionStrategy(ABC): 5 | def __init__(self, organization): 6 | self.organization = organization 7 | 8 | @abstractmethod 9 | def phone_calls_left(self, user): 10 | raise NotImplementedError 11 | 12 | @abstractmethod 13 | def sms_left(self, user): 14 | raise NotImplementedError 15 | 16 | @abstractmethod 17 | def emails_left(self, user): 18 | raise NotImplementedError 19 | -------------------------------------------------------------------------------- /engine/apps/user_management/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/engine/apps/user_management/tests/__init__.py -------------------------------------------------------------------------------- /engine/apps/user_management/tests/test_team.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from django.db.utils import IntegrityError 3 | 4 | 5 | @pytest.mark.django_db 6 | def test_team_uniqueness(make_organization): 7 | organization = make_organization() 8 | 9 | # Create a team 10 | organization.teams.create(name="Team 1", team_id=1) 11 | 12 | # Try to create another team with the same team_id 13 | with pytest.raises(IntegrityError): 14 | organization.teams.create(name="Team 2", team_id=1) 15 | -------------------------------------------------------------------------------- /engine/apps/user_management/types.py: -------------------------------------------------------------------------------- 1 | import typing 2 | 3 | 4 | class AlertGroupTableColumn(typing.TypedDict): 5 | id: str 6 | name: str 7 | type: str 8 | 9 | 10 | class AlertGroupTableColumns(typing.TypedDict): 11 | visible: typing.List[AlertGroupTableColumn] 12 | hidden: typing.List[AlertGroupTableColumn] 13 | default: bool 14 | 15 | 16 | class GoogleCalendarSettings(typing.TypedDict): 17 | oncall_schedules_to_consider_for_shift_swaps: typing.Optional[typing.List[str]] 18 | """ 19 | `public_primary_key` of specific OnCall schedules that should be considered for autogenerated shift swap requests. 20 | """ 21 | -------------------------------------------------------------------------------- /engine/apps/user_management/user_representative.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | 3 | 4 | class UserAbstractRepresentative(ABC): 5 | HANDLER_PREFIX = "on_" 6 | 7 | @abstractmethod 8 | def is_applicable(self): 9 | return None 10 | 11 | @staticmethod 12 | def get_handlers_map(): 13 | from apps.base.models import UserNotificationPolicyLogRecord 14 | 15 | return UserNotificationPolicyLogRecord.TYPE_TO_HANDLERS_MAP 16 | -------------------------------------------------------------------------------- /engine/apps/webhooks/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/engine/apps/webhooks/__init__.py -------------------------------------------------------------------------------- /engine/apps/webhooks/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class WebhooksConfig(AppConfig): 5 | name = "apps.webhooks" 6 | 7 | def ready(self): 8 | from . import signals # noqa: F401 9 | -------------------------------------------------------------------------------- /engine/apps/webhooks/migrations/0005_webhook_is_legacy.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.18 on 2023-04-24 14:07 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('webhooks', '0004_auto_20230418_0109'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='webhook', 15 | name='is_legacy', 16 | field=models.BooleanField(default=False, null=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /engine/apps/webhooks/migrations/0007_webhookresponse_event_data.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.19 on 2023-07-05 18:46 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('webhooks', '0006_auto_20230426_1631'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='webhookresponse', 15 | name='event_data', 16 | field=models.TextField(default=None, null=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /engine/apps/webhooks/migrations/0009_alter_webhook_authorization_header.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.20 on 2023-08-01 19:29 2 | 3 | from django.db import migrations 4 | import mirage.fields 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('webhooks', '0008_auto_20230712_1613'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='webhook', 16 | name='authorization_header', 17 | field=mirage.fields.EncryptedCharField(default=None, max_length=2000, null=True), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /engine/apps/webhooks/migrations/0015_webhook_is_from_connected_integration.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.10 on 2024-02-22 17:47 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('webhooks', '0014_webhook_filtered_integrations'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='webhook', 15 | name='is_from_connected_integration', 16 | field=models.BooleanField(default=False, null=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /engine/apps/webhooks/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/engine/apps/webhooks/migrations/__init__.py -------------------------------------------------------------------------------- /engine/apps/webhooks/models/__init__.py: -------------------------------------------------------------------------------- 1 | from .webhook import PersonalNotificationWebhook, Webhook, WebhookResponse # noqa: F401 2 | -------------------------------------------------------------------------------- /engine/apps/webhooks/presets/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/engine/apps/webhooks/presets/__init__.py -------------------------------------------------------------------------------- /engine/apps/webhooks/signals.py: -------------------------------------------------------------------------------- 1 | from apps.alerts.signals import alert_group_action_triggered_signal, alert_group_escalation_snapshot_built 2 | 3 | from .listeners import on_action_triggered, on_alert_group_created 4 | 5 | alert_group_escalation_snapshot_built.connect(on_alert_group_created) 6 | alert_group_action_triggered_signal.connect(on_action_triggered) 7 | -------------------------------------------------------------------------------- /engine/apps/webhooks/tasks/__init__.py: -------------------------------------------------------------------------------- 1 | from .alert_group_status import alert_group_created, alert_group_status_change # noqa: F401 2 | from .notify_user import notify_user_async # noqa: F401 3 | from .trigger_webhook import execute_webhook, send_webhook_event # noqa: F401 4 | -------------------------------------------------------------------------------- /engine/apps/webhooks/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/engine/apps/webhooks/tests/__init__.py -------------------------------------------------------------------------------- /engine/apps/zvonok/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/engine/apps/zvonok/__init__.py -------------------------------------------------------------------------------- /engine/apps/zvonok/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/engine/apps/zvonok/migrations/__init__.py -------------------------------------------------------------------------------- /engine/apps/zvonok/models/__init__.py: -------------------------------------------------------------------------------- 1 | from .phone_call import ZvonokCallStatuses, ZvonokPhoneCall # noqa: F401 2 | -------------------------------------------------------------------------------- /engine/apps/zvonok/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/engine/apps/zvonok/tests/__init__.py -------------------------------------------------------------------------------- /engine/apps/zvonok/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from .views import CallStatusCallback 4 | 5 | urlpatterns = [ 6 | path("call_status_events", CallStatusCallback.as_view(), name="call_status_events"), 7 | ] 8 | -------------------------------------------------------------------------------- /engine/common/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/engine/common/__init__.py -------------------------------------------------------------------------------- /engine/common/api_helpers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/engine/common/api_helpers/__init__.py -------------------------------------------------------------------------------- /engine/common/cloud_auth_api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/engine/common/cloud_auth_api/__init__.py -------------------------------------------------------------------------------- /engine/common/cloud_auth_api/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/engine/common/cloud_auth_api/tests/__init__.py -------------------------------------------------------------------------------- /engine/common/constants/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/engine/common/constants/__init__.py -------------------------------------------------------------------------------- /engine/common/constants/plugin_ids.py: -------------------------------------------------------------------------------- 1 | class PluginID: 2 | ONCALL = "grafana-oncall-app" 3 | IRM = "grafana-irm-app" 4 | 5 | INCIDENT = "grafana-incident-app" 6 | LABELS = "grafana-labels-app" 7 | ML = "grafana-ml-app" 8 | SLO = "grafana-slo-app" 9 | -------------------------------------------------------------------------------- /engine/common/custom_celery_tasks/__init__.py: -------------------------------------------------------------------------------- 1 | from .dedicated_queue_retry_task import shared_dedicated_queue_retry_task # noqa 2 | -------------------------------------------------------------------------------- /engine/common/custom_celery_tasks/create_alert_base_task.py: -------------------------------------------------------------------------------- 1 | from abc import ABC 2 | 3 | from common.custom_celery_tasks.dedicated_queue_retry_task import DedicatedQueueRetryTask 4 | from common.custom_celery_tasks.safe_to_broker_outage_task import SafeToBrokerOutageTask 5 | 6 | 7 | class CreateAlertBaseTask(SafeToBrokerOutageTask, DedicatedQueueRetryTask, ABC): 8 | pass 9 | -------------------------------------------------------------------------------- /engine/common/custom_celery_tasks/log_exception_on_failure_task.py: -------------------------------------------------------------------------------- 1 | from celery import Task, shared_task 2 | from celery.utils.log import get_task_logger 3 | 4 | logger = logger = get_task_logger(__name__) 5 | 6 | 7 | class LogExceptionOnFailureTask(Task): 8 | def on_failure(self, exc, task_id, args, kwargs, einfo): 9 | logger.exception("An exception occured while executing a celery task", exc_info=exc) 10 | super().on_failure(exc, task_id, args, kwargs, einfo) 11 | 12 | 13 | def shared_log_exception_on_failure_task(*args, **kwargs): 14 | return shared_task(*args, base=LogExceptionOnFailureTask, **kwargs) 15 | -------------------------------------------------------------------------------- /engine/common/custom_celery_tasks/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/engine/common/custom_celery_tasks/tests/__init__.py -------------------------------------------------------------------------------- /engine/common/exceptions/__init__.py: -------------------------------------------------------------------------------- 1 | from .exceptions import ( # noqa: F401 2 | BacksyncIntegrationRequestError, 3 | MaintenanceCouldNotBeStartedError, 4 | TeamCanNotBeChangedError, 5 | UnableToSendDemoAlert, 6 | ) 7 | -------------------------------------------------------------------------------- /engine/common/incident_api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/engine/common/incident_api/__init__.py -------------------------------------------------------------------------------- /engine/common/incident_api/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/engine/common/incident_api/tests/__init__.py -------------------------------------------------------------------------------- /engine/common/insight_log/__init__.py: -------------------------------------------------------------------------------- 1 | from .chatops_insight_logs import ChatOpsEvent, ChatOpsTypePlug, write_chatops_insight_log # noqa 2 | from .maintenance_insight_log import MaintenanceEvent, write_maintenance_insight_log # noqa 3 | from .resource_insight_logs import EntityEvent, write_resource_insight_log # noqa 4 | -------------------------------------------------------------------------------- /engine/common/jinja_templater/__init__.py: -------------------------------------------------------------------------------- 1 | from .apply_jinja_template import apply_jinja_template, apply_jinja_template_to_alert_payload_and_labels # noqa: F401 2 | from .jinja_template_env import jinja_template_env # noqa: F401 3 | -------------------------------------------------------------------------------- /engine/common/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/engine/common/migrations/__init__.py -------------------------------------------------------------------------------- /engine/common/ordered_model/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/engine/common/ordered_model/__init__.py -------------------------------------------------------------------------------- /engine/common/recaptcha/__init__.py: -------------------------------------------------------------------------------- 1 | from .recaptcha_v3 import check_recaptcha_internal_api # noqa 2 | -------------------------------------------------------------------------------- /engine/common/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/engine/common/tests/__init__.py -------------------------------------------------------------------------------- /engine/common/tests/test_regex_replace.py: -------------------------------------------------------------------------------- 1 | from common.jinja_templater.filters import regex_replace 2 | 3 | 4 | def test_regex_replace_drop_field(): 5 | original = "[ var='D0' metric='my_metric' labels={} value=140 ]" 6 | expected = "[ metric='my_metric' labels={} value=140 ]" 7 | assert regex_replace(original, "var='[a-zA-Z0-9]+' ", "") == expected 8 | -------------------------------------------------------------------------------- /engine/engine/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | 3 | # This will make sure the app is always imported when 4 | # Django starts so that shared_task will use this app. 5 | from .celery import app as celery_app 6 | 7 | __all__ = ("celery_app",) 8 | -------------------------------------------------------------------------------- /engine/engine/included_path.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | 3 | 4 | def custom_preprocessing_hook(endpoints): 5 | filtered = [] 6 | for path, path_regex, method, callback in endpoints: 7 | if any(path_prefix in path for path_prefix in settings.SPECTACULAR_INCLUDED_PATHS): 8 | filtered.append((path, path_regex, method, callback)) 9 | return filtered 10 | -------------------------------------------------------------------------------- /engine/engine/integrations_urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import URLPattern, URLResolver, include, path 2 | 3 | from .urls import paths_to_work_even_when_maintenance_mode_is_active 4 | 5 | urlpatterns: list[URLPattern | URLResolver] = paths_to_work_even_when_maintenance_mode_is_active 6 | 7 | urlpatterns += [ 8 | path("integrations/v1/", include("apps.integrations.urls", namespace="integrations")), 9 | ] 10 | -------------------------------------------------------------------------------- /engine/engine/management/commands/create_sqlite_db.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.core.management.base import BaseCommand 3 | from django.db import connection 4 | 5 | 6 | class Command(BaseCommand): 7 | """ 8 | Create SQLite3 database file if it doesn't exist. 9 | """ 10 | 11 | def handle(self, *args, **options): 12 | assert settings.DATABASE_TYPE == "sqlite3" 13 | 14 | # Creating a cursor creates the database file if it doesn't exist. 15 | connection.cursor() 16 | -------------------------------------------------------------------------------- /engine/grpcio-1.64.1-cp312-cp312-linux_aarch64.whl: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:80f9c57bf1b12d2a5e4a084b9bc61a3f7a290a74cd36d73b7782032c14f5d775 3 | size 112987178 4 | -------------------------------------------------------------------------------- /engine/requirements-dev.in: -------------------------------------------------------------------------------- 1 | -c requirements.txt 2 | celery-types==0.18.0 3 | django-filter-stubs==0.1.3 4 | django-stubs[compatible-mypy]==4.2.2 5 | djangorestframework-stubs[compatible-mypy]==3.14.2 6 | httpretty==1.1.4 7 | mypy==1.4.1 8 | pre-commit==2.15.0 9 | pytest==8.2.2 10 | pytest-django==4.8.0 11 | pytest-socket==0.7.0 12 | pytest-xdist[psutil]==3.6.1 13 | pytest_factoryboy==2.7.0 14 | types-beautifulsoup4==4.12.0.5 15 | types-PyMySQL==1.0.19.7 16 | types-python-dateutil==2.8.19.13 17 | types-requests==2.31.0.1 18 | -------------------------------------------------------------------------------- /engine/settings/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/engine/settings/__init__.py -------------------------------------------------------------------------------- /engine/settings/helm.py: -------------------------------------------------------------------------------- 1 | from .prod_without_db import * # noqa: F401, F403 2 | 3 | APPEND_SLASH = False 4 | SECURE_SSL_REDIRECT = False 5 | -------------------------------------------------------------------------------- /engine/settings/hobby.py: -------------------------------------------------------------------------------- 1 | from .prod_without_db import * # noqa: F403 2 | 3 | MIRAGE_SECRET_KEY = SECRET_KEY # noqa: F405 4 | MIRAGE_CIPHER_IV = "1234567890abcdef" # use default 5 | 6 | APPEND_SLASH = False 7 | SECURE_SSL_REDIRECT = False 8 | -------------------------------------------------------------------------------- /engine/static/images/resolution_note.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/engine/static/images/resolution_note.gif -------------------------------------------------------------------------------- /engine/type_stubs/icalendar/cli.pyi: -------------------------------------------------------------------------------- 1 | from . import Calendar as Calendar 2 | 3 | def view(input_handle, output_handle) -> None: ... 4 | def main() -> None: ... 5 | -------------------------------------------------------------------------------- /engine/type_stubs/icalendar/compat.pyi: -------------------------------------------------------------------------------- 1 | from _typeshed import Incomplete 2 | 3 | unicode_type = str 4 | bytes_type = bytes 5 | iteritems: Incomplete 6 | -------------------------------------------------------------------------------- /engine/type_stubs/icalendar/parser_tools.pyi: -------------------------------------------------------------------------------- 1 | from _typeshed import Incomplete 2 | from icalendar import compat as compat 3 | 4 | SEQUENCE_TYPES: Incomplete 5 | DEFAULT_ENCODING: str 6 | 7 | def to_unicode(value, encoding: str = ...): ... 8 | def data_encode(data, encoding=...): ... 9 | -------------------------------------------------------------------------------- /engine/type_stubs/icalendar/timezone_cache.pyi: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/engine/type_stubs/icalendar/timezone_cache.pyi -------------------------------------------------------------------------------- /engine/type_stubs/icalendar/tools.pyi: -------------------------------------------------------------------------------- 1 | from _typeshed import Incomplete 2 | from icalendar.parser_tools import to_unicode as to_unicode 3 | from icalendar.prop import vDatetime as vDatetime 4 | from icalendar.prop import vText as vText 5 | 6 | class UIDGenerator: 7 | chars: Incomplete 8 | def rnd_string(self, length: int = ...): ... 9 | def uid(self, host_name: str = ..., unique: str = ...): ... 10 | -------------------------------------------------------------------------------- /engine/type_stubs/icalendar/windows_to_olson.pyi: -------------------------------------------------------------------------------- 1 | from _typeshed import Incomplete 2 | 3 | WINDOWS_TO_OLSON: Incomplete 4 | -------------------------------------------------------------------------------- /engine/wait_for_test_mysql_start.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | until nc -z -v -w30 "${DATABASE_HOST:=mysql_test}" 3306 4 | do 5 | echo "Waiting for database connection..." 6 | sleep 1 7 | done 8 | -------------------------------------------------------------------------------- /grafana-plugin/.bra.toml: -------------------------------------------------------------------------------- 1 | # default configuration created by the `mage watch` command. 2 | # this file can be edited and should be checked into source control. 3 | # see https://github.com/unknwon/bra/blob/master/templates/default.bra.toml for more configuration options. 4 | [run] 5 | init_cmds = [ 6 | ["mage", "-v", "build:debug"], 7 | ] 8 | watch_all = true 9 | follow_symlinks = false 10 | ignore = [".git", "node_modules", "dist"] 11 | ignore_files = ["mage_output_file.go"] 12 | watch_dirs = ["pkg"] 13 | watch_exts = [".go", ".json"] 14 | build_delay = 2000 15 | cmds = [ 16 | ["mage", "-v", "build:debug"], 17 | ] 18 | -------------------------------------------------------------------------------- /grafana-plugin/.config/.prettierrc.js: -------------------------------------------------------------------------------- 1 | /* 2 | * ⚠️⚠️⚠️ THIS FILE WAS SCAFFOLDED BY `@grafana/create-plugin`. DO NOT EDIT THIS FILE DIRECTLY. ⚠️⚠️⚠️ 3 | * 4 | * In order to extend the configuration follow the steps in .config/README.md 5 | */ 6 | 7 | module.exports = { 8 | endOfLine: 'auto', 9 | printWidth: 120, 10 | trailingComma: 'es5', 11 | semi: true, 12 | jsxSingleQuote: false, 13 | singleQuote: true, 14 | useTabs: false, 15 | tabWidth: 2, 16 | }; 17 | -------------------------------------------------------------------------------- /grafana-plugin/.config/webpack/constants.ts: -------------------------------------------------------------------------------- 1 | export const SOURCE_DIR = 'src'; 2 | export const DIST_DIR = 'dist'; 3 | -------------------------------------------------------------------------------- /grafana-plugin/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | playwright-report 4 | -------------------------------------------------------------------------------- /grafana-plugin/.eslintignore: -------------------------------------------------------------------------------- 1 | /src/assets 2 | /src/network/oncall-api/autogenerated-api.types.d.ts -------------------------------------------------------------------------------- /grafana-plugin/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | .eslintcache 3 | 4 | # testing 5 | /coverage 6 | 7 | # production 8 | /dist 9 | 10 | npm-debug.log* 11 | pnpm-debug.log* 12 | pnpm-error.log* 13 | 14 | # This file is generated 15 | grafana-plugin.yml 16 | 17 | # playwright 18 | /playwright-report* 19 | /playwright/.cache/ 20 | /e2e-tests/storageState.json 21 | 22 | # Jest test report 23 | jest_html_reporters.html 24 | jest-html-reporters* 25 | 26 | .turbo 27 | tsconfig.tsbuildinfo -------------------------------------------------------------------------------- /grafana-plugin/.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // Prettier configuration provided by Grafana scaffolding 3 | ...require('./.config/.prettierrc.js'), 4 | }; 5 | -------------------------------------------------------------------------------- /grafana-plugin/.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["stylelint-config-standard", "stylelint-prettier/recommended"], 3 | "plugins": ["stylelint-prettier"], 4 | "rules": { 5 | "block-no-empty": [true, { "severity": "warning" }], 6 | "selector-max-type": [ 0, { 7 | "severity": "error", 8 | "ignore": ["child", "compounded", "descendant", "next-sibling"] 9 | }], 10 | "selector-pseudo-class-no-unknown": [ 11 | true, 12 | { 13 | "ignorePseudoClasses": ["global"] 14 | } 15 | ], 16 | "prettier/prettier": true 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /grafana-plugin/Dockerfile.dev: -------------------------------------------------------------------------------- 1 | FROM node:20.15.1-alpine 2 | 3 | WORKDIR /etc/app 4 | ENV PATH /etc/app/node_modules/.bin:$PATH 5 | 6 | CMD ["pnpm", "start"] -------------------------------------------------------------------------------- /grafana-plugin/Magefile.go: -------------------------------------------------------------------------------- 1 | //go:build mage 2 | // +build mage 3 | 4 | package main 5 | 6 | import ( 7 | // mage:import 8 | build "github.com/grafana/grafana-plugin-sdk-go/build" 9 | ) 10 | 11 | // Default configures the default target. 12 | var Default = build.BuildAll -------------------------------------------------------------------------------- /grafana-plugin/README.md: -------------------------------------------------------------------------------- 1 | # Grafana OnCall 2 | 3 | Developer-Friendly 4 | Alert Management 5 | with Brilliant Slack Integration 6 | 7 | - Connect monitoring systems 8 | - Collect and analyze data 9 | - On-call rotation 10 | - Automatic escalation 11 | - Never miss alerts with calls and SMS 12 | 13 | ## Documentation 14 | 15 | - [On Github](http://github.com/grafana/oncall) 16 | - [Grafana OnCall](https://grafana.com/docs/oncall/latest/) 17 | -------------------------------------------------------------------------------- /grafana-plugin/e2e-tests/.auth/.gitignore: -------------------------------------------------------------------------------- 1 | *.json 2 | -------------------------------------------------------------------------------- /grafana-plugin/e2e-tests/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "rulesdir/no-relative-import-paths": "off", 4 | "no-console": "off" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /grafana-plugin/e2e-tests/settings/tabs.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '../fixtures'; 2 | import { goToOnCallPage } from '../utils/navigation'; 3 | 4 | test(`tab query param is used to show proper page tab`, async ({ adminRolePage }) => { 5 | const { page } = adminRolePage; 6 | goToOnCallPage(page, `settings`, { tab: 'ChatOps' }); 7 | 8 | const tab = page.getByRole('tab', { name: 'Chat Ops' }); 9 | const isSelected = await tab.getAttribute('aria-selected'); 10 | 11 | expect(isSelected).toBe('true'); 12 | }); 13 | -------------------------------------------------------------------------------- /grafana-plugin/knip.json: -------------------------------------------------------------------------------- 1 | { 2 | "entry": ["src/PluginPage.tsx", "src/module.ts"], 3 | "project": ["**/*.{js,ts,jsx,tsx}"] 4 | } 5 | -------------------------------------------------------------------------------- /grafana-plugin/pkg/plugin/errors.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | const ( 4 | INSTALL_ERROR_CODE = 1000 5 | ) 6 | 7 | type OnCallError struct { 8 | Code int `json:"code"` 9 | Message string `json:"message"` 10 | Fields map[string][]string `json:"fields,omitempty"` 11 | } 12 | -------------------------------------------------------------------------------- /grafana-plugin/src/assets/img/HeartBeatMonitoring.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/grafana-plugin/src/assets/img/HeartBeatMonitoring.png -------------------------------------------------------------------------------- /grafana-plugin/src/assets/img/PagerDuty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/grafana-plugin/src/assets/img/PagerDuty.png -------------------------------------------------------------------------------- /grafana-plugin/src/assets/img/ServiceNow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/grafana-plugin/src/assets/img/ServiceNow.png -------------------------------------------------------------------------------- /grafana-plugin/src/assets/img/github_star.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /grafana-plugin/src/assets/img/inbound-email.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/grafana-plugin/src/assets/img/inbound-email.png -------------------------------------------------------------------------------- /grafana-plugin/src/assets/img/integration-logos.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/grafana-plugin/src/assets/img/integration-logos.png -------------------------------------------------------------------------------- /grafana-plugin/src/assets/img/qr-code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/grafana-plugin/src/assets/img/qr-code.png -------------------------------------------------------------------------------- /grafana-plugin/src/assets/img/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/grafana-plugin/src/assets/img/screenshot.png -------------------------------------------------------------------------------- /grafana-plugin/src/assets/img/slack_instructions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/grafana-plugin/src/assets/img/slack_instructions.png -------------------------------------------------------------------------------- /grafana-plugin/src/components/RenderConditionally/RenderConditionally.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, ReactNode } from 'react'; 2 | 3 | interface RenderConditionallyProps { 4 | shouldRender?: boolean; 5 | children?: ReactNode; 6 | render?: () => ReactNode; 7 | backupChildren?: ReactNode; 8 | } 9 | 10 | export const RenderConditionally: FC = ({ 11 | shouldRender, 12 | children, 13 | render, 14 | backupChildren = null, 15 | }) => { 16 | if (render) { 17 | return shouldRender ? <>{render()} : <>{backupChildren}; 18 | } 19 | 20 | return shouldRender ? <>{children} : <>{backupChildren}; 21 | }; 22 | -------------------------------------------------------------------------------- /grafana-plugin/src/components/ScheduleFilters/ScheduleFilters.types.ts: -------------------------------------------------------------------------------- 1 | import { ApiSchemas } from 'network/oncall-api/api.types'; 2 | 3 | export interface ScheduleFiltersType { 4 | users: Array; 5 | } 6 | -------------------------------------------------------------------------------- /grafana-plugin/src/components/ScheduleQuality/ScheduleQuality.styles.ts: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/css'; 2 | import { GrafanaTheme2 } from '@grafana/data'; 3 | 4 | export const getScheduleQualityStyles = (_theme: GrafanaTheme2) => { 5 | return { 6 | root: css` 7 | display: flex; 8 | flex-direction: row; 9 | align-items: center; 10 | gap: 5px; 11 | `, 12 | 13 | quality: css` 14 | line-height: 16px; 15 | `, 16 | 17 | tag: css` 18 | font-size: 12px; 19 | padding: 5px 10px; 20 | `, 21 | }; 22 | }; 23 | -------------------------------------------------------------------------------- /grafana-plugin/src/components/SortableList/SortableList.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { SortableContainer, SortableContainerProps } from 'react-sortable-hoc'; 4 | 5 | import { Timeline } from 'components/Timeline/Timeline'; 6 | 7 | export const SortableList = SortableContainer< 8 | SortableContainerProps & { className?: string; children: React.ReactNode[] } 9 | >(({ className, children }: any) => { 10 | return {children}; 11 | }); 12 | -------------------------------------------------------------------------------- /grafana-plugin/src/components/Tutorial/Tutorial.types.ts: -------------------------------------------------------------------------------- 1 | export enum TutorialStep { 2 | Integrations, 3 | Escalations, 4 | Slack, 5 | Schedules, 6 | Incidents, 7 | } 8 | -------------------------------------------------------------------------------- /grafana-plugin/src/components/UserGroups/UserGroups.types.ts: -------------------------------------------------------------------------------- 1 | export interface Item { 2 | key: string; 3 | type: string; 4 | data: any; 5 | } 6 | -------------------------------------------------------------------------------- /grafana-plugin/src/containers/AddResponders/AddResponders.helpers.ts: -------------------------------------------------------------------------------- 1 | import { FormData as ManualAlertGroupFormData } from 'components/ManualAlertGroup/ManualAlertGroup'; 2 | import { GrafanaTeam } from 'models/grafana_team/grafana_team.types'; 3 | 4 | import { UserResponders } from './AddResponders.types'; 5 | 6 | export const prepareForUpdate = ( 7 | selectedUsers: UserResponders, 8 | selectedTeam?: GrafanaTeam, 9 | data?: ManualAlertGroupFormData 10 | ) => ({ 11 | ...(data || {}), 12 | team: selectedTeam ? selectedTeam.id : null, 13 | users: selectedUsers.map(({ important, data: { pk } }) => ({ important, id: pk })), 14 | }); 15 | -------------------------------------------------------------------------------- /grafana-plugin/src/containers/ApiTokenSettings/ApiTokenForm.styles.ts: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/css'; 2 | 3 | export const getApiTokenFormStyles = () => { 4 | return { 5 | tokenInputContainer: css` 6 | width: 100%; 7 | display: flex; 8 | `, 9 | tokenInput: css` 10 | border-top-right-radius: 0; 11 | border-bottom-right-radius: 0; 12 | `, 13 | tokenCopyButton: css` 14 | border-top-left-radius: 0; 15 | border-bottom-left-radius: 0; 16 | `, 17 | field: css` 18 | flex-grow: 1; 19 | `, 20 | }; 21 | }; 22 | -------------------------------------------------------------------------------- /grafana-plugin/src/containers/DefaultPageLayout/DefaultPageLayout.types.ts: -------------------------------------------------------------------------------- 1 | export enum SlackError { 2 | WRONG_WORKSPACE = 'wrong_workspace', 3 | USER_ALREADY_CONNECTED = 'user_already_connected', 4 | AUTH_FAILED = 'auth_failed', 5 | REGION_ERROR = 'region_error', 6 | } 7 | -------------------------------------------------------------------------------- /grafana-plugin/src/containers/DefaultPageLayout/helper.ts: -------------------------------------------------------------------------------- 1 | import { ApiSchemas } from 'network/oncall-api/api.types'; 2 | 3 | export const getIfChatOpsConnected = (user: ApiSchemas['User']) => { 4 | return user?.slack_user_identity || user?.telegram_configuration || user?.messaging_backends?.MSTEAMS; 5 | }; 6 | -------------------------------------------------------------------------------- /grafana-plugin/src/containers/IncidentsFilters/IncidentFilters.types.ts: -------------------------------------------------------------------------------- 1 | import { SelectOption } from 'state/types'; 2 | 3 | export interface IncidentsFiltersType {} 4 | 5 | export interface FilterOption { 6 | name: string; 7 | type: 'search' | 'options' | 'boolean' | 'daterange' | 'team_select'; 8 | href?: string; 9 | options?: SelectOption[]; 10 | default?: { value: string }; 11 | } 12 | -------------------------------------------------------------------------------- /grafana-plugin/src/containers/IntegrationForm/IntegrationForm.helpers.ts: -------------------------------------------------------------------------------- 1 | import { ApiSchemas } from 'network/oncall-api/api.types'; 2 | 3 | export function prepareForEdit(item: ApiSchemas['AlertReceiveChannel']): Partial { 4 | return { 5 | verbal_name: item.verbal_name, 6 | description_short: item.description_short, 7 | team: item.team, 8 | labels: item.labels, 9 | integration: item.integration, 10 | 11 | additional_settings: { 12 | ...item.additional_settings, 13 | }, 14 | }; 15 | } 16 | -------------------------------------------------------------------------------- /grafana-plugin/src/containers/MobileAppConnection/MobileAppConnectionTab.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { observer } from 'mobx-react'; 4 | 5 | import { Text } from 'components/Text/Text'; 6 | 7 | const MobileAppConnectionTab: React.FC<{ userPk?: string }> = observer(() => { 8 | return ( 9 | 10 | Mobile settings have been moved to{' '} 11 | 12 | user's profile 13 | 14 | 15 | ); 16 | }); 17 | 18 | export { MobileAppConnectionTab }; 19 | -------------------------------------------------------------------------------- /grafana-plugin/src/containers/MobileAppConnection/parts/QRCode/QRCode.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | 3 | import { QRCodeSVG } from 'qrcode.react'; 4 | 5 | import { Block } from 'components/GBlock/Block'; 6 | 7 | type Props = { 8 | value: string; 9 | className?: string; 10 | }; 11 | 12 | export const QRCode: FC = (props: Props) => { 13 | const { value, className = '' } = props; 14 | 15 | return ( 16 | 17 | 18 | 19 | ); 20 | }; 21 | -------------------------------------------------------------------------------- /grafana-plugin/src/containers/OutgoingWebhookForm/CommonWebhookPresetIcons.config.tsx: -------------------------------------------------------------------------------- 1 | import { ReactElement } from 'react'; 2 | 3 | export const commonWebhookPresetIconsConfig: { [id: string]: () => ReactElement } = {}; 4 | -------------------------------------------------------------------------------- /grafana-plugin/src/containers/OutgoingWebhookForm/OutgoingWebhookForm.styles.ts: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/css'; 2 | import { GrafanaTheme2 } from '@grafana/data'; 3 | 4 | export const getStyles = (_theme: GrafanaTheme2) => ({ 5 | formRow: css` 6 | display: flex; 7 | flex-wrap: nowrap; 8 | gap: 4px; 9 | `, 10 | formField: css` 11 | flex-grow: 1; 12 | `, 13 | }); 14 | -------------------------------------------------------------------------------- /grafana-plugin/src/containers/PersonalNotificationSettings/PersonalNotificationSettings.helpers.ts: -------------------------------------------------------------------------------- 1 | export function getColor(index: number) { 2 | return ['#299C46', '#3274D9', '#9933CC', '#EB7B18'][index] || '#EB7B18'; 3 | } 4 | -------------------------------------------------------------------------------- /grafana-plugin/src/containers/PersonalNotificationSettings/img/default-step.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/grafana-plugin/src/containers/PersonalNotificationSettings/img/default-step.png -------------------------------------------------------------------------------- /grafana-plugin/src/containers/RemoteFilters/RemoteFilters.types.ts: -------------------------------------------------------------------------------- 1 | import { SelectOption } from 'state/types'; 2 | 3 | export interface RemoteFiltersType {} 4 | 5 | export interface FilterOption { 6 | name: string; 7 | display_name?: string; 8 | type: 'search' | 'options' | 'boolean' | 'daterange' | 'team_select' | 'labels' | 'alert_group_labels'; 9 | href?: string; 10 | options?: SelectOption[]; 11 | default?: { value: string }; 12 | global?: boolean; 13 | description?: string; 14 | } 15 | -------------------------------------------------------------------------------- /grafana-plugin/src/containers/RotationForm/RotationForm.types.ts: -------------------------------------------------------------------------------- 1 | export interface RotationCreateData {} 2 | 3 | export interface RotationData {} 4 | 5 | export enum RepeatEveryPeriod { 6 | 'DAYS' = 0, 7 | 'WEEKS' = 1, 8 | 'MONTHS' = 2, 9 | 'HOURS' = 3, 10 | 'MINUTES' = 4, 11 | } 12 | -------------------------------------------------------------------------------- /grafana-plugin/src/containers/Rotations/Animations.styles.ts: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/css'; 2 | 3 | export const getAnimationClasses = () => { 4 | return { 5 | enter: css` 6 | opacity: 0; 7 | `, 8 | enterActive: css` 9 | opacity: 1; 10 | transition: opacity 500ms ease-in; 11 | `, 12 | exit: css` 13 | opacity: 1; 14 | `, 15 | exitActive: css` 16 | opacity: 0; 17 | transition: opacity 500ms ease-in; 18 | `, 19 | }; 20 | }; 21 | -------------------------------------------------------------------------------- /grafana-plugin/src/containers/Rotations/Rotations.config.ts: -------------------------------------------------------------------------------- 1 | export const DEFAULT_TRANSITION_TIMEOUT = { 2 | enter: 500, 3 | exit: 0, 4 | }; 5 | -------------------------------------------------------------------------------- /grafana-plugin/src/containers/UserSettings/UserSettings.types.ts: -------------------------------------------------------------------------------- 1 | export enum UserSettingsTab { 2 | UserInfo, 3 | NotificationSettings, 4 | GoogleCalendar, 5 | PhoneVerification, 6 | SlackInfo, 7 | TelegramInfo, 8 | PersonalWebhookInfo, 9 | MSTeamsInfo, 10 | MobileAppConnection, 11 | MattermostInfo, 12 | } 13 | -------------------------------------------------------------------------------- /grafana-plugin/src/containers/UsersTimezones/UsersTimezones.helpers.ts: -------------------------------------------------------------------------------- 1 | import { Dayjs } from 'dayjs'; 2 | 3 | export const calculateTimePassedInDayPercentage = (date: Dayjs) => { 4 | const midnight = date.startOf('day'); 5 | return (date.diff(midnight, 'minutes') / (60 * 24)) * 100; 6 | }; 7 | -------------------------------------------------------------------------------- /grafana-plugin/src/helpers/async.ts: -------------------------------------------------------------------------------- 1 | import { AttemptContext, retry } from '@lifeomic/attempt'; 2 | 3 | export const retryFailingPromises = async ( 4 | asyncActions: Array<(ctx?: AttemptContext) => Promise>, 5 | { maxAttempts = 3, delayInMs = 500 }: { maxAttempts?: number; delayInMs?: number } = {} 6 | ) => 7 | maxAttempts === 0 8 | ? Promise.allSettled(asyncActions) 9 | : Promise.allSettled(asyncActions.map((asyncAction) => retry(asyncAction, { maxAttempts, delay: delayInMs }))); 10 | 11 | export const waitInMs = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); 12 | -------------------------------------------------------------------------------- /grafana-plugin/src/helpers/loadJs.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Will append a new JS script 3 | * @param {string} url of the script 4 | * @param {string} id optional id. If specified, the script will be loaded only once for that given id 5 | */ 6 | export const loadJs = function (url: string, id: string = undefined) { 7 | if (id) { 8 | const existingScript = document.getElementById(url); 9 | if (existingScript) { 10 | return; 11 | } 12 | } 13 | 14 | let script = document.createElement('script'); 15 | script.src = url; 16 | 17 | if (id) { 18 | // optional 19 | script.id = id; 20 | } 21 | 22 | document.head.appendChild(script); 23 | }; 24 | -------------------------------------------------------------------------------- /grafana-plugin/src/helpers/sanitize.ts: -------------------------------------------------------------------------------- 1 | import dompurify from 'dompurify'; 2 | 3 | export const sanitize = function (str: string): string { 4 | return dompurify.sanitize(str, { 5 | USE_PROFILES: { html: true }, 6 | FORBID_TAGS: ['form', 'input'], 7 | ADD_ATTR: ['target'], 8 | }); 9 | }; 10 | -------------------------------------------------------------------------------- /grafana-plugin/src/helpers/styles.ts: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/css'; 2 | 3 | export const getCommonStyles = () => ({ 4 | bottomDrawerButtons: css({ 5 | padding: '16px 0', 6 | width: '100%', 7 | }), 8 | }); 9 | -------------------------------------------------------------------------------- /grafana-plugin/src/jest/grafanaMock.ts: -------------------------------------------------------------------------------- 1 | export default {}; 2 | -------------------------------------------------------------------------------- /grafana-plugin/src/jest/matchMedia.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | export default global.matchMedia = 3 | global.matchMedia || 4 | function (query) { 5 | return { 6 | matches: false, 7 | media: query, 8 | onchange: null, 9 | addListener: jest.fn(), 10 | removeListener: jest.fn(), 11 | addEventListener: jest.fn(), 12 | removeEventListener: jest.fn(), 13 | dispatchEvent: jest.fn(), 14 | }; 15 | }; 16 | -------------------------------------------------------------------------------- /grafana-plugin/src/jest/openapiFetchMock.ts: -------------------------------------------------------------------------------- 1 | export default () => ({}); 2 | -------------------------------------------------------------------------------- /grafana-plugin/src/jest/styleMock.ts: -------------------------------------------------------------------------------- 1 | export default {}; 2 | -------------------------------------------------------------------------------- /grafana-plugin/src/jest/svgTransform.ts: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | process() { 3 | return { code: 'module.exports = {};' }; 4 | }, 5 | getCacheKey() { 6 | return 'svgTransform'; 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /grafana-plugin/src/jest/utils.ts: -------------------------------------------------------------------------------- 1 | export function mockUseStore() { 2 | jest.mock('state/useStore', () => ({ 3 | useStore: () => ({ 4 | isUserActionAllowed: jest.fn().mockReturnValue(true), 5 | }), 6 | })); 7 | } 8 | -------------------------------------------------------------------------------- /grafana-plugin/src/models/api_token/api_token.types.ts: -------------------------------------------------------------------------------- 1 | export interface ApiToken { 2 | id: number; 3 | created_at: string; 4 | name: string; 5 | token: string; 6 | } 7 | -------------------------------------------------------------------------------- /grafana-plugin/src/models/channel/channel.ts: -------------------------------------------------------------------------------- 1 | export interface Channel { 2 | id: string; 3 | alert_groups_count: number; 4 | integration: number; 5 | integration_verbose: string; 6 | verbal_name: string; 7 | deleted: boolean; 8 | } 9 | -------------------------------------------------------------------------------- /grafana-plugin/src/models/cloud/cloud.types.ts: -------------------------------------------------------------------------------- 1 | export interface Cloud { 2 | id: string; 3 | username: string; 4 | email: string; 5 | cloud_data?: { 6 | status?: number; 7 | link?: string; 8 | }; 9 | } 10 | -------------------------------------------------------------------------------- /grafana-plugin/src/models/direct_paging/direct_paging.types.ts: -------------------------------------------------------------------------------- 1 | export type ManualAlertGroupPayload = { 2 | team: string | null; 3 | users: Array<{ id: string; important: boolean }>; 4 | }; 5 | -------------------------------------------------------------------------------- /grafana-plugin/src/models/escalation_chain/escalation_chain.types.ts: -------------------------------------------------------------------------------- 1 | import { GrafanaTeam } from 'models/grafana_team/grafana_team.types'; 2 | export interface EscalationChain { 3 | id: string; 4 | pk: string; //? because GET related_escalation_chains returns {name,pk}[] 5 | is_default: boolean; 6 | name: string; 7 | number_of_integrations: number; 8 | number_of_routes: number; 9 | team: GrafanaTeam['id']; 10 | } 11 | 12 | export interface EscalationChainDetails { 13 | id: string; 14 | display_name: string; 15 | channel_filters: Array<{ 16 | id: string; 17 | display_name: string; 18 | }>; 19 | team: string; 20 | } 21 | -------------------------------------------------------------------------------- /grafana-plugin/src/models/escalation_policy/escalation_policy.helpers.ts: -------------------------------------------------------------------------------- 1 | import { pick } from 'lodash-es'; 2 | 3 | import { EscalationPolicy } from './escalation_policy.types'; 4 | 5 | export function prepareEscalationPolicy(value: EscalationPolicy) { 6 | return pick(value, ['step', 'important']); 7 | } 8 | -------------------------------------------------------------------------------- /grafana-plugin/src/models/global_setting/global_setting.types.ts: -------------------------------------------------------------------------------- 1 | export interface GlobalSetting { 2 | id: string; 3 | name: string; 4 | description: string; 5 | default_value: any; 6 | value: any; 7 | is_secret: boolean; 8 | error: string; 9 | } 10 | -------------------------------------------------------------------------------- /grafana-plugin/src/models/grafana_team/grafana_team.types.ts: -------------------------------------------------------------------------------- 1 | export interface GrafanaTeam { 2 | id: string; 3 | name: string; 4 | email: string; 5 | avatar_url: string; 6 | is_sharing_resources_to_all: boolean; 7 | number_of_users_currently_oncall?: number; 8 | } 9 | -------------------------------------------------------------------------------- /grafana-plugin/src/models/heartbeat/heartbeat.types.ts: -------------------------------------------------------------------------------- 1 | import { ApiSchemas } from 'network/oncall-api/api.types'; 2 | 3 | export interface Heartbeat { 4 | id: string; 5 | last_heartbeat_time_verbal: string; 6 | alert_receive_channel: ApiSchemas['AlertReceiveChannel']['id']; 7 | link: string; 8 | timeout_seconds: number; 9 | status: boolean; 10 | instruction: string; 11 | } 12 | -------------------------------------------------------------------------------- /grafana-plugin/src/models/label/label.types.ts: -------------------------------------------------------------------------------- 1 | import { ApiSchemas } from 'network/oncall-api/api.types'; 2 | 3 | export interface LabelKeyValue { 4 | key: ApiSchemas['LabelKey']; 5 | value: ApiSchemas['LabelValue']; 6 | } 7 | 8 | export type LabelsErrors = Array<{ key?: { id: string[]; name: string[] }; value?: { id: string[]; name: string[] } }>; 9 | -------------------------------------------------------------------------------- /grafana-plugin/src/models/loader/loader.helpers.ts: -------------------------------------------------------------------------------- 1 | import { LoaderStore } from './loader'; 2 | 3 | export class LoaderHelper { 4 | static isLoading(store: typeof LoaderStore, actionKey: string | string[]) { 5 | return typeof actionKey === 'string' ? store.items[actionKey] : actionKey.some((key) => store.items[key]); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /grafana-plugin/src/models/mattermost/mattermost.types.ts: -------------------------------------------------------------------------------- 1 | export interface MattermostChannel { 2 | id: string; 3 | channel_id: string; 4 | channel_name: string; 5 | display_name: string; 6 | is_default_channel: false; 7 | } 8 | -------------------------------------------------------------------------------- /grafana-plugin/src/models/msteams_channel/msteams_channel.types.ts: -------------------------------------------------------------------------------- 1 | // TODO check backend response to verify data shape 2 | 3 | export interface MSTeamsChannel { 4 | id: string; 5 | name: string; 6 | display_name: string; 7 | team: string; 8 | is_default: false; 9 | } 10 | -------------------------------------------------------------------------------- /grafana-plugin/src/models/notification_policy/notification_policy.ts: -------------------------------------------------------------------------------- 1 | import { ApiSchemas } from 'network/oncall-api/api.types'; 2 | 3 | export interface NotificationPolicyType { 4 | id: string; 5 | step: number; 6 | notify_by: number | null; 7 | wait_delay: string | null; 8 | important: boolean; 9 | user: ApiSchemas['User']['pk']; 10 | } 11 | 12 | export function prepareNotificationPolicy(value: NotificationPolicyType): NotificationPolicyType { 13 | return { 14 | ...value, 15 | notify_by: null, 16 | wait_delay: null, 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /grafana-plugin/src/models/plugin/plugin.helper.ts: -------------------------------------------------------------------------------- 1 | import { makeRequest } from 'network/network'; 2 | 3 | export class PluginHelper { 4 | static async install() { 5 | return makeRequest(`/plugin/install`, { 6 | method: 'POST', 7 | }); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /grafana-plugin/src/models/slack/slack.types.ts: -------------------------------------------------------------------------------- 1 | export interface SlackSettings { 2 | pk: string; 3 | acknowledge_remind_timeout: number; 4 | unacknowledge_timeout: number; 5 | } 6 | -------------------------------------------------------------------------------- /grafana-plugin/src/models/slack_channel/slack_channel.config.ts: -------------------------------------------------------------------------------- 1 | export const PRIVATE_CHANNEL_NAME = '🔒 private slack channel'; 2 | -------------------------------------------------------------------------------- /grafana-plugin/src/models/slack_channel/slack_channel.helpers.ts: -------------------------------------------------------------------------------- 1 | import { PRIVATE_CHANNEL_NAME } from 'models/slack_channel/slack_channel.config'; 2 | import { SlackChannel } from 'models/slack_channel/slack_channel.types'; 3 | 4 | export const getSlackChannelName = (channel?: SlackChannel | null): string | undefined => { 5 | if (!channel) { 6 | return undefined; 7 | } 8 | if (!channel.display_name) { 9 | return PRIVATE_CHANNEL_NAME; 10 | } 11 | 12 | return channel.display_name; 13 | }; 14 | -------------------------------------------------------------------------------- /grafana-plugin/src/models/slack_channel/slack_channel.types.ts: -------------------------------------------------------------------------------- 1 | export interface SlackChannel { 2 | id: string; 3 | display_name: string; 4 | slack_id: string; 5 | } 6 | -------------------------------------------------------------------------------- /grafana-plugin/src/models/telegram_channel/telegram_channel.types.ts: -------------------------------------------------------------------------------- 1 | export interface TelegramChannel { 2 | id: string; 3 | channel_chat_id: string; 4 | channel_name: string; 5 | discussion_group_chat_id: string; 6 | discussion_group_name: string; 7 | is_default_channel: false; 8 | } 9 | 10 | export interface TelegramChannelDetails { 11 | display_name: string; 12 | id: string; 13 | } 14 | -------------------------------------------------------------------------------- /grafana-plugin/src/models/timezone/timezone.types.ts: -------------------------------------------------------------------------------- 1 | import { allTimezones } from './timezone.helpers'; 2 | 3 | export type Timezone = (typeof allTimezones)[number]; 4 | -------------------------------------------------------------------------------- /grafana-plugin/src/models/user_group/user_group.types.ts: -------------------------------------------------------------------------------- 1 | export interface UserGroup { 2 | id: string; 3 | name: string; 4 | handle: string; 5 | } 6 | -------------------------------------------------------------------------------- /grafana-plugin/src/navbar/LegacyNavHeading.tsx: -------------------------------------------------------------------------------- 1 | interface LegacyNavHeadingProps { 2 | children: JSX.Element; 3 | show?: boolean; 4 | } 5 | 6 | export const LegacyNavHeading = function (props: LegacyNavHeadingProps): JSX.Element { 7 | const { show = false, children } = props; 8 | return show ? children : null; 9 | }; 10 | -------------------------------------------------------------------------------- /grafana-plugin/src/network/oncall-api/types-generator/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module", 3 | "scripts": { 4 | "generate": "node --loader ts-node/esm ./generate-types.ts && prettier --write ../autogenerated-api.types.d.ts" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /grafana-plugin/src/network/oncall-api/types-generator/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "ESNext", 4 | "target": "ESNext", 5 | "moduleResolution": "node", 6 | "esModuleInterop": true 7 | }, 8 | "ts-node": { "esm": true } 9 | } 10 | -------------------------------------------------------------------------------- /grafana-plugin/src/pages/insights/Insights.types.ts: -------------------------------------------------------------------------------- 1 | import { DataSourceRef } from '@grafana/schema'; 2 | 3 | export interface InsightsConfig { 4 | isOpenSource: boolean; 5 | datasource: DataSourceRef; 6 | stack: string; 7 | } 8 | -------------------------------------------------------------------------------- /grafana-plugin/src/pages/outgoing_webhooks/OutgoingWebhooks.types.ts: -------------------------------------------------------------------------------- 1 | export enum WebhookFormActionType { 2 | NEW = 1, 3 | COPY = 2, 4 | VIEW_LAST_RUN = 3, 5 | EDIT_SETTINGS = 4, 6 | } 7 | -------------------------------------------------------------------------------- /grafana-plugin/src/pages/settings/SettingsPage.types.ts: -------------------------------------------------------------------------------- 1 | import { KeyValuePair } from 'helpers/helpers'; 2 | 3 | export const SettingsPageTab = { 4 | MainSettings: new KeyValuePair('MainSettings', 'Organization Settings'), 5 | TeamsSettings: new KeyValuePair('TeamsSettings', 'Teams and Access Settings'), 6 | ChatOps: new KeyValuePair('ChatOps', 'Chat Ops'), 7 | EnvVariables: new KeyValuePair('EnvVariables', 'Env Variables'), 8 | Cloud: new KeyValuePair('Cloud', 'Cloud'), 9 | }; 10 | -------------------------------------------------------------------------------- /grafana-plugin/src/pages/settings/tabs/ChatOps/ChatOps.helpers.ts: -------------------------------------------------------------------------------- 1 | import { LocationHelper } from 'helpers/LocationHelper'; 2 | import { openErrorNotification } from 'helpers/helpers'; 3 | 4 | export const handleChatOpsQueryParamError = () => { 5 | const error = LocationHelper.getQueryParam('error'); 6 | 7 | if (error) { 8 | openErrorNotification(error); 9 | LocationHelper.update({ error: undefined }, 'partial'); 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /grafana-plugin/src/pages/settings/tabs/LiveSettings/LiveSettings.config.ts: -------------------------------------------------------------------------------- 1 | export const PLACEHOLDER = '**********'; 2 | export const NULL_VALUE = '—'; 3 | -------------------------------------------------------------------------------- /grafana-plugin/src/pages/settings/tabs/LiveSettings/LiveSettings.helpers.ts: -------------------------------------------------------------------------------- 1 | import { NULL_VALUE } from './LiveSettings.config'; 2 | 3 | export function normalizeValue(value: string) { 4 | if (value === null || value === '') { 5 | return NULL_VALUE; 6 | } 7 | 8 | return String(value); 9 | } 10 | 11 | export function prepareForUpdate(value: string | boolean) { 12 | if (value === '') { 13 | return null; 14 | } 15 | 16 | return value; 17 | } 18 | -------------------------------------------------------------------------------- /grafana-plugin/src/plugin/GrafanaPluginRootPage.helpers.tsx: -------------------------------------------------------------------------------- 1 | 2 | export function getQueryParams(): any { 3 | const searchParams = new URLSearchParams(window.location.search); 4 | const result = {}; 5 | for (const [key, value] of searchParams) { 6 | if (result[key]) { 7 | // key already existing, we're handling an array 8 | if (!Array.isArray(result[key])) { 9 | result[key] = new Array(result[key]); 10 | } 11 | 12 | result[key].push(value); 13 | } else { 14 | result[key] = value; 15 | } 16 | } 17 | 18 | return result; 19 | } 20 | -------------------------------------------------------------------------------- /grafana-plugin/src/state/features.ts: -------------------------------------------------------------------------------- 1 | export enum AppFeature { 2 | Slack = 'slack', 3 | UnifiedSlack = 'unified_slack', 4 | Telegram = 'telegram', 5 | LiveSettings = 'live_settings', 6 | CloudNotifications = 'grafana_cloud_notifications', 7 | CloudConnection = 'grafana_cloud_connection', 8 | Labels = 'labels', 9 | MsTeams = 'msteams', 10 | GoogleOauth2 = 'google_oauth2', 11 | PersonalWebhook = 'personal_webhook', 12 | Mattermost = 'mattermost', 13 | } 14 | -------------------------------------------------------------------------------- /grafana-plugin/src/state/helpers.ts: -------------------------------------------------------------------------------- 1 | export function move(arr: any[], old_index: number, new_index: number) { 2 | while (old_index < 0) { 3 | old_index += arr.length; 4 | } 5 | while (new_index < 0) { 6 | new_index += arr.length; 7 | } 8 | if (new_index >= arr.length) { 9 | let k = new_index - arr.length; 10 | while (k-- + 1) { 11 | arr.push(undefined); 12 | } 13 | } 14 | arr.splice(new_index, 0, arr.splice(old_index, 1)[0]); 15 | return arr; 16 | } 17 | -------------------------------------------------------------------------------- /grafana-plugin/src/state/rootStore.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Important! 3 | Make sure import of plugin/dayjs is placed in a proper location 4 | Otherwise the dayjs extenders won't be called and the dayjs functionality will be altered, thus leading to all sort of bugs 5 | */ 6 | 7 | import 'plugin/dayjs'; 8 | 9 | import { RootBaseStore } from './rootBaseStore/RootBaseStore'; 10 | 11 | export class RootStore extends RootBaseStore {} 12 | 13 | export const rootStore = new RootStore(); 14 | -------------------------------------------------------------------------------- /grafana-plugin/src/state/types.ts: -------------------------------------------------------------------------------- 1 | import { AppPluginMeta, KeyValue } from '@grafana/data'; 2 | import { useDrawer } from 'helpers/hooks'; 3 | 4 | import { RootStore } from 'state/rootStore'; 5 | 6 | export interface WithStoreProps { 7 | store: RootStore; 8 | } 9 | 10 | export interface WithDrawerConfig { 11 | drawerConfig: ReturnType>; 12 | } 13 | 14 | export interface PageProps { 15 | meta: AppPluginMeta; 16 | query: KeyValue; 17 | } 18 | 19 | export interface SelectOption { 20 | value: string | number; 21 | display_name: string; 22 | } 23 | -------------------------------------------------------------------------------- /grafana-plugin/src/state/useStore.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { MobXProviderContext } from 'mobx-react'; 4 | 5 | import { RootStore } from './rootStore'; 6 | 7 | export function useStore(): RootStore { 8 | const { store } = React.useContext(MobXProviderContext); 9 | return store; 10 | } 11 | -------------------------------------------------------------------------------- /grafana-plugin/src/state/withStore.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { MobXProviderContext } from 'mobx-react'; 4 | 5 | export const withMobXProviderContext = (BaseComponent: any) => { 6 | const MobXProviderWrappedComponent = (props: any) => ( 7 | 8 | {(mobXProviderContext) => } 9 | 10 | ); 11 | 12 | return MobXProviderWrappedComponent; 13 | }; 14 | -------------------------------------------------------------------------------- /grafana-plugin/src/version.ts: -------------------------------------------------------------------------------- 1 | // NOTE: This file is replaced by the release script, so do not expect it to be as it is always. 2 | export const GIT_COMMIT = 'dev'; 3 | 4 | // Declare a constant that will be updated by release-please action 5 | export const CURRENT_VERSION = '1.9.28' as string; // x-release-please-version 6 | -------------------------------------------------------------------------------- /grafana-plugin/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.config/tsconfig.json", 3 | "include": ["src", "e2e-tests", "playwright.config.ts"], 4 | "types": ["node", "@emotion/core"], 5 | "compilerOptions": { 6 | "composite": true, 7 | "declaration": true, 8 | "rootDir": "", 9 | "typeRoots": ["./node_modules/@types"], 10 | "noUnusedLocals": false, 11 | "noUnusedParameters": false, 12 | "strict": false, 13 | "resolveJsonModule": true, 14 | "noImplicitAny": false, 15 | "skipLibCheck": true, 16 | "useDefineForClassFields": true 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /helm/cr.yaml: -------------------------------------------------------------------------------- 1 | git-repo: helm-charts 2 | owner: grafana 3 | skip-existing: true 4 | -------------------------------------------------------------------------------- /helm/ct.yaml: -------------------------------------------------------------------------------- 1 | # See https://github.com/helm/chart-testing#configuration 2 | remote: origin 3 | target-branch: main 4 | chart-dirs: 5 | - helm 6 | chart-repos: 7 | - jetstack=https://charts.jetstack.io 8 | - bitnami=https://charts.bitnami.com/bitnami 9 | - grafana=https://grafana.github.io/helm-charts 10 | - ingress-nginx=https://kubernetes.github.io/ingress-nginx 11 | helm-extra-args: --timeout 600s 12 | validate-maintainers: false 13 | -------------------------------------------------------------------------------- /helm/local_image.yml: -------------------------------------------------------------------------------- 1 | image: 2 | repository: oncall/engine 3 | tag: latest 4 | pullPolicy: IfNotPresent 5 | oncall: 6 | devMode: true 7 | -------------------------------------------------------------------------------- /helm/oncall/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *.orig 18 | *~ 19 | # Various IDEs 20 | .project 21 | .idea/ 22 | *.tmproj 23 | .vscode/ 24 | 25 | # exclude helm unit tests 26 | tests/ 27 | -------------------------------------------------------------------------------- /helm/oncall/charts/cert-manager-v1.8.0.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/helm/oncall/charts/cert-manager-v1.8.0.tgz -------------------------------------------------------------------------------- /helm/oncall/charts/grafana-8.4.6.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/helm/oncall/charts/grafana-8.4.6.tgz -------------------------------------------------------------------------------- /helm/oncall/charts/ingress-nginx-4.1.4.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/helm/oncall/charts/ingress-nginx-4.1.4.tgz -------------------------------------------------------------------------------- /helm/oncall/charts/mariadb-12.2.5.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/helm/oncall/charts/mariadb-12.2.5.tgz -------------------------------------------------------------------------------- /helm/oncall/charts/postgresql-11.9.10.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/helm/oncall/charts/postgresql-11.9.10.tgz -------------------------------------------------------------------------------- /helm/oncall/charts/prometheus-25.8.2.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/helm/oncall/charts/prometheus-25.8.2.tgz -------------------------------------------------------------------------------- /helm/oncall/charts/rabbitmq-12.0.0.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/helm/oncall/charts/rabbitmq-12.0.0.tgz -------------------------------------------------------------------------------- /helm/oncall/charts/redis-16.13.2.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/helm/oncall/charts/redis-16.13.2.tgz -------------------------------------------------------------------------------- /helm/oncall/templates/engine/service-internal.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ include "oncall.engine.fullname" . }} 5 | labels: 6 | {{- include "oncall.engine.labels" . | nindent 4 }} 7 | spec: 8 | type: ClusterIP 9 | ports: 10 | - port: 8080 11 | targetPort: http 12 | protocol: TCP 13 | name: http 14 | selector: 15 | {{- include "oncall.engine.selectorLabels" . | nindent 4 }} 16 | -------------------------------------------------------------------------------- /helm/oncall/templates/integrations/service-internal.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.detached_integrations.enabled -}} 2 | apiVersion: v1 3 | kind: Service 4 | metadata: 5 | name: {{ include "oncall.detached_integrations.fullname" . }} 6 | labels: 7 | {{- include "oncall.detached_integrations.labels" . | nindent 4 }} 8 | spec: 9 | type: ClusterIP 10 | ports: 11 | - port: 8080 12 | targetPort: http 13 | protocol: TCP 14 | name: http 15 | selector: 16 | {{- include "oncall.detached_integrations.selectorLabels" . | nindent 4 }} 17 | {{- end -}} 18 | -------------------------------------------------------------------------------- /helm/oncall/templates/plugin-provisioning.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: helm-testing-grafana-plugin-provisioning 5 | labels: 6 | app: {{ include "oncall.name" . }} 7 | data: 8 | grafana-oncall-app-provisioning.yaml: | 9 | apps: 10 | - type: grafana-oncall-app 11 | name: grafana-oncall-app 12 | disabled: false 13 | jsonData: 14 | stackId: 5 15 | orgId: 100 16 | onCallApiUrl: http://helm-testing-oncall-engine:8080 17 | -------------------------------------------------------------------------------- /helm/oncall/templates/serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.serviceAccount.create -}} 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: {{ include "oncall.serviceAccountName" . }} 6 | labels: 7 | {{- include "oncall.labels" . | nindent 4 }} 8 | {{- if or (.Values.migrate.useHook) (.Values.serviceAccount.annotations) }} 9 | annotations: 10 | {{- if .Values.migrate.useHook }} 11 | "helm.sh/hook": pre-install,pre-upgrade 12 | "helm.sh/hook-weight": "-5" 13 | {{- end }} 14 | {{- with .Values.serviceAccount.annotations }} 15 | {{- toYaml . | nindent 4 }} 16 | {{- end }} 17 | {{- end }} 18 | {{- end }} 19 | -------------------------------------------------------------------------------- /helm/oncall/templates/ui/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{- define "ui.env" -}} 2 | {{- if .Values.ui.env }} 3 | {{- range $key, $value := .Values.ui.env }} 4 | - name: {{ $key }} 5 | value: "{{ $value }}" 6 | {{- end -}} 7 | {{- end }} 8 | {{- end }} 9 | -------------------------------------------------------------------------------- /helm/oncall/tests/__snapshot__/dev_mode_db_migrate_job_volumes_test.yaml.snap: -------------------------------------------------------------------------------- 1 | does add a volume and volume mount when oncall.devMode is enabled: 2 | 1: | 3 | - mountPath: /etc/app 4 | name: dev-reloaded-engine 5 | 2: | 6 | - hostPath: 7 | path: /engine 8 | name: dev-reloaded-engine 9 | -------------------------------------------------------------------------------- /helm/oncall/tests/__snapshot__/nodeselector_deployments_test.yaml.snap: -------------------------------------------------------------------------------- 1 | nodeSelector -> should use custom nodeSelector: 2 | 1: | 3 | unittest: here 4 | 2: | 5 | unittest: here 6 | 3: | 7 | unittest: here 8 | -------------------------------------------------------------------------------- /helm/oncall/tests/__snapshot__/tolerations_deployments_test.yaml.snap: -------------------------------------------------------------------------------- 1 | tolerations -> should use custom tolerations: 2 | 1: | 3 | - effect: NoSchedule 4 | key: node-role.kubernetes.io/unittest 5 | operator: Exists 6 | 2: | 7 | - effect: NoSchedule 8 | key: node-role.kubernetes.io/unittest 9 | operator: Exists 10 | 3: | 11 | - effect: NoSchedule 12 | key: node-role.kubernetes.io/unittest 13 | operator: Exists 14 | -------------------------------------------------------------------------------- /helm/oncall/tests/__snapshot__/topology_deployments_test.yaml.snap: -------------------------------------------------------------------------------- 1 | topologySpreadConstraints -> should use custom topologySpreadConstraints: 2 | 1: | 3 | - labelSelector: 4 | matchLabels: 5 | app.kubernetes.io/component: engine 6 | maxSkew: 1 7 | topologyKey: topology.kubernetes.io/zone 8 | whenUnsatisfiable: DoNotSchedule 9 | 2: | 10 | - labelSelector: 11 | matchLabels: 12 | app.kubernetes.io/component: engine 13 | maxSkew: 1 14 | topologyKey: topology.kubernetes.io/zone 15 | whenUnsatisfiable: DoNotSchedule 16 | -------------------------------------------------------------------------------- /helm/oncall/tests/dev_mode_volumes_test.yaml: -------------------------------------------------------------------------------- 1 | suite: dev mode 2 | templates: 3 | - celery/deployment.yaml 4 | - engine/deployment.yaml 5 | release: 6 | name: oncall 7 | tests: 8 | - it: doesn't add a volume and volume mount when oncall.devMode is disabled 9 | set: 10 | oncall: 11 | devMode: false 12 | asserts: 13 | - notExists: 14 | path: spec.template.spec.containers[0].volumeMounts 15 | - notExists: 16 | path: spec.template.spec.initContainers[0].volumeMounts 17 | - notExists: 18 | path: spec.template.spec.volumes 19 | -------------------------------------------------------------------------------- /helm/simple.yml: -------------------------------------------------------------------------------- 1 | base_url: localhost:30001 2 | ingress: 3 | enabled: false 4 | ingress-nginx: 5 | enabled: false 6 | cert-manager: 7 | enabled: false 8 | service: 9 | enabled: true 10 | type: NodePort 11 | port: 8080 12 | nodePort: 30001 13 | grafana: 14 | service: 15 | type: NodePort 16 | nodePort: 30002 17 | detached_integrations: 18 | enabled: false 19 | detached_integrations_service: 20 | enabled: false 21 | type: NodePort 22 | port: 8080 23 | nodePort: 30003 24 | database: 25 | # can be either mysql or postgresql 26 | type: postgresql 27 | mariadb: 28 | enabled: false 29 | postgresql: 30 | enabled: true 31 | -------------------------------------------------------------------------------- /tools/migrators/.gitignore: -------------------------------------------------------------------------------- 1 | .session 2 | __pycache__/ 3 | *.pyc 4 | -------------------------------------------------------------------------------- /tools/migrators/.isort.cfg: -------------------------------------------------------------------------------- 1 | [settings] 2 | profile=black 3 | -------------------------------------------------------------------------------- /tools/migrators/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.12.10-alpine3.21 2 | 3 | ENV PYTHONUNBUFFERED=1 4 | WORKDIR /app 5 | 6 | COPY requirements.txt requirements.txt 7 | RUN python3 -m pip install -r requirements.txt 8 | 9 | COPY . . 10 | 11 | # Create data directory and generate session ID 12 | RUN mkdir -p /app/data && \ 13 | python3 -c "import uuid; open('/app/data/.session', 'w').write(str(uuid.uuid4()))" 14 | 15 | # Set session file location 16 | ENV SESSION_FILE=/app/data/.session 17 | 18 | CMD ["python3", "main.py"] 19 | -------------------------------------------------------------------------------- /tools/migrators/lib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/tools/migrators/lib/__init__.py -------------------------------------------------------------------------------- /tools/migrators/lib/common/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/tools/migrators/lib/common/__init__.py -------------------------------------------------------------------------------- /tools/migrators/lib/common/report.py: -------------------------------------------------------------------------------- 1 | TAB = " " * 4 2 | SUCCESS_SIGN = "✅" 3 | ERROR_SIGN = "❌" 4 | WARNING_SIGN = "⚠️" # TODO: warning sign does not renders properly 5 | 6 | 7 | def format_error_list(errors: list[str]) -> str: 8 | """Format a list of errors into a string with bullet points.""" 9 | if not errors: 10 | return "" 11 | return "\n".join(f"{TAB}- {error}" for error in errors) 12 | -------------------------------------------------------------------------------- /tools/migrators/lib/common/resources/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/tools/migrators/lib/common/resources/__init__.py -------------------------------------------------------------------------------- /tools/migrators/lib/common/resources/teams.py: -------------------------------------------------------------------------------- 1 | import typing 2 | 3 | 4 | class MatchTeam(typing.TypedDict): 5 | name: str 6 | oncall_team: typing.Optional[typing.Dict[str, typing.Any]] 7 | 8 | 9 | def match_team(team: MatchTeam, oncall_teams: typing.List[MatchTeam]) -> None: 10 | oncall_team = None 11 | for candidate_team in oncall_teams: 12 | if team["name"].lower() == candidate_team["name"].lower(): 13 | oncall_team = candidate_team 14 | break 15 | 16 | team["oncall_team"] = oncall_team 17 | -------------------------------------------------------------------------------- /tools/migrators/lib/common/resources/users.py: -------------------------------------------------------------------------------- 1 | import typing 2 | 3 | 4 | class MatchUser(typing.TypedDict): 5 | email: str 6 | oncall_user: typing.Optional[typing.Dict[str, typing.Any]] 7 | 8 | 9 | def match_user(user: MatchUser, oncall_users: typing.List[MatchUser]) -> None: 10 | oncall_user = None 11 | for candidate_user in oncall_users: 12 | if user["email"].lower() == candidate_user["email"].lower(): 13 | oncall_user = candidate_user 14 | break 15 | 16 | user["oncall_user"] = oncall_user 17 | -------------------------------------------------------------------------------- /tools/migrators/lib/constants.py: -------------------------------------------------------------------------------- 1 | ONCALL_SHIFT_WEB_SOURCE = 0 # alias for "web" 2 | -------------------------------------------------------------------------------- /tools/migrators/lib/grafana/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/tools/migrators/lib/grafana/__init__.py -------------------------------------------------------------------------------- /tools/migrators/lib/oncall/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/tools/migrators/lib/oncall/__init__.py -------------------------------------------------------------------------------- /tools/migrators/lib/opsgenie/resources/users.py: -------------------------------------------------------------------------------- 1 | from lib.opsgenie.config import OPSGENIE_FILTER_TEAM, OPSGENIE_FILTER_USERS 2 | 3 | 4 | def filter_users(users: list[dict]) -> list[dict]: 5 | """Apply filters to users.""" 6 | if OPSGENIE_FILTER_TEAM: 7 | filtered_users = [] 8 | for u in users: 9 | if any(t["id"] == OPSGENIE_FILTER_TEAM for t in u["teams"]): 10 | filtered_users.append(u) 11 | users = filtered_users 12 | 13 | if OPSGENIE_FILTER_USERS: 14 | users = [u for u in users if u["id"] in OPSGENIE_FILTER_USERS] 15 | 16 | return users 17 | -------------------------------------------------------------------------------- /tools/migrators/lib/pagerduty/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/tools/migrators/lib/pagerduty/__init__.py -------------------------------------------------------------------------------- /tools/migrators/lib/pagerduty/resources/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/tools/migrators/lib/pagerduty/resources/__init__.py -------------------------------------------------------------------------------- /tools/migrators/lib/splunk/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/tools/migrators/lib/splunk/__init__.py -------------------------------------------------------------------------------- /tools/migrators/lib/splunk/resources/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/tools/migrators/lib/splunk/resources/__init__.py -------------------------------------------------------------------------------- /tools/migrators/lib/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/tools/migrators/lib/tests/__init__.py -------------------------------------------------------------------------------- /tools/migrators/lib/tests/common/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/tools/migrators/lib/tests/common/__init__.py -------------------------------------------------------------------------------- /tools/migrators/lib/tests/common/resources/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/tools/migrators/lib/tests/common/resources/__init__.py -------------------------------------------------------------------------------- /tools/migrators/lib/tests/common/resources/test_user.py: -------------------------------------------------------------------------------- 1 | from lib.common.resources.users import match_user 2 | 3 | 4 | def test_match_user_email_case_insensitive(): 5 | pd_user = {"email": "test@test.com"} 6 | oncall_users = [{"email": "TEST@TEST.COM"}] 7 | 8 | match_user(pd_user, oncall_users) 9 | assert pd_user["oncall_user"] == oncall_users[0] 10 | -------------------------------------------------------------------------------- /tools/migrators/lib/tests/opsgenie/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/tools/migrators/lib/tests/opsgenie/__init__.py -------------------------------------------------------------------------------- /tools/migrators/lib/tests/opsgenie/resources/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/tools/migrators/lib/tests/opsgenie/resources/__init__.py -------------------------------------------------------------------------------- /tools/migrators/lib/tests/pagerduty/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/tools/migrators/lib/tests/pagerduty/__init__.py -------------------------------------------------------------------------------- /tools/migrators/lib/tests/pagerduty/resources/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/tools/migrators/lib/tests/pagerduty/resources/__init__.py -------------------------------------------------------------------------------- /tools/migrators/lib/tests/splunk/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/tools/migrators/lib/tests/splunk/__init__.py -------------------------------------------------------------------------------- /tools/migrators/lib/tests/splunk/resources/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/oncall/88ff96deb5563a1c3a8fa45982dee503fa906b51/tools/migrators/lib/tests/splunk/resources/__init__.py -------------------------------------------------------------------------------- /tools/migrators/main.py: -------------------------------------------------------------------------------- 1 | from lib.base_config import MIGRATING_FROM, OPSGENIE, PAGERDUTY, SPLUNK 2 | 3 | if __name__ == "__main__": 4 | if MIGRATING_FROM == PAGERDUTY: 5 | from lib.pagerduty.migrate import migrate 6 | 7 | migrate() 8 | elif MIGRATING_FROM == SPLUNK: 9 | from lib.splunk.migrate import migrate 10 | 11 | migrate() 12 | elif MIGRATING_FROM == OPSGENIE: 13 | from lib.opsgenie.migrate import migrate 14 | 15 | migrate() 16 | else: 17 | raise ValueError("Invalid MIGRATING_FROM value") 18 | -------------------------------------------------------------------------------- /tools/migrators/pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | env = 3 | D:PAGERDUTY_API_TOKEN=test 4 | D:ONCALL_API_TOKEN=test 5 | D:ONCALL_API_URL=test 6 | D:MIGRATING_FROM=pagerduty 7 | D:SPLUNK_API_ID=abcd 8 | D:SPLUNK_API_KEY=abcd 9 | D:OPSGENIE_API_KEY=abcd 10 | D:OPSGENIE_API_URL=test 11 | -------------------------------------------------------------------------------- /tools/migrators/requirements.in: -------------------------------------------------------------------------------- 1 | requests==2.32.3 2 | pdpyras==4.5.0 3 | pytest==8.2.2 4 | pytest-env==0.6.2 5 | kubernetes==29.0.0 6 | --------------------------------------------------------------------------------