Skip to content

feat(dashboards): Add GET endpoint for starred dashboards #93812

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions src/sentry/api/endpoints/organization_dashboards_starred.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,54 @@
from sentry.api.api_publish_status import ApiPublishStatus
from sentry.api.base import region_silo_endpoint
from sentry.api.bases.organization import OrganizationEndpoint, OrganizationPermission
from sentry.api.paginator import GenericOffsetPaginator
from sentry.api.serializers.base import serialize
from sentry.api.serializers.models.dashboard import DashboardListSerializer
from sentry.api.serializers.rest_framework.dashboard import DashboardStarredOrderSerializer
from sentry.models.dashboard import DashboardFavoriteUser
from sentry.models.organization import Organization


class MemberPermission(OrganizationPermission):
scope_map = {
"GET": ["member:read", "member:write"],
"PUT": ["member:read", "member:write"],
}


@region_silo_endpoint
class OrganizationDashboardsStarredEndpoint(OrganizationEndpoint):
publish_status = {"GET": ApiPublishStatus.PRIVATE}
owner = ApiOwner.PERFORMANCE
permission_classes = (MemberPermission,)

def has_feature(self, organization, request):
return features.has(
"organizations:dashboards-starred-reordering", organization, actor=request.user
)

def get(self, request: Request, organization: Organization) -> Response:
if not request.user.is_authenticated:
return Response(status=status.HTTP_400_BAD_REQUEST)

if not self.has_feature(organization, request):
return self.respond(status=status.HTTP_404_NOT_FOUND)

favorites = DashboardFavoriteUser.objects.get_favorite_dashboards(
organization=organization, user_id=request.user.id
).select_related("dashboard")

def data_fn(offset, limit):
return [favorite.dashboard for favorite in favorites[offset : offset + limit]]

return self.paginate(
request=request,
paginator=GenericOffsetPaginator(data_fn=data_fn),
on_results=lambda x: serialize(x, request.user, serializer=DashboardListSerializer()),
default_per_page=25,
)


