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:
Joey Orlando 2024-10-09 08:55:10 -04:00 committed by GitHub
parent 2545bf8336
commit bfcc0b9f29
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
30 changed files with 302 additions and 101 deletions

View file

@ -4,7 +4,6 @@ import typing
import urllib import urllib
from collections import namedtuple from collections import namedtuple
from functools import partial from functools import partial
from urllib.parse import urljoin
from celery import uuid as celery_uuid from celery import uuid as celery_uuid
from django.conf import settings from django.conf import settings
@ -27,10 +26,10 @@ from apps.alerts.tasks import (
send_alert_group_signal_for_delete, send_alert_group_signal_for_delete,
unsilence_task, unsilence_task,
) )
from apps.grafana_plugin.ui_url_builder import UIURLBuilder
from apps.metrics_exporter.tasks import update_metrics_for_alert_group from apps.metrics_exporter.tasks import update_metrics_for_alert_group
from apps.slack.slack_formatter import SlackFormatter from apps.slack.slack_formatter import SlackFormatter
from apps.user_management.models import User 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.public_primary_keys import generate_public_primary_key, increase_public_primary_key_length
from common.utils import clean_markup, str_or_backup from common.utils import clean_markup, str_or_backup
@ -543,17 +542,19 @@ class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models.
@property @property
def web_link(self) -> str: 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 @property
def declare_incident_link(self) -> str: 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") 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 = 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 title = title[:2000] # set max title length to avoid exceptions with too long declare incident link
link = urllib.parse.quote_plus(self.web_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 @property
def happened_while_maintenance(self): def happened_while_maintenance(self):

View file

@ -1,7 +1,6 @@
import logging import logging
import typing import typing
from functools import cached_property from functools import cached_property
from urllib.parse import urljoin
import emoji import emoji
from celery import uuid as celery_uuid 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.alerts.tasks import disable_maintenance, disconnect_integration_from_alerting_contact_points
from apps.base.messaging import get_messaging_backend_from_id from apps.base.messaging import get_messaging_backend_from_id
from apps.base.utils import live_settings 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.legacy_prefix import remove_legacy_prefix
from apps.integrations.metadata import heartbeat from apps.integrations.metadata import heartbeat
from apps.integrations.tasks import create_alert, create_alertmanager_alerts from apps.integrations.tasks import create_alert, create_alertmanager_alerts
@ -422,8 +422,8 @@ class AlertReceiveChannel(IntegrationOptionsMixin, MaintainableObject):
@property @property
def new_incidents_web_link(self): def new_incidents_web_link(self):
return urljoin( return UIURLBuilder(self.organization).alert_groups(
self.organization.web_link, f"?page=incidents&integration={self.public_primary_key}&status=0&p=1" f"?integration={self.public_primary_key}&status={AlertGroup.NEW}",
) )
@property @property
@ -531,8 +531,8 @@ class AlertReceiveChannel(IntegrationOptionsMixin, MaintainableObject):
return f"{self.get_integration_display()} {self.smile_code}" return f"{self.get_integration_display()} {self.smile_code}"
@property @property
def web_link(self): def web_link(self) -> str:
return urljoin(self.organization.web_link, f"integrations/{self.public_primary_key}") return UIURLBuilder(self.organization).integration_detail(self.public_primary_key)
@property @property
def integration_url(self) -> str | None: def integration_url(self) -> str | None:

View file

@ -181,7 +181,7 @@ class ListUserSerializer(DynamicFieldsModelSerializer, EagerLoadingMixin):
connector = self.context.get("connector", None) connector = self.context.get("connector", None)
identities = self.context.get("cloud_identities", {}) identities = self.context.get("cloud_identities", {})
identity = identities.get(obj.email, None) 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 status
return None return None

View file

@ -10,15 +10,18 @@ from rest_framework.test import APIClient
from apps.auth_token.constants import SLACK_AUTH_TOKEN_NAME from apps.auth_token.constants import SLACK_AUTH_TOKEN_NAME
from apps.social_auth.backends import SLACK_INSTALLATION_BACKEND 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 from common.constants.slack_auth import SLACK_OAUTH_ACCESS_RESPONSE
GRAFANA_URL = "http://example.com"
@pytest.mark.django_db @pytest.mark.django_db
@pytest.mark.parametrize( @pytest.mark.parametrize(
"backend_name,expected_url", "backend_name,expected_url",
( (
("slack-login", "/a/grafana-oncall-app/users/me"), ("slack-login", f"{GRAFANA_URL}/a/{PluginID.ONCALL}/users/me"),
(SLACK_INSTALLATION_BACKEND, "/a/grafana-oncall-app/chat-ops"), (SLACK_INSTALLATION_BACKEND, f"{GRAFANA_URL}/a/{PluginID.ONCALL}/chat-ops"),
), ),
) )
def test_complete_slack_auth_redirect_ok( def test_complete_slack_auth_redirect_ok(
@ -28,7 +31,7 @@ def test_complete_slack_auth_redirect_ok(
backend_name, backend_name,
expected_url, expected_url,
): ):
organization = make_organization() organization = make_organization(grafana_url=GRAFANA_URL)
admin = make_user_for_organization(organization) admin = make_user_for_organization(organization)
_, slack_token = make_slack_token_for_user(admin) _, slack_token = make_slack_token_for_user(admin)
@ -181,7 +184,7 @@ def test_google_complete_auth_redirect_ok(
make_user_for_organization, make_user_for_organization,
make_google_oauth2_token_for_user, make_google_oauth2_token_for_user,
): ):
organization = make_organization() organization = make_organization(grafana_url=GRAFANA_URL)
admin = make_user_for_organization(organization) admin = make_user_for_organization(organization)
_, google_oauth2_token = make_google_oauth2_token_for_user(admin) _, 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) response = client.get(url)
assert response.status_code == status.HTTP_302_FOUND 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 @pytest.mark.django_db

