update URLs constructed by the backend to support IRM plugin (#5137)
# What this PR does
Introduces a new class,
`apps.grafana_plugin.ui_url_builder.UIURLBuilder`, which is responsible
for... building UI URLs (😄). The class mainly does two things:
- it will decide if the URL should point to `grafana-oncall-app` or
`grafana-irm-app` based on the value of
`organization.is_grafana_irm_enabled` (**NOTE**: this value isn't yet
being set + defaults to `False`; logic for setting this value will be
done in a subsequent PR)
- Adds `enum`s, `OnCallPage` and `IncidentPage` to DRYify hardcoded UI
URLs (in case we decide to change these slightly in the near future)
## Checklist
- [x] Unit, integration, and e2e (if applicable) tests updated
- [x] Documentation added (or `pr:no public docs` PR label added if not
required)
- [x] Added the relevant release notes label (see labels prefixed w/
`release:`). These labels dictate how your PR will
show up in the autogenerated release notes.
This commit is contained in:
parent
2545bf8336
commit
bfcc0b9f29
30 changed files with 302 additions and 101 deletions
|
|
@ -4,7 +4,6 @@ import typing
|
|||
import urllib
|
||||
from collections import namedtuple
|
||||
from functools import partial
|
||||
from urllib.parse import urljoin
|
||||
|
||||
from celery import uuid as celery_uuid
|
||||
from django.conf import settings
|
||||
|
|
@ -27,10 +26,10 @@ from apps.alerts.tasks import (
|
|||
send_alert_group_signal_for_delete,
|
||||
unsilence_task,
|
||||
)
|
||||
from apps.grafana_plugin.ui_url_builder import UIURLBuilder
|
||||
from apps.metrics_exporter.tasks import update_metrics_for_alert_group
|
||||
from apps.slack.slack_formatter import SlackFormatter
|
||||
from apps.user_management.models import User
|
||||
from common.constants.plugin_ids import PluginID
|
||||
from common.public_primary_keys import generate_public_primary_key, increase_public_primary_key_length
|
||||
from common.utils import clean_markup, str_or_backup
|
||||
|
||||
|
|
@ -543,17 +542,19 @@ class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models.
|
|||
|
||||
@property
|
||||
def web_link(self) -> str:
|
||||
return urljoin(self.channel.organization.web_link, f"alert-groups/{self.public_primary_key}")
|
||||
return UIURLBuilder(self.channel.organization).alert_group_detail(self.public_primary_key)
|
||||
|
||||
@property
|
||||
def declare_incident_link(self) -> str:
|
||||
"""Generate a link for AlertGroup to declare Grafana Incident by click"""
|
||||
incident_link = urljoin(self.channel.organization.grafana_url, f"a/{PluginID.INCIDENT}/incidents/declare/")
|
||||
"""
|
||||
Generate a link for AlertGroup to declare Grafana Incident by click
|
||||
"""
|
||||
caption = urllib.parse.quote_plus("OnCall Alert Group")
|
||||
title = urllib.parse.quote_plus(self.web_title_cache) if self.web_title_cache else DEFAULT_BACKUP_TITLE
|
||||
title = title[:2000] # set max title length to avoid exceptions with too long declare incident link
|
||||
link = urllib.parse.quote_plus(self.web_link)
|
||||
return urljoin(incident_link, f"?caption={caption}&url={link}&title={title}")
|
||||
|
||||
return UIURLBuilder(self.channel.organization).declare_incident(f"?caption={caption}&url={link}&title={title}")
|
||||
|
||||
@property
|
||||
def happened_while_maintenance(self):
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import logging
|
||||
import typing
|
||||
from functools import cached_property
|
||||
from urllib.parse import urljoin
|
||||
|
||||
import emoji
|
||||
from celery import uuid as celery_uuid
|
||||
|
|
@ -21,6 +20,7 @@ from apps.alerts.models.maintainable_object import MaintainableObject
|
|||
from apps.alerts.tasks import disable_maintenance, disconnect_integration_from_alerting_contact_points
|
||||
from apps.base.messaging import get_messaging_backend_from_id
|
||||
from apps.base.utils import live_settings
|
||||
from apps.grafana_plugin.ui_url_builder import UIURLBuilder
|
||||
from apps.integrations.legacy_prefix import remove_legacy_prefix
|
||||
from apps.integrations.metadata import heartbeat
|
||||
from apps.integrations.tasks import create_alert, create_alertmanager_alerts
|
||||
|
|
@ -422,8 +422,8 @@ class AlertReceiveChannel(IntegrationOptionsMixin, MaintainableObject):
|
|||
|
||||
@property
|
||||
def new_incidents_web_link(self):
|
||||
return urljoin(
|
||||
self.organization.web_link, f"?page=incidents&integration={self.public_primary_key}&status=0&p=1"
|
||||
return UIURLBuilder(self.organization).alert_groups(
|
||||
f"?integration={self.public_primary_key}&status={AlertGroup.NEW}",
|
||||
)
|
||||
|
||||
@property
|
||||
|
|
@ -531,8 +531,8 @@ class AlertReceiveChannel(IntegrationOptionsMixin, MaintainableObject):
|
|||
return f"{self.get_integration_display()} {self.smile_code}"
|
||||
|
||||
@property
|
||||
def web_link(self):
|
||||
return urljoin(self.organization.web_link, f"integrations/{self.public_primary_key}")
|
||||
def web_link(self) -> str:
|
||||
return UIURLBuilder(self.organization).integration_detail(self.public_primary_key)
|
||||
|
||||
@property
|
||||
def integration_url(self) -> str | None:
|
||||
|
|
|
|||
|
|
@ -181,7 +181,7 @@ class ListUserSerializer(DynamicFieldsModelSerializer, EagerLoadingMixin):
|
|||
connector = self.context.get("connector", None)
|
||||
identities = self.context.get("cloud_identities", {})
|
||||
identity = identities.get(obj.email, None)
|
||||
status, _ = cloud_user_identity_status(connector, identity)
|
||||
status, _ = cloud_user_identity_status(obj.organization, connector, identity)
|
||||
return status
|
||||
return None
|
||||
|
||||
|
|
|
|||
|
|
@ -10,15 +10,18 @@ from rest_framework.test import APIClient
|
|||
|
||||
from apps.auth_token.constants import SLACK_AUTH_TOKEN_NAME
|
||||
from apps.social_auth.backends import SLACK_INSTALLATION_BACKEND
|
||||
from common.constants.plugin_ids import PluginID
|
||||
from common.constants.slack_auth import SLACK_OAUTH_ACCESS_RESPONSE
|
||||
|
||||
GRAFANA_URL = "http://example.com"
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.parametrize(
|
||||
"backend_name,expected_url",
|
||||
(
|
||||
("slack-login", "/a/grafana-oncall-app/users/me"),
|
||||
(SLACK_INSTALLATION_BACKEND, "/a/grafana-oncall-app/chat-ops"),
|
||||
("slack-login", f"{GRAFANA_URL}/a/{PluginID.ONCALL}/users/me"),
|
||||
(SLACK_INSTALLATION_BACKEND, f"{GRAFANA_URL}/a/{PluginID.ONCALL}/chat-ops"),
|
||||
),
|
||||
)
|
||||
def test_complete_slack_auth_redirect_ok(
|
||||
|
|
@ -28,7 +31,7 @@ def test_complete_slack_auth_redirect_ok(
|
|||
backend_name,
|
||||
expected_url,
|
||||
):
|
||||
organization = make_organization()
|
||||
organization = make_organization(grafana_url=GRAFANA_URL)
|
||||
admin = make_user_for_organization(organization)
|
||||
_, slack_token = make_slack_token_for_user(admin)
|
||||
|
||||
|
|
@ -181,7 +184,7 @@ def test_google_complete_auth_redirect_ok(
|
|||
make_user_for_organization,
|
||||
make_google_oauth2_token_for_user,
|
||||
):
|
||||
organization = make_organization()
|
||||
organization = make_organization(grafana_url=GRAFANA_URL)
|
||||
admin = make_user_for_organization(organization)
|
||||
_, google_oauth2_token = make_google_oauth2_token_for_user(admin)
|
||||
|
||||
|
|
@ -194,7 +197,7 @@ def test_google_complete_auth_redirect_ok(
|
|||
response = client.get(url)
|
||||
|
||||
assert response.status_code == status.HTTP_302_FOUND
|
||||
assert response.url == "/a/grafana-oncall-app/users/me"
|
||||
assert response.url == f"{GRAFANA_URL}/a/{PluginID.ONCALL}/users/me"
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import logging
|
||||
from urllib.parse import urljoin
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import REDIRECT_FIELD_NAME
|
||||
|
|
@ -19,6 +18,7 @@ from apps.chatops_proxy.utils import (
|
|||
get_installation_link_from_chatops_proxy,
|
||||
get_slack_oauth_response_from_chatops_proxy,
|
||||
)
|
||||
from apps.grafana_plugin.ui_url_builder import UIURLBuilder
|
||||
from apps.slack.installation import install_slack_integration
|
||||
from apps.social_auth.backends import SLACK_INSTALLATION_BACKEND, LoginSlackOAuth2V2
|
||||
|
||||
|
|
@ -73,13 +73,6 @@ def overridden_login_social_auth(request: Request, backend: str) -> Response:
|
|||
@psa("social:complete")
|
||||
def overridden_complete_social_auth(request: Request, backend: str, *args, **kwargs) -> Response:
|
||||
"""Authentication complete view"""
|
||||
if isinstance(request.backend, (LoginSlackOAuth2V2, GoogleOAuth2)):
|
||||
# if this was a user login/linking account, redirect to profile
|
||||
redirect_to = "/a/grafana-oncall-app/users/me"
|
||||
else:
|
||||
# InstallSlackOAuth2V2 backend
|
||||
redirect_to = "/a/grafana-oncall-app/chat-ops"
|
||||
|
||||
kwargs.update(
|
||||
user=request.user,
|
||||
redirect_name=REDIRECT_FIELD_NAME,
|
||||
|
|
@ -99,8 +92,16 @@ def overridden_complete_social_auth(request: Request, backend: str, *args, **kwa
|
|||
return_to = request.backend.strategy.session.get(REDIRECT_FIELD_NAME)
|
||||
|
||||
if return_to is None:
|
||||
# We build the frontend url using org url since multiple stacks could be connected to one backend.
|
||||
return_to = urljoin(request.user.organization.grafana_url, redirect_to)
|
||||
url_builder = UIURLBuilder(request.user.organization)
|
||||
|
||||
# if this was a user login/linking account, redirect to profile (ie. users/me)
|
||||
# otherwise it pertains to the InstallSlackOAuth2V2 backend, and we should redirect to the chat-ops page
|
||||
return_to = (
|
||||
url_builder.user_profile()
|
||||
if isinstance(request.backend, (LoginSlackOAuth2V2, GoogleOAuth2))
|
||||
else url_builder.chatops()
|
||||
)
|
||||
|
||||
return HttpResponseRedirect(return_to)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,15 @@
|
|||
# register_oncall_tenant moved to separate file from engine/apps/chatops_proxy/utils.py to avoid circular imports.
|
||||
import typing
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
from apps.chatops_proxy.client import APP_TYPE_ONCALL, ChatopsProxyAPIClient
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from apps.user_management.models import Organization
|
||||
|
||||
def register_oncall_tenant(org):
|
||||
|
||||
def register_oncall_tenant(org: "Organization") -> None:
|
||||
"""
|
||||
register_oncall_tenant registers oncall organization as a tenant in chatops-proxy.
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -3,10 +3,11 @@ Set of utils to handle oncall and chatops-proxy interaction.
|
|||
"""
|
||||
import logging
|
||||
import typing
|
||||
from urllib.parse import urljoin
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
from apps.grafana_plugin.ui_url_builder import UIURLBuilder
|
||||
|
||||
from .client import APP_TYPE_ONCALL, PROVIDER_TYPE_SLACK, ChatopsProxyAPIClient, ChatopsProxyAPIException
|
||||
from .register_oncall_tenant import register_oncall_tenant
|
||||
from .tasks import (
|
||||
|
|
@ -16,10 +17,13 @@ from .tasks import (
|
|||
unregister_oncall_tenant_async,
|
||||
)
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from apps.user_management.models import Organization, User
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_installation_link_from_chatops_proxy(user) -> typing.Optional[str]:
|
||||
def get_installation_link_from_chatops_proxy(user: "User") -> typing.Optional[str]:
|
||||
"""
|
||||
get_installation_link_from_chatops_proxy fetches slack installation link from chatops proxy.
|
||||
If there is no existing slack installation - if returns link, If slack already installed, it returns None.
|
||||
|
|
@ -30,7 +34,7 @@ def get_installation_link_from_chatops_proxy(user) -> typing.Optional[str]:
|
|||
link, _ = client.get_slack_oauth_link(
|
||||
org.stack_id,
|
||||
user.user_id,
|
||||
urljoin(org.web_link, "settings?tab=ChatOps&chatOpsTab=Slack"),
|
||||
UIURLBuilder(org).settings("?tab=ChatOps&chatOpsTab=Slack"),
|
||||
APP_TYPE_ONCALL,
|
||||
)
|
||||
return link
|
||||
|
|
@ -44,13 +48,13 @@ def get_installation_link_from_chatops_proxy(user) -> typing.Optional[str]:
|
|||
raise api_exc
|
||||
|
||||
|
||||
def get_slack_oauth_response_from_chatops_proxy(stack_id) -> dict:
|
||||
def get_slack_oauth_response_from_chatops_proxy(stack_id: int) -> dict:
|
||||
client = ChatopsProxyAPIClient(settings.ONCALL_GATEWAY_URL, settings.ONCALL_GATEWAY_API_TOKEN)
|
||||
slack_installation, _ = client.get_oauth_installation(stack_id, PROVIDER_TYPE_SLACK)
|
||||
return slack_installation.oauth_response
|
||||
|
||||
|
||||
def register_oncall_tenant_with_async_fallback(org):
|
||||
def register_oncall_tenant_with_async_fallback(org: "Organization") -> None:
|
||||
"""
|
||||
register_oncall_tenant tries to register oncall tenant synchronously and fall back to task in case of any exceptions
|
||||
to make sure that tenant is registered.
|
||||
|
|
|
|||
105
engine/apps/grafana_plugin/tests/test_ui_url_builder.py
Normal file
105
engine/apps/grafana_plugin/tests/test_ui_url_builder.py
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
import pytest
|
||||
|
||||
from apps.grafana_plugin.ui_url_builder import UIURLBuilder
|
||||
from common.constants.plugin_ids import PluginID
|
||||
|
||||
GRAFANA_URL = "http://example.com"
|
||||
ALERT_GROUP_ID = "1234"
|
||||
INTEGRATION_ID = "5678"
|
||||
SCHEDULE_ID = "lasdfasdf"
|
||||
PATH_EXTRA = "/extra?foo=bar"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def org_setup(make_organization):
|
||||
def _org_setup(is_grafana_irm_enabled=False):
|
||||
return make_organization(grafana_url=GRAFANA_URL, is_grafana_irm_enabled=is_grafana_irm_enabled)
|
||||
|
||||
return _org_setup
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"func,call_kwargs,expected_url",
|
||||
[
|
||||
# oncall pages
|
||||
(
|
||||
"home",
|
||||
{"path_extra": PATH_EXTRA},
|
||||
f"{GRAFANA_URL}/a/{PluginID.ONCALL}{PATH_EXTRA}",
|
||||
),
|
||||
(
|
||||
"alert_groups",
|
||||
{"path_extra": PATH_EXTRA},
|
||||
f"{GRAFANA_URL}/a/{PluginID.ONCALL}/alert-groups{PATH_EXTRA}",
|
||||
),
|
||||
(
|
||||
"alert_group_detail",
|
||||
{"id": ALERT_GROUP_ID, "path_extra": PATH_EXTRA},
|
||||
f"{GRAFANA_URL}/a/{PluginID.ONCALL}/alert-groups/{ALERT_GROUP_ID}{PATH_EXTRA}",
|
||||
),
|
||||
(
|
||||
"integration_detail",
|
||||
{"id": INTEGRATION_ID, "path_extra": PATH_EXTRA},
|
||||
f"{GRAFANA_URL}/a/{PluginID.ONCALL}/integrations/{INTEGRATION_ID}{PATH_EXTRA}",
|
||||
),
|
||||
(
|
||||
"schedules",
|
||||
{"path_extra": PATH_EXTRA},
|
||||
f"{GRAFANA_URL}/a/{PluginID.ONCALL}/schedules{PATH_EXTRA}",
|
||||
),
|
||||
(
|
||||
"schedule_detail",
|
||||
{"id": SCHEDULE_ID, "path_extra": PATH_EXTRA},
|
||||
f"{GRAFANA_URL}/a/{PluginID.ONCALL}/schedules/{SCHEDULE_ID}{PATH_EXTRA}",
|
||||
),
|
||||
(
|
||||
"users",
|
||||
{"path_extra": PATH_EXTRA},
|
||||
f"{GRAFANA_URL}/a/{PluginID.ONCALL}/users{PATH_EXTRA}",
|
||||
),
|
||||
(
|
||||
"user_profile",
|
||||
{"path_extra": PATH_EXTRA},
|
||||
f"{GRAFANA_URL}/a/{PluginID.ONCALL}/users/me{PATH_EXTRA}",
|
||||
),
|
||||
(
|
||||
"chatops",
|
||||
{"path_extra": PATH_EXTRA},
|
||||
f"{GRAFANA_URL}/a/{PluginID.ONCALL}/chat-ops{PATH_EXTRA}",
|
||||
),
|
||||
(
|
||||
"settings",
|
||||
{"path_extra": PATH_EXTRA},
|
||||
f"{GRAFANA_URL}/a/{PluginID.ONCALL}/settings{PATH_EXTRA}",
|
||||
),
|
||||
# incident pages
|
||||
(
|
||||
"declare_incident",
|
||||
{"path_extra": "?caption=abcd&url=asdf&title=test1234"},
|
||||
f"{GRAFANA_URL}/a/{PluginID.INCIDENT}/incidents/declare?caption=abcd&url=asdf&title=test1234",
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.django_db
|
||||
def test_build_page_urls(org_setup, func, call_kwargs, expected_url):
|
||||
builder = UIURLBuilder(org_setup())
|
||||
assert getattr(builder, func)(**call_kwargs) == expected_url
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_build_url_overriden_base_url(org_setup):
|
||||
overriden_base_url = "http://overriden.com"
|
||||
builder = UIURLBuilder(org_setup(), base_url=overriden_base_url)
|
||||
assert builder.chatops() == f"{overriden_base_url}/a/{PluginID.ONCALL}/chat-ops"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"is_grafana_irm_enabled,expected_url",
|
||||
[
|
||||
(True, f"{GRAFANA_URL}/a/{PluginID.IRM}/alert-groups/{ALERT_GROUP_ID}"),
|
||||
(False, f"{GRAFANA_URL}/a/{PluginID.ONCALL}/alert-groups/{ALERT_GROUP_ID}"),
|
||||
],
|
||||
)
|
||||
@pytest.mark.django_db
|
||||
def test_build_url_works_for_irm_and_oncall_plugins(org_setup, is_grafana_irm_enabled, expected_url):
|
||||
assert UIURLBuilder(org_setup(is_grafana_irm_enabled)).alert_group_detail(ALERT_GROUP_ID) == expected_url
|
||||
58
engine/apps/grafana_plugin/ui_url_builder.py
Normal file
58
engine/apps/grafana_plugin/ui_url_builder.py
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
import typing
|
||||
from urllib.parse import urljoin
|
||||
|
||||
from common.constants.plugin_ids import PluginID
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from apps.user_management.models import Organization
|
||||
|
||||
|
||||
class UIURLBuilder:
|
||||
"""
|
||||
If `base_url` is passed into the constructor, it will override using `organization.grafana_url`
|
||||
"""
|
||||
|
||||
def __init__(self, organization: "Organization", base_url: typing.Optional[str] = None) -> None:
|
||||
self.base_url = base_url if base_url else organization.grafana_url
|
||||
self.is_grafana_irm_enabled = organization.is_grafana_irm_enabled
|
||||
|
||||
def _build_url(self, page: str, path_extra: str = "", plugin_id: typing.Optional[str] = None) -> str:
|
||||
"""
|
||||
Constructs an absolute URL to a Grafana plugin page.
|
||||
"""
|
||||
if not plugin_id:
|
||||
plugin_id = PluginID.IRM if self.is_grafana_irm_enabled else PluginID.ONCALL
|
||||
return urljoin(self.base_url, f"a/{plugin_id}/{page}{path_extra}")
|
||||
|
||||
def home(self, path_extra: str = "") -> str:
|
||||
return self._build_url("", path_extra)
|
||||
|
||||
def alert_groups(self, path_extra: str = "") -> str:
|
||||
return self._build_url("alert-groups", path_extra)
|
||||
|
||||
def alert_group_detail(self, id: str, path_extra: str = "") -> str:
|
||||
return self._build_url(f"alert-groups/{id}", path_extra)
|
||||
|
||||
def integration_detail(self, id: str, path_extra: str = "") -> str:
|
||||
return self._build_url(f"integrations/{id}", path_extra)
|
||||
|
||||
def schedules(self, path_extra: str = "") -> str:
|
||||
return self._build_url("schedules", path_extra)
|
||||
|
||||
def schedule_detail(self, id: str, path_extra: str = "") -> str:
|
||||
return self._build_url(f"schedules/{id}", path_extra)
|
||||
|
||||
def users(self, path_extra: str = "") -> str:
|
||||
return self._build_url("users", path_extra)
|
||||
|
||||
def user_profile(self, path_extra: str = "") -> str:
|
||||
return self._build_url("users/me", path_extra)
|
||||
|
||||
def chatops(self, path_extra: str = "") -> str:
|
||||
return self._build_url("chat-ops", path_extra)
|
||||
|
||||
def settings(self, path_extra: str = "") -> str:
|
||||
return self._build_url("settings", path_extra)
|
||||
|
||||
def declare_incident(self, path_extra: str = "") -> str:
|
||||
return self._build_url("incidents/declare", path_extra, plugin_id=PluginID.INCIDENT)
|
||||
|
|
@ -1,14 +1,19 @@
|
|||
import logging
|
||||
import random
|
||||
from urllib.parse import urljoin
|
||||
import typing
|
||||
|
||||
import requests
|
||||
from django.conf import settings
|
||||
from rest_framework import status
|
||||
|
||||
from apps.base.utils import live_settings
|
||||
from apps.grafana_plugin.ui_url_builder import UIURLBuilder
|
||||
from common.api_helpers.utils import create_engine_url
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from apps.oss_installation.models import CloudConnector, CloudHeartbeat
|
||||
from apps.user_management.models import Organization
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
|
@ -102,9 +107,13 @@ def send_cloud_heartbeat():
|
|||
logger.info("Finish send cloud heartbeat")
|
||||
|
||||
|
||||
def get_heartbeat_link(connector, heartbeat):
|
||||
def get_heartbeat_link(
|
||||
organization: "Organization",
|
||||
connector: typing.Optional["CloudConnector"],
|
||||
heartbeat: typing.Optional["CloudHeartbeat"],
|
||||
) -> typing.Optional[str]:
|
||||
if connector is None:
|
||||
return None
|
||||
if heartbeat is None:
|
||||
return None
|
||||
return urljoin(connector.cloud_url, f"a/grafana-oncall-app/?page=integrations&id={heartbeat.integration_id}")
|
||||
return UIURLBuilder(organization, base_url=connector.cloud_url).integration_detail(heartbeat.integration_id)
|
||||
|
|
|
|||
|
|
@ -12,9 +12,9 @@ class CloudUserSerializer(serializers.ModelSerializer):
|
|||
model = User
|
||||
fields = ["cloud_data"]
|
||||
|
||||
def get_cloud_data(self, obj):
|
||||
def get_cloud_data(self, obj: User):
|
||||
connector = CloudConnector.objects.filter().first()
|
||||
cloud_user_identity = CloudUserIdentity.objects.filter(email=obj.email).first()
|
||||
status, link = cloud_user_identity_status(connector, cloud_user_identity)
|
||||
status, link = cloud_user_identity_status(obj.organization, connector, cloud_user_identity)
|
||||
cloud_data = {"status": status, "link": link}
|
||||
return cloud_data
|
||||
|
|
|
|||
|
|
@ -1,11 +1,15 @@
|
|||
import logging
|
||||
from urllib.parse import urljoin
|
||||
import typing
|
||||
|
||||
from django.utils import timezone
|
||||
|
||||
from apps.grafana_plugin.ui_url_builder import UIURLBuilder
|
||||
from apps.oss_installation.constants import CloudSyncStatus
|
||||
from apps.schedules.ical_utils import list_users_to_notify_from_ical_for_period
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from apps.user_management.models import Organization
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
|
@ -67,7 +71,7 @@ def active_oss_users_count():
|
|||
return len(unique_active_users)
|
||||
|
||||
|
||||
def cloud_user_identity_status(connector, identity):
|
||||
def cloud_user_identity_status(org: "Organization", connector, identity):
|
||||
link = None
|
||||
if connector is None:
|
||||
status = CloudSyncStatus.NOT_SYNCED
|
||||
|
|
@ -80,5 +84,5 @@ def cloud_user_identity_status(connector, identity):
|
|||
else:
|
||||
status = CloudSyncStatus.SYNCED_PHONE_NOT_VERIFIED
|
||||
|
||||
link = urljoin(connector.cloud_url, f"a/grafana-oncall-app/?page=users&p=1&id={identity.cloud_id}")
|
||||
link = UIURLBuilder(org, base_url=connector.cloud_url).users(f"?p=1&id={identity.cloud_id}")
|
||||
return status, link
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ class CloudConnectionView(APIView):
|
|||
"cloud_connection_status": connector is not None,
|
||||
"cloud_notifications_enabled": live_settings.GRAFANA_CLOUD_NOTIFICATIONS_ENABLED,
|
||||
"cloud_heartbeat_enabled": live_settings.GRAFANA_CLOUD_ONCALL_HEARTBEAT_ENABLED,
|
||||
"cloud_heartbeat_link": get_heartbeat_link(connector, heartbeat),
|
||||
"cloud_heartbeat_link": get_heartbeat_link(self.request.auth.organization, connector, heartbeat),
|
||||
"cloud_heartbeat_status": heartbeat is not None and heartbeat.success,
|
||||
}
|
||||
return Response(response)
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ class CloudHeartbeatView(APIView):
|
|||
return Response(status=status.HTTP_400_BAD_REQUEST, data={"detail": "Cloud heartbeat already exists"})
|
||||
except CloudHeartbeat.DoesNotExist:
|
||||
heartbeat = setup_heartbeat_integration()
|
||||
link = get_heartbeat_link(connector, heartbeat)
|
||||
link = get_heartbeat_link(self.request.auth.organization, connector, heartbeat)
|
||||
return Response(status=status.HTTP_200_OK, data={"link": link})
|
||||
else:
|
||||
return Response(status=status.HTTP_400_BAD_REQUEST, data={"detail": "Grafana Cloud is not connected"})
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ class CloudUsersView(CloudUsersPagination, APIView):
|
|||
|
||||
for user in results:
|
||||
cloud_identity = cloud_identities.get(user.email, None)
|
||||
status, link = cloud_user_identity_status(connector, cloud_identity)
|
||||
status, link = cloud_user_identity_status(organization, connector, cloud_identity)
|
||||
data.append(
|
||||
{
|
||||
"id": user.public_primary_key,
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ from rest_framework.test import APIClient
|
|||
|
||||
from apps.alerts.models import AlertGroup
|
||||
from apps.alerts.paging import DirectPagingAlertGroupResolvedError, DirectPagingUserTeamValidationError
|
||||
from common.constants.plugin_ids import PluginID
|
||||
|
||||
title = "Custom title"
|
||||
message = "Testing escalation with new alert group"
|
||||
|
|
@ -70,7 +71,7 @@ def test_escalation_new_alert_group(
|
|||
"slack": None,
|
||||
"slack_app": None,
|
||||
"telegram": None,
|
||||
"web": f"a/grafana-oncall-app/alert-groups/{ag.public_primary_key}",
|
||||
"web": f"a/{PluginID.ONCALL}/alert-groups/{ag.public_primary_key}",
|
||||
},
|
||||
"silenced_at": None,
|
||||
"last_alert": {
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ from polymorphic.managers import PolymorphicManager
|
|||
from polymorphic.models import PolymorphicModel
|
||||
from polymorphic.query import PolymorphicQuerySet
|
||||
|
||||
from apps.grafana_plugin.ui_url_builder import UIURLBuilder
|
||||
from apps.schedules.constants import (
|
||||
EXPORT_WINDOW_DAYS_AFTER,
|
||||
EXPORT_WINDOW_DAYS_BEFORE,
|
||||
|
|
@ -247,11 +248,15 @@ class OnCallSchedule(PolymorphicModel):
|
|||
|
||||
@property
|
||||
def web_page_link(self) -> str:
|
||||
return f"{self.organization.web_link}schedules"
|
||||
return UIURLBuilder(self.organization).schedules()
|
||||
|
||||
@property
|
||||
def web_detail_page_link(self) -> str:
|
||||
return f"{self.web_page_link}/{self.public_primary_key}"
|
||||
return UIURLBuilder(self.organization).schedule_detail(self.public_primary_key)
|
||||
|
||||
@property
|
||||
def slack_url(self) -> str:
|
||||
return f"<{self.web_detail_page_link}|{self.name}>"
|
||||
|
||||
def get_icalendars(self) -> typing.Tuple[typing.Optional[icalendar.Calendar], typing.Optional[icalendar.Calendar]]:
|
||||
"""Returns list of calendars. Primary calendar should always be the first"""
|
||||
|
|
|
|||
|
|
@ -171,10 +171,6 @@ class ShiftSwapRequest(models.Model):
|
|||
"""
|
||||
return self.schedule.channel
|
||||
|
||||
@property
|
||||
def schedule_slack_url(self) -> str:
|
||||
return f"<{self.schedule.web_detail_page_link}|{self.schedule.name}>"
|
||||
|
||||
@property
|
||||
def organization(self) -> "Organization":
|
||||
return self.schedule.organization
|
||||
|
|
@ -183,11 +179,6 @@ class ShiftSwapRequest(models.Model):
|
|||
def possible_benefactors(self) -> QuerySet["User"]:
|
||||
return self.schedule.related_users().exclude(pk=self.beneficiary_id)
|
||||
|
||||
@property
|
||||
def web_link(self) -> str:
|
||||
# TODO: finish this once we know the proper URL we'll need
|
||||
return f"{self.schedule.web_detail_page_link}"
|
||||
|
||||
def delete(self):
|
||||
self.deleted_at = timezone.now()
|
||||
self.save()
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import enum
|
|||
import json
|
||||
import logging
|
||||
import typing
|
||||
from urllib.parse import urljoin
|
||||
from uuid import uuid4
|
||||
|
||||
from django.conf import settings
|
||||
|
|
@ -12,6 +11,7 @@ from rest_framework.response import Response
|
|||
from apps.alerts.models import AlertReceiveChannel
|
||||
from apps.alerts.paging import DirectPagingUserTeamValidationError, UserNotifications, direct_paging, user_is_oncall
|
||||
from apps.api.permissions import RBACPermission, user_is_authorized
|
||||
from apps.grafana_plugin.ui_url_builder import UIURLBuilder
|
||||
from apps.schedules.ical_utils import get_cached_oncall_users_for_multiple_schedules
|
||||
from apps.slack.chatops_proxy_routing import make_private_metadata, make_value
|
||||
from apps.slack.constants import DIVIDER, PRIVATE_METADATA_MAX_LENGTH
|
||||
|
|
@ -146,8 +146,8 @@ class StartDirectPaging(scenario_step.ScenarioStep):
|
|||
if slack_team_identity.needs_reinstall:
|
||||
organizations = _get_available_organizations(slack_team_identity, slack_user_identity)
|
||||
if len(organizations) == 1:
|
||||
# Provide a link to web if user has access only to one organization
|
||||
link = urljoin(organizations[0].web_link, "settings?tab=ChatOps&chatOpsTab=Slack")
|
||||
# Provide a link to web if user has access only to one organization
|
||||
link = UIURLBuilder(organizations[0]).settings("?tab=ChatOps&chatOpsTab=Slack")
|
||||
else:
|
||||
# Otherwise, provide a link to the documentation
|
||||
link = (
|
||||
|
|
|
|||
|
|
@ -215,7 +215,7 @@ class EditScheduleShiftNotifyStep(scenario_step.ScenarioStep):
|
|||
"type": "section",
|
||||
"text": {
|
||||
"type": "mrkdwn",
|
||||
"text": f"On-call shifts update for schedule *<{schedule.web_detail_page_link}|{schedule.name}>*",
|
||||
"text": f"On-call shifts update for schedule *{schedule.slack_url}*",
|
||||
"verbatim": True,
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ class BaseShiftSwapRequestStep(scenario_step.ScenarioStep):
|
|||
pk = shift_swap_request.pk
|
||||
|
||||
main_message_text = (
|
||||
f"*New shift swap request for {shift_swap_request.schedule_slack_url}*\n"
|
||||
f"*New shift swap request for {shift_swap_request.schedule.slack_url}*\n"
|
||||
f"Your teammate {shift_swap_request.beneficiary.get_username_with_slack_verbal(True)} has submitted "
|
||||
"a shift swap request."
|
||||
)
|
||||
|
|
@ -262,7 +262,7 @@ class ShiftSwapRequestFollowUp(BaseShiftSwapRequestStep):
|
|||
"text": {
|
||||
"type": "mrkdwn",
|
||||
"text": (
|
||||
f"⚠️ This shift swap request for {shift_swap_request.schedule_slack_url} is "
|
||||
f"⚠️ This shift swap request for {shift_swap_request.schedule.slack_url} is "
|
||||
f"still open and will start in {delta}. Jump back into the thread and accept it if "
|
||||
"you're available!"
|
||||
),
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ from apps.slack.scenarios.paging import OnPagingTeamChange, StartDirectPaging
|
|||
from apps.slack.scenarios.schedules import EditScheduleShiftNotifyStep
|
||||
from apps.slack.scenarios.shift_swap_requests import AcceptShiftSwapRequestStep
|
||||
from apps.slack.types import PayloadType
|
||||
from common.constants.plugin_ids import PluginID
|
||||
|
||||
EVENT_TRIGGER_ID = "5333959822612.4122782784722.4734ff484b2ac4d36a185bb242ee9932"
|
||||
WARNING_TEXT = (
|
||||
|
|
@ -83,7 +84,7 @@ def test_no_user_in_organization_for_slack_team_identity(
|
|||
mock_open_warning_window_if_needed.assert_called_once_with(
|
||||
event_payload,
|
||||
slack_team_identity,
|
||||
"Permission denied. Please connect your Slack account to OnCall: https://test.com/a/grafana-oncall-app/users/me/",
|
||||
f"Permission denied. Please connect your Slack account to OnCall: https://test.com/a/{PluginID.ONCALL}/users/me",
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ class TestBaseShiftSwapRequestStep:
|
|||
blocks = step._generate_blocks(ssr)
|
||||
|
||||
assert blocks[0]["text"]["text"] == (
|
||||
f"*New shift swap request for <{ssr.schedule.web_detail_page_link}|{ssr.schedule.name}>*\n"
|
||||
f"*New shift swap request for {ssr.schedule.slack_url}*\n"
|
||||
f"Your teammate {beneficiary.get_username_with_slack_verbal()} has submitted a shift swap request."
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import hmac
|
|||
import json
|
||||
import logging
|
||||
from contextlib import suppress
|
||||
from urllib.parse import urljoin
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
|
|
@ -17,6 +16,7 @@ from apps.api.permissions import RBACPermission
|
|||
from apps.auth_token.auth import PluginAuthentication
|
||||
from apps.base.utils import live_settings
|
||||
from apps.chatops_proxy.utils import uninstall_slack as uninstall_slack_from_chatops_proxy
|
||||
from apps.grafana_plugin.ui_url_builder import UIURLBuilder
|
||||
from apps.slack.client import SlackClient
|
||||
from apps.slack.errors import SlackAPIError
|
||||
from apps.slack.scenarios.alertgroup_appearance import STEPS_ROUTING as ALERTGROUP_APPEARANCE_ROUTING
|
||||
|
|
@ -293,7 +293,7 @@ class SlackEventApiEndpointView(APIView):
|
|||
elif organization:
|
||||
user = slack_user_identity.get_user(organization)
|
||||
if not user: # SlackUserIdentity exists but not connected to any user in this organization
|
||||
user_settings_url = urljoin(organization.grafana_url, "/a/grafana-oncall-app/users/me/")
|
||||
user_settings_url = UIURLBuilder(organization).user_profile()
|
||||
self._open_warning_window_if_needed(
|
||||
payload,
|
||||
slack_team_identity,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import logging
|
||||
from urllib.parse import urljoin
|
||||
|
||||
from django.http import HttpResponse
|
||||
from django.shortcuts import redirect
|
||||
|
|
@ -7,9 +6,10 @@ from rest_framework import status
|
|||
from social_core import exceptions
|
||||
from social_django.middleware import SocialAuthExceptionMiddleware
|
||||
|
||||
from apps.grafana_plugin.ui_url_builder import UIURLBuilder
|
||||
from apps.social_auth.backends import LoginSlackOAuth2V2
|
||||
from apps.social_auth.exceptions import InstallMultiRegionSlackException
|
||||
from common.constants.slack_auth import REDIRECT_AFTER_SLACK_INSTALL, SLACK_AUTH_FAILED
|
||||
from common.constants.slack_auth import REDIRECT_AFTER_SLACK_INSTALL, SLACK_AUTH_FAILED, SLACK_REGION_ERROR
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -17,28 +17,24 @@ logger = logging.getLogger(__name__)
|
|||
class SocialAuthAuthCanceledExceptionMiddleware(SocialAuthExceptionMiddleware):
|
||||
def process_exception(self, request, exception):
|
||||
backend = getattr(exception, "backend", None)
|
||||
redirect_to = "/a/grafana-oncall-app/chat-ops"
|
||||
url_builder = UIURLBuilder(request.user.organization)
|
||||
url_builder_function = url_builder.chatops
|
||||
|
||||
if backend is not None and isinstance(backend, LoginSlackOAuth2V2):
|
||||
redirect_to = "/a/grafana-oncall-app/users/me"
|
||||
url_builder_function = url_builder.user_profile
|
||||
|
||||
if exception:
|
||||
logger.warning(f"SocialAuthAuthCanceledExceptionMiddleware.process_exception: {exception}")
|
||||
|
||||
if isinstance(exception, exceptions.AuthCanceled):
|
||||
# if user canceled authentication, redirect them to the previous page using the same link
|
||||
# as we used to redirect after auth/install
|
||||
url_to_redirect = urljoin(request.user.organization.grafana_url, redirect_to)
|
||||
return redirect(url_to_redirect)
|
||||
return redirect(url_builder_function())
|
||||
elif isinstance(exception, exceptions.AuthFailed):
|
||||
# if authentication was failed, redirect user to the plugin page using the same link
|
||||
# as we used to redirect after auth/install with error flag
|
||||
url_to_redirect = urljoin(
|
||||
request.user.organization.grafana_url, f"{redirect_to}&slack_error={SLACK_AUTH_FAILED}"
|
||||
)
|
||||
return redirect(url_to_redirect)
|
||||
return redirect(url_builder_function(f"?slack_error={SLACK_AUTH_FAILED}"))
|
||||
elif isinstance(exception, KeyError) and REDIRECT_AFTER_SLACK_INSTALL in exception.args:
|
||||
return HttpResponse(status=status.HTTP_401_UNAUTHORIZED)
|
||||
elif isinstance(exception, InstallMultiRegionSlackException):
|
||||
REGION_ERROR = "region_error"
|
||||
url_to_redirect = urljoin(
|
||||
request.user.organization.grafana_url, f"{redirect_to}?tab=Slack&slack_error={REGION_ERROR}"
|
||||
)
|
||||
return redirect(url_to_redirect)
|
||||
return redirect(url_builder_function(f"?tab=Slack&slack_error={SLACK_REGION_ERROR}"))
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import logging
|
||||
import typing
|
||||
from urllib.parse import urljoin
|
||||
|
||||
import requests
|
||||
from django.contrib.auth import REDIRECT_FIELD_NAME
|
||||
|
|
@ -9,6 +8,7 @@ from rest_framework import status
|
|||
from social_core.backends.base import BaseAuth
|
||||
|
||||
from apps.google.utils import user_granted_all_required_scopes
|
||||
from apps.grafana_plugin.ui_url_builder import UIURLBuilder
|
||||
from apps.social_auth.exceptions import GOOGLE_AUTH_MISSING_GRANTED_SCOPE_ERROR
|
||||
from apps.social_auth.types import GoogleOauth2Response
|
||||
from apps.user_management.models import Organization, User
|
||||
|
|
@ -32,10 +32,9 @@ def connect_user_to_google(
|
|||
f"granted_scopes={granted_scopes}"
|
||||
)
|
||||
|
||||
base_url_to_redirect = urljoin(organization.grafana_url, "/a/grafana-oncall-app/users/me")
|
||||
strategy.session[
|
||||
REDIRECT_FIELD_NAME
|
||||
] = f"{base_url_to_redirect}?google_error={GOOGLE_AUTH_MISSING_GRANTED_SCOPE_ERROR}"
|
||||
strategy.session[REDIRECT_FIELD_NAME] = UIURLBuilder(organization).user_profile(
|
||||
f"?google_error={GOOGLE_AUTH_MISSING_GRANTED_SCOPE_ERROR}"
|
||||
)
|
||||
|
||||
return HttpResponse(status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import logging
|
||||
from urllib.parse import urljoin
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import REDIRECT_FIELD_NAME
|
||||
|
|
@ -7,6 +6,7 @@ from django.http import HttpResponse
|
|||
from rest_framework import status
|
||||
|
||||
from apps.chatops_proxy.utils import can_link_slack_team, link_slack_team
|
||||
from apps.grafana_plugin.ui_url_builder import UIURLBuilder
|
||||
from apps.slack.installation import SlackInstallationExc, install_slack_integration
|
||||
from apps.social_auth.backends import SLACK_INSTALLATION_BACKEND
|
||||
from apps.social_auth.exceptions import InstallMultiRegionSlackException
|
||||
|
|
@ -25,9 +25,7 @@ def connect_user_to_slack(response, backend, strategy, user, organization, *args
|
|||
|
||||
slack_team_identity = organization.slack_team_identity
|
||||
slack_user_id = response["authed_user"]["id"]
|
||||
|
||||
redirect_to = "/a/grafana-oncall-app/users/me/"
|
||||
base_url_to_redirect = urljoin(organization.grafana_url, redirect_to)
|
||||
url_builder = UIURLBuilder(organization)
|
||||
|
||||
if slack_team_identity is None:
|
||||
# means that organization doesn't have slack integration, so user cannot connect their account to slack
|
||||
|
|
@ -36,14 +34,16 @@ def connect_user_to_slack(response, backend, strategy, user, organization, *args
|
|||
if slack_team_identity.slack_id != response["team"]["id"]:
|
||||
# means that user authed in another slack workspace that is not connected to their organization
|
||||
# change redirect url to show user error message and save it in session param
|
||||
url = base_url_to_redirect + f"?slack_error={SLACK_AUTH_WRONG_WORKSPACE_ERROR}"
|
||||
strategy.session[REDIRECT_FIELD_NAME] = url
|
||||
strategy.session[REDIRECT_FIELD_NAME] = url_builder.user_profile(
|
||||
f"?slack_error={SLACK_AUTH_WRONG_WORKSPACE_ERROR}",
|
||||
)
|
||||
return HttpResponse(status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
if organization.users.filter(slack_user_identity__slack_id=slack_user_id).exists():
|
||||
# means that slack user has already been connected to another user in current organization
|
||||
url = base_url_to_redirect + f"?slack_error={SLACK_AUTH_SLACK_USER_ALREADY_CONNECTED_ERROR}"
|
||||
strategy.session[REDIRECT_FIELD_NAME] = url
|
||||
strategy.session[REDIRECT_FIELD_NAME] = url_builder.user_profile(
|
||||
f"?slack_error={SLACK_AUTH_SLACK_USER_ALREADY_CONNECTED_ERROR}",
|
||||
)
|
||||
return HttpResponse(status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# at this point everything is correct and we can create the SlackUserIdentity
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 4.2.15 on 2024-10-08 14:57
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('user_management', '0022_alter_team_unique_together'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='organization',
|
||||
name='is_grafana_irm_enabled',
|
||||
field=models.BooleanField(default=False, null=True),
|
||||
),
|
||||
]
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
import logging
|
||||
import typing
|
||||
import uuid
|
||||
from urllib.parse import urljoin
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.validators import MinLengthValidator
|
||||
|
|
@ -16,6 +15,7 @@ from apps.chatops_proxy.utils import (
|
|||
unlink_slack_team,
|
||||
unregister_oncall_tenant,
|
||||
)
|
||||
from apps.grafana_plugin.ui_url_builder import UIURLBuilder
|
||||
from apps.user_management.subscription_strategy import FreePublicBetaSubscriptionStrategy
|
||||
from apps.user_management.types import AlertGroupTableColumn
|
||||
from common.insight_log import ChatOpsEvent, ChatOpsTypePlug, write_chatops_insight_log
|
||||
|
|
@ -253,6 +253,7 @@ class Organization(MaintainableObject):
|
|||
is_rbac_permissions_enabled = models.BooleanField(default=False)
|
||||
is_grafana_incident_enabled = models.BooleanField(default=False)
|
||||
is_grafana_labels_enabled = models.BooleanField(default=False, null=True)
|
||||
is_grafana_irm_enabled = models.BooleanField(default=False, null=True)
|
||||
|
||||
alert_group_table_columns: list[AlertGroupTableColumn] | None = JSONField(default=None, null=True)
|
||||
grafana_incident_backend_url = models.CharField(max_length=300, null=True, default=None)
|
||||
|
|
@ -344,14 +345,12 @@ class Organization(MaintainableObject):
|
|||
.distinct()
|
||||
)
|
||||
|
||||
@property
|
||||
def web_link(self):
|
||||
return urljoin(self.grafana_url, "a/grafana-oncall-app/")
|
||||
|
||||
@property
|
||||
def web_link_with_uuid(self):
|
||||
# It's a workaround to pass some unique identifier to the oncall gateway while proxying telegram requests
|
||||
return urljoin(self.grafana_url, f"a/grafana-oncall-app/?oncall-uuid={self.uuid}")
|
||||
"""
|
||||
It's a workaround to pass some unique identifier to the oncall gateway while proxying telegram requests
|
||||
"""
|
||||
return UIURLBuilder(self).home(f"?oncall-uuid={self.uuid}")
|
||||
|
||||
@classmethod
|
||||
def __str__(self):
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ REDIRECT_AFTER_SLACK_INSTALL = "redirect_after_slack_install"
|
|||
SLACK_AUTH_WRONG_WORKSPACE_ERROR = "wrong_workspace"
|
||||
SLACK_AUTH_SLACK_USER_ALREADY_CONNECTED_ERROR = "user_already_connected"
|
||||
SLACK_AUTH_FAILED = "auth_failed"
|
||||
SLACK_REGION_ERROR = "region_error"
|
||||
|
||||
|
||||
# Example of a slack oauth response to be used in tests.
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue