Skip to content

Commit

Permalink
ref(dashboards): Be able to create widgets when creating a new dashbo…
Browse files Browse the repository at this point in the history
…ard (#22314)
  • Loading branch information
dashed committed Nov 30, 2020
1 parent f10fdf3 commit ed04809
Show file tree
Hide file tree
Showing 7 changed files with 113 additions and 48 deletions.
19 changes: 6 additions & 13 deletions src/sentry/api/endpoints/organization_dashboards.py
@@ -1,19 +1,15 @@
from __future__ import absolute_import

from django.db import IntegrityError, transaction
from rest_framework import serializers

from sentry.api.bases.organization import OrganizationEndpoint
from sentry.api.paginator import OffsetPaginator
from sentry.api.serializers import serialize
from sentry.api.serializers.rest_framework import DashboardSerializer
from sentry.models import Dashboard
from rest_framework.response import Response


class DashboardSerializer(serializers.Serializer):
title = serializers.CharField(required=True)


class OrganizationDashboardsEndpoint(OrganizationEndpoint):
def get(self, request, organization):
"""
Expand Down Expand Up @@ -47,19 +43,16 @@ def post(self, request, organization):
dashboards belongs to.
:param string title: the title of the dashboard.
"""
serializer = DashboardSerializer(data=request.data)
serializer = DashboardSerializer(
data=request.data, context={"organization_id": organization.id, "request": request}
)

if not serializer.is_valid():
return Response(serializer.errors, status=400)

result = serializer.validated_data

try:
with transaction.atomic():
dashboard = Dashboard.objects.create(
organization_id=organization.id, title=result["title"], created_by=request.user
)
dashboard = serializer.save()
return Response(serialize(dashboard, request.user), status=201)
except IntegrityError:
return Response("Dashboard title already taken", status=409)

return Response(serialize(dashboard, request.user), status=201)
23 changes: 23 additions & 0 deletions src/sentry/api/serializers/rest_framework/dashboard.py
Expand Up @@ -13,6 +13,7 @@
DashboardWidget,
DashboardWidgetQuery,
DashboardWidgetDisplayTypes,
Dashboard,
)
from sentry.utils.dates import parse_stats_period

Expand Down Expand Up @@ -226,3 +227,25 @@ def update_widget_query(self, query, data, order):

def remove_missing_queries(self, widget_id, keep_ids):
DashboardWidgetQuery.objects.filter(widget_id=widget_id).exclude(id__in=keep_ids).delete()


class DashboardSerializer(DashboardDetailsSerializer):
title = serializers.CharField(required=True)

def create(self, validated_data):
"""
Create a dashboard, and create any widgets and their queries
Only call save() on this serializer from within a transaction or
bad things will happen
"""
self.instance = Dashboard.objects.create(
organization_id=self.context.get("organization_id"),
title=validated_data["title"],
created_by=self.context.get("request").user,
)

if "widgets" in validated_data:
self.update_widgets(self.instance, validated_data["widgets"])

return self.instance
4 changes: 3 additions & 1 deletion src/sentry/static/sentry/app/actionCreators/dashboards.tsx
Expand Up @@ -13,11 +13,13 @@ export function createDashboard(
orgId: string,
newDashboard: DashboardListItem
): Promise<OrgDashboardResponse> {
const {title, widgets} = newDashboard;

const promise: Promise<OrgDashboardResponse> = api.requestPromise(
`/organizations/${orgId}/dashboards/`,
{
method: 'POST',
data: {title: newDashboard.title},
data: {title, widgets},
}
);

Expand Down
5 changes: 4 additions & 1 deletion src/sentry/static/sentry/app/views/dashboardsV2/detail.tsx
Expand Up @@ -234,7 +234,10 @@ class DashboardDetail extends React.Component<Props, State> {
<Dashboard
dashboard={this.state.changesDashboard || dashboard}
organization={organization}
isEditing={this.state.dashboardState === 'edit'}
isEditing={
this.state.dashboardState === 'edit' ||
this.state.dashboardState === 'create'
}
onUpdate={this.onWidgetChange}
/>
</React.Fragment>
Expand Down
27 changes: 27 additions & 0 deletions src/sentry/testutils/cases.py
Expand Up @@ -76,6 +76,8 @@
Organization,
Dashboard,
DashboardWidgetQuery,
DashboardWidget,
DashboardWidgetDisplayTypes,
)
from sentry.plugins.base import plugins
from sentry.rules import EventState
Expand Down Expand Up @@ -996,3 +998,28 @@ def assert_widget_data(self, data, title, display_type, queries=None):
return

self.assert_widget_queries(data["id"], queries)

def assert_serialized_widget_query(self, data, widget_data_source):
if "id" in data:
assert data["id"] == six.text_type(widget_data_source.id)
if "name" in data:
assert data["name"] == widget_data_source.name
if "fields" in data:
assert data["fields"] == widget_data_source.fields
if "conditions" in data:
assert data["conditions"] == widget_data_source.conditions

def get_widgets(self, dashboard_id):
return DashboardWidget.objects.filter(dashboard_id=dashboard_id).order_by("order")

def assert_serialized_widget(self, data, expected_widget):
if "id" in data:
assert data["id"] == six.text_type(expected_widget.id)
if "title" in data:
assert data["title"] == expected_widget.title
if "interval" in data:
assert data["interval"] == expected_widget.interval
if "displayType" in data:
assert data["displayType"] == DashboardWidgetDisplayTypes.get_type_name(
expected_widget.display_type
)
25 changes: 0 additions & 25 deletions tests/sentry/api/endpoints/test_organization_dashboard_details.py
Expand Up @@ -58,33 +58,11 @@ def url(self, dashboard_id):
kwargs={"organization_slug": self.organization.slug, "dashboard_id": dashboard_id},
)