View file

@ -1,5 +1,4 @@
import logging import logging
from urllib.parse import urljoin
from django.conf import settings from django.conf import settings
from django.contrib.auth import REDIRECT_FIELD_NAME 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_installation_link_from_chatops_proxy,
get_slack_oauth_response_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.slack.installation import install_slack_integration
from apps.social_auth.backends import SLACK_INSTALLATION_BACKEND, LoginSlackOAuth2V2 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") @psa("social:complete")
def overridden_complete_social_auth(request: Request, backend: str, *args, **kwargs) -> Response: def overridden_complete_social_auth(request: Request, backend: str, *args, **kwargs) -> Response:
"""Authentication complete view""" """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( kwargs.update(
user=request.user, user=request.user,
redirect_name=REDIRECT_FIELD_NAME, 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) return_to = request.backend.strategy.session.get(REDIRECT_FIELD_NAME)
if return_to is None: if return_to is None:
# We build the frontend url using org url since multiple stacks could be connected to one backend. url_builder = UIURLBuilder(request.user.organization)
return_to = urljoin(request.user.organization.grafana_url, redirect_to)
# 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) return HttpResponseRedirect(return_to)

View file

@ -1,10 +1,15 @@
# register_oncall_tenant moved to separate file from engine/apps/chatops_proxy/utils.py to avoid circular imports. # 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 django.conf import settings
from apps.chatops_proxy.client import APP_TYPE_ONCALL, ChatopsProxyAPIClient 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. register_oncall_tenant registers oncall organization as a tenant in chatops-proxy.
""" """

View file

@ -3,10 +3,11 @@ Set of utils to handle oncall and chatops-proxy interaction.
""" """
import logging import logging
import typing import typing
from urllib.parse import urljoin
from django.conf import settings 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 .client import APP_TYPE_ONCALL, PROVIDER_TYPE_SLACK, ChatopsProxyAPIClient, ChatopsProxyAPIException
from .register_oncall_tenant import register_oncall_tenant from .register_oncall_tenant import register_oncall_tenant
from .tasks import ( from .tasks import (
@ -16,10 +17,13 @@ from .tasks import (
unregister_oncall_tenant_async, unregister_oncall_tenant_async,
) )
if typing.TYPE_CHECKING:
from apps.user_management.models import Organization, User
logger = logging.getLogger(__name__) 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. 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. 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( link, _ = client.get_slack_oauth_link(
org.stack_id, org.stack_id,
user.user_id, user.user_id,
urljoin(org.web_link, "settings?tab=ChatOps&chatOpsTab=Slack"), UIURLBuilder(org).settings("?tab=ChatOps&chatOpsTab=Slack"),
APP_TYPE_ONCALL, APP_TYPE_ONCALL,
) )
return link return link
@ -44,13 +48,13 @@ def get_installation_link_from_chatops_proxy(user) -> typing.Optional[str]:
raise api_exc 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) client = ChatopsProxyAPIClient(settings.ONCALL_GATEWAY_URL, settings.ONCALL_GATEWAY_API_TOKEN)
slack_installation, _ = client.get_oauth_installation(stack_id, PROVIDER_TYPE_SLACK) slack_installation, _ = client.get_oauth_installation(stack_id, PROVIDER_TYPE_SLACK)
return slack_installation.oauth_response 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 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. to make sure that tenant is registered.

View 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

View 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)

View file

@ -1,14 +1,19 @@
import logging import logging
import random import random
from urllib.parse import urljoin import typing
import requests import requests
from django.conf import settings from django.conf import settings
from rest_framework import status from rest_framework import status
from apps.base.utils import live_settings 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 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__) logger = logging.getLogger(__name__)
@ -102,9 +107,13 @@ def send_cloud_heartbeat():
logger.info("Finish 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: if connector is None:
return None return None
if heartbeat is None: if heartbeat is None:
return 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)

View file

@ -12,9 +12,9 @@ class CloudUserSerializer(serializers.ModelSerializer):
model = User model = User
fields = ["cloud_data"] fields = ["cloud_data"]
def get_cloud_data(self, obj): def get_cloud_data(self, obj: User):
connector = CloudConnector.objects.filter().first() connector = CloudConnector.objects.filter().first()
cloud_user_identity = CloudUserIdentity.objects.filter(email=obj.email).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} cloud_data = {"status": status, "link": link}
return cloud_data return cloud_data

View file

@ -1,11 +1,15 @@
import logging import logging
from urllib.parse import urljoin import typing
from django.utils import timezone from django.utils import timezone
from apps.grafana_plugin.ui_url_builder import UIURLBuilder
from apps.oss_installation.constants import CloudSyncStatus from apps.oss_installation.constants import CloudSyncStatus
from apps.schedules.ical_utils import list_users_to_notify_from_ical_for_period 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__) logger = logging.getLogger(__name__)
@ -67,7 +71,7 @@ def active_oss_users_count():
return len(unique_active_users) return len(unique_active_users)
def cloud_user_identity_status(connector, identity): def cloud_user_identity_status(org: "Organization", connector, identity):
link = None link = None
if connector is None: if connector is None:
status = CloudSyncStatus.NOT_SYNCED status = CloudSyncStatus.NOT_SYNCED
@ -80,5 +84,5 @@ def cloud_user_identity_status(connector, identity):
else: else:
status = CloudSyncStatus.SYNCED_PHONE_NOT_VERIFIED 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 return status, link

View file

@ -26,7 +26,7 @@ class CloudConnectionView(APIView):
"cloud_connection_status": connector is not None, "cloud_connection_status": connector is not None,
"cloud_notifications_enabled": live_settings.GRAFANA_CLOUD_NOTIFICATIONS_ENABLED, "cloud_notifications_enabled": live_settings.GRAFANA_CLOUD_NOTIFICATIONS_ENABLED,
"cloud_heartbeat_enabled": live_settings.GRAFANA_CLOUD_ONCALL_HEARTBEAT_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, "cloud_heartbeat_status": heartbeat is not None and heartbeat.success,
} }
return Response(response) return Response(response)

View file

@ -24,7 +24,7 @@ class CloudHeartbeatView(APIView):
return Response(status=status.HTTP_400_BAD_REQUEST, data={"detail": "Cloud heartbeat already exists"}) return Response(status=status.HTTP_400_BAD_REQUEST, data={"detail": "Cloud heartbeat already exists"})
except CloudHeartbeat.DoesNotExist: except CloudHeartbeat.DoesNotExist:
heartbeat = setup_heartbeat_integration() 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}) return Response(status=status.HTTP_200_OK, data={"link": link})
else: else:
return Response(status=status.HTTP_400_BAD_REQUEST, data={"detail": "Grafana Cloud is not connected"}) return Response(status=status.HTTP_400_BAD_REQUEST, data={"detail": "Grafana Cloud is not connected"})

View file

@ -57,7 +57,7 @@ class CloudUsersView(CloudUsersPagination, APIView):
for user in results: for user in results:
cloud_identity = cloud_identities.get(user.email, None) 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( data.append(
{ {
"id": user.public_primary_key, "id": user.public_primary_key,

View file

@ -7,6 +7,7 @@ from rest_framework.test import APIClient
from apps.alerts.models import AlertGroup from apps.alerts.models import AlertGroup
from apps.alerts.paging import DirectPagingAlertGroupResolvedError, DirectPagingUserTeamValidationError from apps.alerts.paging import DirectPagingAlertGroupResolvedError, DirectPagingUserTeamValidationError
from common.constants.plugin_ids import PluginID
title = "Custom title" title = "Custom title"
message = "Testing escalation with new alert group" message = "Testing escalation with new alert group"
@ -70,7 +71,7 @@ def test_escalation_new_alert_group(
"slack": None, "slack": None,
"slack_app": None, "slack_app": None,
"telegram": 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, "silenced_at": None,
"last_alert": { "last_alert": {

View file

@ -18,6 +18,7 @@ from polymorphic.managers import PolymorphicManager
from polymorphic.models import PolymorphicModel from polymorphic.models import PolymorphicModel
from polymorphic.query import PolymorphicQuerySet from polymorphic.query import PolymorphicQuerySet
from apps.grafana_plugin.ui_url_builder import UIURLBuilder
from apps.schedules.constants import ( from apps.schedules.constants import (
EXPORT_WINDOW_DAYS_AFTER, EXPORT_WINDOW_DAYS_AFTER,
EXPORT_WINDOW_DAYS_BEFORE, EXPORT_WINDOW_DAYS_BEFORE,
@ -247,11 +248,15 @@ class OnCallSchedule(PolymorphicModel):
@property @property
def web_page_link(self) -> str: def web_page_link(self) -> str:
return f"{self.organization.web_link}schedules" return UIURLBuilder(self.organization).schedules()
@property @property
def web_detail_page_link(self) -> str: 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]]: 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""" """Returns list of calendars. Primary calendar should always be the first"""