@region_silo_endpoint
class OrganizationDashboardsStarredOrderEndpoint(OrganizationEndpoint):
publish_status = {"PUT": ApiPublishStatus.PRIVATE}
Expand Down
16 changes: 11 additions & 5 deletions src/sentry/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
)
from sentry.api.endpoints.organization_auth_tokens import OrganizationAuthTokensEndpoint
from sentry.api.endpoints.organization_dashboards_starred import (
OrganizationDashboardsStarredEndpoint,
OrganizationDashboardsStarredOrderEndpoint,
)
from sentry.api.endpoints.organization_events_anomalies import OrganizationEventsAnomaliesEndpoint
Expand Down Expand Up @@ -1389,6 +1390,16 @@ def create_group_urls(name_prefix: str) -> list[URLPattern | URLResolver]:
OrganizationDashboardWidgetDetailsEndpoint.as_view(),
name="sentry-api-0-organization-dashboard-widget-details",
),
re_path(
r"^(?P<organization_id_or_slug>[^/]+)/dashboards/starred/$",
OrganizationDashboardsStarredEndpoint.as_view(),
name="sentry-api-0-organization-dashboard-starred",
),
re_path(
r"^(?P<organization_id_or_slug>[^/]+)/dashboards/starred/order/$",
OrganizationDashboardsStarredOrderEndpoint.as_view(),
name="sentry-api-0-organization-dashboard-starred-order",
),
re_path(
r"^(?P<organization_id_or_slug>[^/]+)/dashboards/(?P<dashboard_id>[^/]+)/$",
OrganizationDashboardDetailsEndpoint.as_view(),
Expand All @@ -1399,11 +1410,6 @@ def create_group_urls(name_prefix: str) -> list[URLPattern | URLResolver]:
OrganizationDashboardVisitEndpoint.as_view(),
name="sentry-api-0-organization-dashboard-visit",
),
re_path(
r"^(?P<organization_id_or_slug>[^/]+)/dashboards/starred/order/$",
OrganizationDashboardsStarredOrderEndpoint.as_view(),
name="sentry-api-0-organization-dashboard-starred-order",
),
re_path(
r"^(?P<organization_id_or_slug>[^/]+)/dashboards/(?P<dashboard_id>[^/]+)/favorite/$",
OrganizationDashboardFavoriteEndpoint.as_view(),
Expand Down
11 changes: 11 additions & 0 deletions src/sentry/models/dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

from django.db import models, router, transaction
from django.db.models import UniqueConstraint
from django.db.models.query import QuerySet
from django.utils import timezone

from sentry.backup.scopes import RelocationScope
Expand Down Expand Up @@ -50,6 +51,16 @@ def get_last_position(self, organization: Organization, user_id: int) -> int:
return last_favorite_dashboard.position
return 0

def get_favorite_dashboards(
self, organization: Organization, user_id: int
) -> QuerySet[DashboardFavoriteUser]:
"""
Returns all favorited dashboards for a user in an organization.
"""
return self.filter(organization=organization, user_id=user_id).order_by(
"position", "dashboard__title"
)

def get_favorite_dashboard(
self, organization: Organization, user_id: int, dashboard: Dashboard
) -> DashboardFavoriteUser | None:
Expand Down
98 changes: 63 additions & 35 deletions tests/sentry/api/endpoints/test_organization_dashboards_starred.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,60 @@
from sentry.testutils.cases import OrganizationDashboardWidgetTestCase


class OrganizationDashboardsStarredOrderTest(OrganizationDashboardWidgetTestCase):
class StarredDashboardTestCase(OrganizationDashboardWidgetTestCase):
def create_dashboard_favorite(self, dashboard, user, organization, position):
DashboardFavoriteUser.objects.create(
dashboard=dashboard, user_id=user.id, organization=organization, position=position
)

def do_request(self, *args, **kwargs):
with self.feature("organizations:dashboards-starred-reordering"):
return super().do_request(*args, **kwargs)


class OrganizationDashboardsStarredTest(StarredDashboardTestCase):
def setUp(self):
super().setUp()
self.login_as(self.user)
self.url = reverse(
"sentry-api-0-organization-dashboard-starred-order",
"sentry-api-0-organization-dashboard-starred",
kwargs={"organization_id_or_slug": self.organization.slug},
)
self.dashboard_1 = self.create_dashboard(title="Dashboard 1")
self.dashboard_2 = self.create_dashboard(title="Dashboard 2")
self.dashboard_3 = self.create_dashboard(title="Dashboard 3")

def create_dashboard_favorite(self, dashboard, user, organization, position):
DashboardFavoriteUser.objects.create(
dashboard=dashboard, user_id=user.id, organization=organization, position=position
def test_get_favorite_dashboards(self):
self.create_dashboard_favorite(self.dashboard_1, self.user, self.organization, 2)
self.create_dashboard_favorite(self.dashboard_2, self.user, self.organization, 0)
self.create_dashboard_favorite(self.dashboard_3, self.user, self.organization, 1)

# Add a dashboard starred by another user to verify that it is not returned
other_user = self.create_user("[email protected]")
other_dashboard = self.create_dashboard(title="Other Dashboard")
self.create_dashboard_favorite(other_dashboard, other_user, self.organization, 0)

response = self.do_request("get", self.url)
assert response.status_code == 200
assert len(response.data) == 3
assert [int(dashboard["id"]) for dashboard in response.data] == [
self.dashboard_2.id,
self.dashboard_3.id,
self.dashboard_1.id,
]


class OrganizationDashboardsStarredOrderTest(StarredDashboardTestCase):
def setUp(self):
super().setUp()
self.login_as(self.user)
self.url = reverse(
"sentry-api-0-organization-dashboard-starred-order",
kwargs={"organization_id_or_slug": self.organization.slug},
)
self.dashboard_1 = self.create_dashboard(title="Dashboard 1")
self.dashboard_2 = self.create_dashboard(title="Dashboard 2")
self.dashboard_3 = self.create_dashboard(title="Dashboard 3")

def test_reorder_dashboards(self):
self.create_dashboard_favorite(self.dashboard_1, self.user, self.organization, 0)
Expand All @@ -40,14 +78,11 @@ def test_reorder_dashboards(self):
]

# Reorder the favorited dashboards
with self.feature("organizations:dashboards-starred-reordering"):
response = self.do_request(
"put",
self.url,
data={
"dashboard_ids": [self.dashboard_3.id, self.dashboard_1.id, self.dashboard_2.id]
},
)
response = self.do_request(
"put",
self.url,
data={"dashboard_ids": [self.dashboard_3.id, self.dashboard_1.id, self.dashboard_2.id]},
)
assert response.status_code == 204

assert list(
Expand All @@ -67,14 +102,11 @@ def test_throws_an_error_if_dashboard_ids_are_not_unique(self):
self.create_dashboard_favorite(self.dashboard_2, self.user, self.organization, 1)
self.create_dashboard_favorite(self.dashboard_3, self.user, self.organization, 2)

with self.feature("organizations:dashboards-starred-reordering"):
response = self.do_request(
"put",
self.url,
data={
"dashboard_ids": [self.dashboard_1.id, self.dashboard_1.id, self.dashboard_2.id]
},
)
response = self.do_request(
"put",
self.url,
data={"dashboard_ids": [self.dashboard_1.id, self.dashboard_1.id, self.dashboard_2.id]},
)
assert response.status_code == 400
assert response.data == {
"dashboard_ids": ["Single dashboard cannot take up multiple positions"]
Expand All @@ -85,12 +117,11 @@ def test_throws_an_error_if_reordered_dashboard_ids_are_not_complete(self):
self.create_dashboard_favorite(self.dashboard_2, self.user, self.organization, 1)
self.create_dashboard_favorite(self.dashboard_3, self.user, self.organization, 2)

with self.feature("organizations:dashboards-starred-reordering"):
response = self.do_request(
"put",
self.url,
data={"dashboard_ids": [self.dashboard_1.id, self.dashboard_2.id]},
)
response = self.do_request(
"put",
self.url,
data={"dashboard_ids": [self.dashboard_1.id, self.dashboard_2.id]},
)
assert response.status_code == 400
assert response.data == {
"detail": ErrorDetail(
Expand All @@ -104,14 +135,11 @@ def test_allows_reordering_even_if_no_initial_positions(self):
self.create_dashboard_favorite(self.dashboard_2, self.user, self.organization, 1)
self.create_dashboard_favorite(self.dashboard_3, self.user, self.organization, 2)

with self.feature("organizations:dashboards-starred-reordering"):
response = self.do_request(
"put",
self.url,
data={
"dashboard_ids": [self.dashboard_3.id, self.dashboard_1.id, self.dashboard_2.id]
},
)
response = self.do_request(
"put",
self.url,
data={"dashboard_ids": [self.dashboard_3.id, self.dashboard_1.id, self.dashboard_2.id]},
)
assert response.status_code == 204

assert list(
Expand Down
Loading