def assert_serialized_widget(self, data, expected_widget):
if "id" in data:
assert data["id"] == six.text_type(expected_widget.id)
if "title" in data:
assert data["title"] == expected_widget.title
if "interval" in data:
assert data["interval"] == expected_widget.interval
if "displayType" in data:
assert data["displayType"] == DashboardWidgetDisplayTypes.get_type_name(
expected_widget.display_type
)

def assert_serialized_dashboard(self, data, dashboard):
assert data["id"] == six.text_type(dashboard.id)
assert data["title"] == dashboard.title
assert data["createdBy"] == six.text_type(dashboard.created_by.id)

def assert_serialized_widget_query(self, data, widget_data_source):
if "id" in data:
assert data["id"] == six.text_type(widget_data_source.id)
if "name" in data:
assert data["name"] == widget_data_source.name
if "fields" in data:
assert data["fields"] == widget_data_source.fields
if "conditions" in data:
assert data["conditions"] == widget_data_source.conditions


class OrganizationDashboardDetailsGetTest(OrganizationDashboardDetailsTestCase):
def test_get(self):
Expand Down Expand Up @@ -147,9 +125,6 @@ def setUp(self):
)
self.widget_ids = [self.widget_1.id, self.widget_2.id, self.widget_3.id, self.widget_4.id]

def get_widgets(self, dashboard_id):
return DashboardWidget.objects.filter(dashboard_id=dashboard_id).order_by("order")

def get_widget_queries(self, widget):
return DashboardWidgetQuery.objects.filter(widget=widget).order_by("order")

Expand Down
58 changes: 50 additions & 8 deletions tests/sentry/api/endpoints/test_organization_dashboards.py
Expand Up @@ -4,21 +4,19 @@

from django.core.urlresolvers import reverse

from sentry.utils.compat import zip
from sentry.models import Dashboard
from sentry.testutils import APITestCase
from sentry.testutils import OrganizationDashboardWidgetTestCase


class OrganizationDashboardsTest(APITestCase):
class OrganizationDashboardsTest(OrganizationDashboardWidgetTestCase):
def setUp(self):
super(OrganizationDashboardsTest, self).setUp()
self.login_as(self.user)
self.url = reverse(
"sentry-api-0-organization-dashboards",
kwargs={"organization_slug": self.organization.slug},
)
self.dashboard_1 = Dashboard.objects.create(
title="Dashboard 1", created_by=self.user, organization=self.organization
)
self.dashboard_2 = Dashboard.objects.create(
title="Dashboard 2", created_by=self.user, organization=self.organization
)
Expand All @@ -33,7 +31,7 @@ def test_get(self):
assert response.status_code == 200, response.content
assert len(response.data) == 2

self.assert_equal_dashboards(self.dashboard_1, response.data[0])
self.assert_equal_dashboards(self.dashboard, response.data[0])
self.assert_equal_dashboards(self.dashboard_2, response.data[1])

def test_post(self):
Expand All @@ -44,14 +42,58 @@ def test_post(self):
)
assert dashboard.created_by == self.user

def test_post_with_widgets(self):
data = {
"title": "Dashboard from Post",
"widgets": [
{
"displayType": "line",
"interval": "5m",
"title": "Transaction count()",
"queries": [
{
"name": "Transactions",
"fields": ["count()"],
"conditions": "event.type:transaction",
}
],
},
{
"displayType": "bar",
"interval": "5m",
"title": "Error count()",
"queries": [
{"name": "Errors", "fields": ["count()"], "conditions": "event.type:error"}
],
},
],
}

response = self.client.post(self.url, data=data)
assert response.status_code == 201
dashboard = Dashboard.objects.get(
organization=self.organization, title="Dashboard from Post"
)
assert dashboard.created_by == self.user

widgets = self.get_widgets(dashboard.id)
assert len(widgets) == 2

for expected_widget, actual_widget in zip(data["widgets"], widgets):
self.assert_serialized_widget(expected_widget, actual_widget)

queries = actual_widget.dashboardwidgetquery_set.all()
for expected_query, actual_query in zip(expected_widget["queries"], queries):
self.assert_serialized_widget_query(expected_query, actual_query)

def test_query(self):
dashboard = Dashboard.objects.create(
title="Dashboard 11", created_by=self.user, organization=self.organization
)
response = self.client.get(self.url, data={"query": "1"})
assert response.status_code == 200, response.content
assert len(response.data) == 2
self.assert_equal_dashboards(self.dashboard_1, response.data[0])
self.assert_equal_dashboards(self.dashboard, response.data[0])
self.assert_equal_dashboards(dashboard, response.data[1])

def test_query_no_results(self):
Expand All @@ -64,6 +106,6 @@ def test_invalid_data(self):
assert response.status_code == 400

def test_integrity_error(self):
response = self.client.post(self.url, data={"title": self.dashboard_1.title})
response = self.client.post(self.url, data={"title": self.dashboard.title})
assert response.status_code == 409
assert response.data == "Dashboard title already taken"

0 comments on commit ed04809

Please sign in to comment.