View file

@ -171,10 +171,6 @@ class ShiftSwapRequest(models.Model):
""" """
return self.schedule.channel return self.schedule.channel
@property
def schedule_slack_url(self) -> str:
return f"<{self.schedule.web_detail_page_link}|{self.schedule.name}>"
@property @property
def organization(self) -> "Organization": def organization(self) -> "Organization":
return self.schedule.organization return self.schedule.organization
@ -183,11 +179,6 @@ class ShiftSwapRequest(models.Model):
def possible_benefactors(self) -> QuerySet["User"]: def possible_benefactors(self) -> QuerySet["User"]:
return self.schedule.related_users().exclude(pk=self.beneficiary_id) 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): def delete(self):
self.deleted_at = timezone.now() self.deleted_at = timezone.now()
self.save() self.save()

View file

@ -2,7 +2,6 @@ import enum
import json import json
import logging import logging
import typing import typing
from urllib.parse import urljoin
from uuid import uuid4 from uuid import uuid4
from django.conf import settings from django.conf import settings
@ -12,6 +11,7 @@ from rest_framework.response import Response
from apps.alerts.models import AlertReceiveChannel from apps.alerts.models import AlertReceiveChannel
from apps.alerts.paging import DirectPagingUserTeamValidationError, UserNotifications, direct_paging, user_is_oncall from apps.alerts.paging import DirectPagingUserTeamValidationError, UserNotifications, direct_paging, user_is_oncall
from apps.api.permissions import RBACPermission, user_is_authorized 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.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.chatops_proxy_routing import make_private_metadata, make_value
from apps.slack.constants import DIVIDER, PRIVATE_METADATA_MAX_LENGTH 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: if slack_team_identity.needs_reinstall:
organizations = _get_available_organizations(slack_team_identity, slack_user_identity) organizations = _get_available_organizations(slack_team_identity, slack_user_identity)
if len(organizations) == 1: if len(organizations) == 1:
# Provide a link to web if user has access only to one organization # Provide a link to web if user has access only to one organization
link = urljoin(organizations[0].web_link, "settings?tab=ChatOps&chatOpsTab=Slack") link = UIURLBuilder(organizations[0]).settings("?tab=ChatOps&chatOpsTab=Slack")
else: else:
# Otherwise, provide a link to the documentation # Otherwise, provide a link to the documentation
link = ( link = (

View file

@ -215,7 +215,7 @@ class EditScheduleShiftNotifyStep(scenario_step.ScenarioStep):
"type": "section", "type": "section",
"text": { "text": {
"type": "mrkdwn", "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, "verbatim": True,
}, },
}, },

View file

@ -30,7 +30,7 @@ class BaseShiftSwapRequestStep(scenario_step.ScenarioStep):
pk = shift_swap_request.pk pk = shift_swap_request.pk
main_message_text = ( 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 " f"Your teammate {shift_swap_request.beneficiary.get_username_with_slack_verbal(True)} has submitted "
"a shift swap request." "a shift swap request."
) )
@ -262,7 +262,7 @@ class ShiftSwapRequestFollowUp(BaseShiftSwapRequestStep):
"text": { "text": {
"type": "mrkdwn", "type": "mrkdwn",
"text": ( "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 " f"still open and will start in {delta}. Jump back into the thread and accept it if "
"you're available!" "you're available!"
), ),

View file

@ -11,6 +11,7 @@ from apps.slack.scenarios.paging import OnPagingTeamChange, StartDirectPaging
from apps.slack.scenarios.schedules import EditScheduleShiftNotifyStep from apps.slack.scenarios.schedules import EditScheduleShiftNotifyStep
from apps.slack.scenarios.shift_swap_requests import AcceptShiftSwapRequestStep from apps.slack.scenarios.shift_swap_requests import AcceptShiftSwapRequestStep
from apps.slack.types import PayloadType from apps.slack.types import PayloadType
from common.constants.plugin_ids import PluginID
EVENT_TRIGGER_ID = "5333959822612.4122782784722.4734ff484b2ac4d36a185bb242ee9932" EVENT_TRIGGER_ID = "5333959822612.4122782784722.4734ff484b2ac4d36a185bb242ee9932"
WARNING_TEXT = ( 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( mock_open_warning_window_if_needed.assert_called_once_with(
event_payload, event_payload,
slack_team_identity, 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",
) )

View file

@ -42,7 +42,7 @@ class TestBaseShiftSwapRequestStep:
blocks = step._generate_blocks(ssr) blocks = step._generate_blocks(ssr)
assert blocks[0]["text"]["text"] == ( 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." f"Your teammate {beneficiary.get_username_with_slack_verbal()} has submitted a shift swap request."
) )

View file

@ -3,7 +3,6 @@ import hmac
import json import json
import logging import logging
from contextlib import suppress from contextlib import suppress
from urllib.parse import urljoin
from django.conf import settings from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist 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.auth_token.auth import PluginAuthentication
from apps.base.utils import live_settings from apps.base.utils import live_settings
from apps.chatops_proxy.utils import uninstall_slack as uninstall_slack_from_chatops_proxy 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.client import SlackClient
from apps.slack.errors import SlackAPIError from apps.slack.errors import SlackAPIError
from apps.slack.scenarios.alertgroup_appearance import STEPS_ROUTING as ALERTGROUP_APPEARANCE_ROUTING from apps.slack.scenarios.alertgroup_appearance import STEPS_ROUTING as ALERTGROUP_APPEARANCE_ROUTING
@ -293,7 +293,7 @@ class SlackEventApiEndpointView(APIView):
elif organization: elif organization:
user = slack_user_identity.get_user(organization) user = slack_user_identity.get_user(organization)
if not user: # SlackUserIdentity exists but not connected to any user in this 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( self._open_warning_window_if_needed(
payload, payload,
slack_team_identity, slack_team_identity,

View file

@ -1,5 +1,4 @@
import logging import logging
from urllib.parse import urljoin
from django.http import HttpResponse from django.http import HttpResponse
from django.shortcuts import redirect from django.shortcuts import redirect
@ -7,9 +6,10 @@ from rest_framework import status
from social_core import exceptions from social_core import exceptions
from social_django.middleware import SocialAuthExceptionMiddleware 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.backends import LoginSlackOAuth2V2
from apps.social_auth.exceptions import InstallMultiRegionSlackException 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__) logger = logging.getLogger(__name__)
@ -17,28 +17,24 @@ logger = logging.getLogger(__name__)
class SocialAuthAuthCanceledExceptionMiddleware(SocialAuthExceptionMiddleware): class SocialAuthAuthCanceledExceptionMiddleware(SocialAuthExceptionMiddleware):
def process_exception(self, request, exception): def process_exception(self, request, exception):
backend = getattr(exception, "backend", None) 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): 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: if exception:
logger.warning(f"SocialAuthAuthCanceledExceptionMiddleware.process_exception: {exception}") logger.warning(f"SocialAuthAuthCanceledExceptionMiddleware.process_exception: {exception}")
if isinstance(exception, exceptions.AuthCanceled): if isinstance(exception, exceptions.AuthCanceled):
# if user canceled authentication, redirect them to the previous page using the same link # if user canceled authentication, redirect them to the previous page using the same link
# as we used to redirect after auth/install # as we used to redirect after auth/install
url_to_redirect = urljoin(request.user.organization.grafana_url, redirect_to) return redirect(url_builder_function())
return redirect(url_to_redirect)
elif isinstance(exception, exceptions.AuthFailed): elif isinstance(exception, exceptions.AuthFailed):
# if authentication was failed, redirect user to the plugin page using the same link # 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 # as we used to redirect after auth/install with error flag
url_to_redirect = urljoin( return redirect(url_builder_function(f"?slack_error={SLACK_AUTH_FAILED}"))
request.user.organization.grafana_url, f"{redirect_to}&slack_error={SLACK_AUTH_FAILED}"
)
return redirect(url_to_redirect)
elif isinstance(exception, KeyError) and REDIRECT_AFTER_SLACK_INSTALL in exception.args: elif isinstance(exception, KeyError) and REDIRECT_AFTER_SLACK_INSTALL in exception.args:
return HttpResponse(status=status.HTTP_401_UNAUTHORIZED) return HttpResponse(status=status.HTTP_401_UNAUTHORIZED)
elif isinstance(exception, InstallMultiRegionSlackException): elif isinstance(exception, InstallMultiRegionSlackException):
REGION_ERROR = "region_error" return redirect(url_builder_function(f"?tab=Slack&slack_error={SLACK_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)

View file

@ -1,6 +1,5 @@
import logging import logging
import typing import typing
from urllib.parse import urljoin
import requests import requests
from django.contrib.auth import REDIRECT_FIELD_NAME 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 social_core.backends.base import BaseAuth
from apps.google.utils import user_granted_all_required_scopes 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.exceptions import GOOGLE_AUTH_MISSING_GRANTED_SCOPE_ERROR
from apps.social_auth.types import GoogleOauth2Response from apps.social_auth.types import GoogleOauth2Response
from apps.user_management.models import Organization, User from apps.user_management.models import Organization, User
@ -32,10 +32,9 @@ def connect_user_to_google(
f"granted_scopes={granted_scopes}" f"granted_scopes={granted_scopes}"
) )
base_url_to_redirect = urljoin(organization.grafana_url, "/a/grafana-oncall-app/users/me") strategy.session[REDIRECT_FIELD_NAME] = UIURLBuilder(organization).user_profile(
strategy.session[ f"?google_error={GOOGLE_AUTH_MISSING_GRANTED_SCOPE_ERROR}"
REDIRECT_FIELD_NAME )
] = f"{base_url_to_redirect}?google_error={GOOGLE_AUTH_MISSING_GRANTED_SCOPE_ERROR}"
return HttpResponse(status=status.HTTP_400_BAD_REQUEST) return HttpResponse(status=status.HTTP_400_BAD_REQUEST)

View file

@ -1,5 +1,4 @@
import logging import logging
from urllib.parse import urljoin
from django.conf import settings from django.conf import settings
from django.contrib.auth import REDIRECT_FIELD_NAME from django.contrib.auth import REDIRECT_FIELD_NAME
@ -7,6 +6,7 @@ from django.http import HttpResponse
from rest_framework import status from rest_framework import status
from apps.chatops_proxy.utils import can_link_slack_team, link_slack_team 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.slack.installation import SlackInstallationExc, install_slack_integration
from apps.social_auth.backends import SLACK_INSTALLATION_BACKEND from apps.social_auth.backends import SLACK_INSTALLATION_BACKEND
from apps.social_auth.exceptions import InstallMultiRegionSlackException 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_team_identity = organization.slack_team_identity
slack_user_id = response["authed_user"]["id"] slack_user_id = response["authed_user"]["id"]
url_builder = UIURLBuilder(organization)
redirect_to = "/a/grafana-oncall-app/users/me/"
base_url_to_redirect = urljoin(organization.grafana_url, redirect_to)
if slack_team_identity is None: if slack_team_identity is None:
# means that organization doesn't have slack integration, so user cannot connect their account to slack # 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"]: if slack_team_identity.slack_id != response["team"]["id"]:
# means that user authed in another slack workspace that is not connected to their organization # 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 # 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_builder.user_profile(
strategy.session[REDIRECT_FIELD_NAME] = url f"?slack_error={SLACK_AUTH_WRONG_WORKSPACE_ERROR}",
)
return HttpResponse(status=status.HTTP_400_BAD_REQUEST) return HttpResponse(status=status.HTTP_400_BAD_REQUEST)
if organization.users.filter(slack_user_identity__slack_id=slack_user_id).exists(): 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 # 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_builder.user_profile(
strategy.session[REDIRECT_FIELD_NAME] = url f"?slack_error={SLACK_AUTH_SLACK_USER_ALREADY_CONNECTED_ERROR}",
)
return HttpResponse(status=status.HTTP_400_BAD_REQUEST) return HttpResponse(status=status.HTTP_400_BAD_REQUEST)
# at this point everything is correct and we can create the SlackUserIdentity # at this point everything is correct and we can create the SlackUserIdentity

View file

@ -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),
),
]

View file

@ -1,7 +1,6 @@
import logging import logging
import typing import typing
import uuid import uuid
from urllib.parse import urljoin
from django.conf import settings from django.conf import settings
from django.core.validators import MinLengthValidator from django.core.validators import MinLengthValidator
@ -16,6 +15,7 @@ from apps.chatops_proxy.utils import (
unlink_slack_team, unlink_slack_team,
unregister_oncall_tenant, unregister_oncall_tenant,
) )
from apps.grafana_plugin.ui_url_builder import UIURLBuilder
from apps.user_management.subscription_strategy import FreePublicBetaSubscriptionStrategy from apps.user_management.subscription_strategy import FreePublicBetaSubscriptionStrategy
from apps.user_management.types import AlertGroupTableColumn from apps.user_management.types import AlertGroupTableColumn
from common.insight_log import ChatOpsEvent, ChatOpsTypePlug, write_chatops_insight_log 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_rbac_permissions_enabled = models.BooleanField(default=False)
is_grafana_incident_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_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) 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) grafana_incident_backend_url = models.CharField(max_length=300, null=True, default=None)
@ -344,14 +345,12 @@ class Organization(MaintainableObject):
.distinct() .distinct()
) )
@property
def web_link(self):
return urljoin(self.grafana_url, "a/grafana-oncall-app/")
@property @property
def web_link_with_uuid(self): 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 @classmethod
def __str__(self): def __str__(self):

View file

@ -3,6 +3,7 @@ REDIRECT_AFTER_SLACK_INSTALL = "redirect_after_slack_install"
SLACK_AUTH_WRONG_WORKSPACE_ERROR = "wrong_workspace" SLACK_AUTH_WRONG_WORKSPACE_ERROR = "wrong_workspace"
SLACK_AUTH_SLACK_USER_ALREADY_CONNECTED_ERROR = "user_already_connected" SLACK_AUTH_SLACK_USER_ALREADY_CONNECTED_ERROR = "user_already_connected"
SLACK_AUTH_FAILED = "auth_failed" SLACK_AUTH_FAILED = "auth_failed"
SLACK_REGION_ERROR = "region_error"
# Example of a slack oauth response to be used in tests. # Example of a slack oauth response to be used in tests.