mirror of
https://github.com/valitydev/redash.git
synced 2024-11-06 09:05:17 +00:00
Move widget position logic migration to the backend and remove some unused code.
Closes #2218.
This commit is contained in:
parent
5dde17e421
commit
33b4c7c4c3
@ -2,32 +2,13 @@ import * as _ from 'underscore';
|
||||
|
||||
function Dashboard($resource, $http, currentUser, Widget, dashboardGridOptions) {
|
||||
function prepareDashboardWidgets(widgets) {
|
||||
if (_.isArray(widgets) && (widgets.length > 0) && _.isArray(widgets[0])) {
|
||||
// Dashboard v1 processing
|
||||
// v1 dashboard has two columns, and widget can occupy one of them or both;
|
||||
// this means, that there can be at most two widgets per row.
|
||||
// Here we will map gridster columns and rows to v1-style grid
|
||||
const dashboardV1ColumnSize = Math.round(dashboardGridOptions.columns / 2);
|
||||
widgets = _.map(
|
||||
widgets,
|
||||
(row, rowIndex) => _.map(row, (widget, widgetIndex) => {
|
||||
widget.options = widget.options || {};
|
||||
widget.options.position = _.extend({}, {
|
||||
row: rowIndex,
|
||||
col: widgetIndex * dashboardV1ColumnSize,
|
||||
sizeX: dashboardV1ColumnSize * widget.width,
|
||||
// do not set sizeY - let widget to use defaults for visualization
|
||||
}, widget.options.position);
|
||||
return widget;
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return _.map(_.flatten(widgets), widget => new Widget(widget));
|
||||
return widgets.map(widget => new Widget(widget));
|
||||
}
|
||||
|
||||
function transformSingle(dashboard) {
|
||||
dashboard.widgets = prepareDashboardWidgets(dashboard.widgets);
|
||||
if (dashboard.widgets) {
|
||||
dashboard.widgets = prepareDashboardWidgets(dashboard.widgets);
|
||||
}
|
||||
dashboard.publicAccessEnabled = dashboard.public_url !== undefined;
|
||||
}
|
||||
|
||||
|
@ -270,7 +270,7 @@ export default function init(ngModule) {
|
||||
columnMapping: {},
|
||||
defaultColumns: 3,
|
||||
defaultRows: 8,
|
||||
minColumns: 2,
|
||||
minColumns: 1,
|
||||
minRows: 5,
|
||||
};
|
||||
|
||||
|
69
migrations/versions/969126bd800f_.py
Normal file
69
migrations/versions/969126bd800f_.py
Normal file
@ -0,0 +1,69 @@
|
||||
"""Update widget's position data based on dashboard layout.
|
||||
|
||||
Revision ID: 969126bd800f
|
||||
Revises: 6b5be7e0a0ef
|
||||
Create Date: 2018-01-31 15:20:30.396533
|
||||
|
||||
"""
|
||||
import json
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
from redash.models import Dashboard, Widget, db
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '969126bd800f'
|
||||
down_revision = '6b5be7e0a0ef'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# Update widgets position data:
|
||||
column_size = 3
|
||||
print "Updating dashboards position data:"
|
||||
for dashboard in Dashboard.query:
|
||||
print " Updating dashboard: {}".format(dashboard.id)
|
||||
layout = json.loads(dashboard.layout)
|
||||
|
||||
print " Building widgets map:"
|
||||
widgets = {}
|
||||
for w in dashboard.widgets:
|
||||
print " Widget: {}".format(w.id)
|
||||
widgets[w.id] = w
|
||||
|
||||
print " Iterating over layout:"
|
||||
for row_index, row in enumerate(layout):
|
||||
print " Row: {} - {}".format(row_index, row)
|
||||
if row is None:
|
||||
continue
|
||||
|
||||
for column_index, widget_id in enumerate(row):
|
||||
print " Column: {} - {}".format(column_index, widget_id)
|
||||
widget = widgets.get(widget_id)
|
||||
|
||||
if widget is None:
|
||||
continue
|
||||
|
||||
options = json.loads(widget.options) or {}
|
||||
options['position'] = {
|
||||
"row": row_index,
|
||||
"col": column_index * column_size,
|
||||
"sizeX": column_size * widget.width
|
||||
}
|
||||
|
||||
widget.options = json.dumps(options)
|
||||
|
||||
db.session.add(widget)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
# Remove legacy columns no longer in use.
|
||||
op.drop_column('widgets', 'type')
|
||||
op.drop_column('widgets', 'query_id')
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.add_column('widgets', sa.Column('query_id', sa.INTEGER(), autoincrement=False, nullable=True))
|
||||
op.add_column('widgets', sa.Column('type', sa.VARCHAR(length=100), autoincrement=False, nullable=True))
|
@ -21,9 +21,6 @@ class WidgetListResource(BaseResource):
|
||||
:<json number width: Width for widget display
|
||||
|
||||
:>json object widget: The created widget
|
||||
:>json array layout: The new layout of the dashboard this widget was added to
|
||||
:>json boolean new_row: Whether this widget was added on a new row or not
|
||||
:>json number version: The revision number of the dashboard
|
||||
"""
|
||||
widget_properties = request.get_json(force=True)
|
||||
dashboard = models.Dashboard.get_by_id_and_org(widget_properties.pop('dashboard_id'), self.current_org)
|
||||
@ -46,25 +43,8 @@ class WidgetListResource(BaseResource):
|
||||
models.db.session.add(widget)
|
||||
models.db.session.commit()
|
||||
|
||||
layout = json.loads(widget.dashboard.layout)
|
||||
new_row = True
|
||||
|
||||
if len(layout) == 0 or widget.width == 2:
|
||||
layout.append([widget.id])
|
||||
elif len(layout[-1]) == 1:
|
||||
neighbour_widget = models.Widget.query.get(layout[-1][0])
|
||||
if neighbour_widget.width == 1:
|
||||
layout[-1].append(widget.id)
|
||||
new_row = False
|
||||
else:
|
||||
layout.append([widget.id])
|
||||
else:
|
||||
layout.append([widget.id])
|
||||
|
||||
widget.dashboard.layout = json.dumps(layout)
|
||||
models.db.session.add(widget.dashboard)
|
||||
models.db.session.commit()
|
||||
return {'widget': widget.to_dict(), 'layout': layout, 'new_row': new_row, 'version': dashboard.version}
|
||||
return {'widget': widget.to_dict()}
|
||||
|
||||
|
||||
class WidgetResource(BaseResource):
|
||||
@ -92,12 +72,8 @@ class WidgetResource(BaseResource):
|
||||
Remove a widget from a dashboard.
|
||||
|
||||
:param number widget_id: ID of widget to remove
|
||||
|
||||
:>json array layout: New layout of dashboard this widget was removed from
|
||||
:>json number version: Revision number of dashboard
|
||||
"""
|
||||
widget = models.Widget.get_by_id_and_org(widget_id, self.current_org)
|
||||
require_object_modify_permission(widget.dashboard, self.current_user)
|
||||
widget.delete()
|
||||
models.db.session.delete(widget)
|
||||
models.db.session.commit()
|
||||
return {'layout': widget.dashboard.layout, 'version': widget.dashboard.version}
|
||||
|
@ -1295,8 +1295,7 @@ class Dashboard(ChangeTrackingMixin, TimestampMixin, BelongsToOrgMixin, db.Model
|
||||
name = Column(db.String(100))
|
||||
user_id = Column(db.Integer, db.ForeignKey("users.id"))
|
||||
user = db.relationship(User)
|
||||
# TODO: The layout should dynamically be built from position and size information on each widget.
|
||||
# Will require update in the frontend code to support this.
|
||||
# layout is no longer used, but kept so we know how to render old dashboards.
|
||||
layout = Column(db.Text)
|
||||
dashboard_filters_enabled = Column(db.Boolean, default=False)
|
||||
is_archived = Column(db.Boolean, default=False, index=True)
|
||||
@ -1311,39 +1310,22 @@ class Dashboard(ChangeTrackingMixin, TimestampMixin, BelongsToOrgMixin, db.Model
|
||||
def to_dict(self, with_widgets=False, user=None):
|
||||
layout = json.loads(self.layout)
|
||||
|
||||
widgets = []
|
||||
|
||||
if with_widgets:
|
||||
widget_list = Widget.query.filter(Widget.dashboard == self)
|
||||
|
||||
widgets = {}
|
||||
|
||||
for w in widget_list:
|
||||
for w in self.widgets:
|
||||
pass
|
||||
if w.visualization_id is None:
|
||||
widgets[w.id] = w.to_dict()
|
||||
widgets.append(w.to_dict())
|
||||
elif user and has_access(w.visualization.query_rel.groups, user, view_only):
|
||||
widgets[w.id] = w.to_dict()
|
||||
widgets.append(w.to_dict())
|
||||
else:
|
||||
widgets[w.id] = project(w.to_dict(),
|
||||
('id', 'width', 'dashboard_id', 'options', 'created_at', 'updated_at'))
|
||||
widgets[w.id]['restricted'] = True
|
||||
|
||||
# The following is a workaround for cases when the widget object gets deleted without the dashboard layout
|
||||
# updated. This happens for users with old databases that didn't have a foreign key relationship between
|
||||
# visualizations and widgets.
|
||||
# It's temporary until better solution is implemented (we probably should move the position information
|
||||
# to the widget).
|
||||
widgets_layout = []
|
||||
for row in layout:
|
||||
if not row:
|
||||
continue
|
||||
new_row = []
|
||||
for widget_id in row:
|
||||
widget = widgets.get(widget_id, None)
|
||||
if widget:
|
||||
new_row.append(widget)
|
||||
|
||||
widgets_layout.append(new_row)
|
||||
widget = project(w.to_dict(),
|
||||
('id', 'width', 'dashboard_id', 'options', 'created_at', 'updated_at'))
|
||||
widget['restricted'] = True
|
||||
widgets.append(widget)
|
||||
else:
|
||||
widgets_layout = None
|
||||
widgets = None
|
||||
|
||||
return {
|
||||
'id': self.id,
|
||||
@ -1352,7 +1334,7 @@ class Dashboard(ChangeTrackingMixin, TimestampMixin, BelongsToOrgMixin, db.Model
|
||||
'user_id': self.user_id,
|
||||
'layout': layout,
|
||||
'dashboard_filters_enabled': self.dashboard_filters_enabled,
|
||||
'widgets': widgets_layout,
|
||||
'widgets': widgets,
|
||||
'is_archived': self.is_archived,
|
||||
'is_draft': self.is_draft,
|
||||
'updated_at': self.updated_at,
|
||||
@ -1464,10 +1446,6 @@ class Widget(TimestampMixin, db.Model):
|
||||
options = Column(db.Text)
|
||||
dashboard_id = Column(db.Integer, db.ForeignKey("dashboards.id"), index=True)
|
||||
|
||||
# unused; kept for backward compatability:
|
||||
type = Column(db.String(100), nullable=True)
|
||||
query_id = Column(db.Integer, nullable=True)
|
||||
|
||||
__tablename__ = 'widgets'
|
||||
|
||||
def to_dict(self):
|
||||
@ -1486,15 +1464,6 @@ class Widget(TimestampMixin, db.Model):
|
||||
|
||||
return d
|
||||
|
||||
def delete(self):
|
||||
layout = json.loads(self.dashboard.layout)
|
||||
layout = map(lambda row: filter(lambda w: w != self.id, row), layout)
|
||||
layout = filter(lambda row: len(row) > 0, layout)
|
||||
self.dashboard.layout = json.dumps(layout)
|
||||
|
||||
db.session.add(self.dashboard)
|
||||
db.session.delete(self)
|
||||
|
||||
def __unicode__(self):
|
||||
return u"%s" % self.id
|
||||
|
||||
|
@ -118,7 +118,6 @@ visualization_factory = ModelFactory(redash.models.Visualization,
|
||||
options='{}')
|
||||
|
||||
widget_factory = ModelFactory(redash.models.Widget,
|
||||
type='chart',
|
||||
width=1,
|
||||
options='{}',
|
||||
dashboard=dashboard_factory.create,
|
||||
|
@ -38,8 +38,8 @@ class TestDashboardResourceGet(BaseTestCase):
|
||||
|
||||
rv = self.make_request('get', '/api/dashboards/{0}'.format(dashboard.slug))
|
||||
self.assertEquals(rv.status_code, 200)
|
||||
self.assertTrue(rv.json['widgets'][0][1]['restricted'])
|
||||
self.assertNotIn('restricted', rv.json['widgets'][0][0])
|
||||
self.assertTrue(rv.json['widgets'][0]['restricted'])
|
||||
self.assertNotIn('restricted', rv.json['widgets'][1])
|
||||
|
||||
def test_get_non_existing_dashboard(self):
|
||||
rv = self.make_request('get', '/api/dashboards/not_existing')
|
||||
|
@ -22,28 +22,6 @@ class WidgetAPITest(BaseTestCase):
|
||||
rv = self.create_widget(dashboard, vis)
|
||||
self.assertEquals(rv.status_code, 200)
|
||||
|
||||
dashboard = models.Dashboard.query.get(dashboard.id)
|
||||
self.assertEquals(unicode(rv.json['layout']), dashboard.layout)
|
||||
|
||||
self.assertEquals(dashboard.widgets.count(), 1)
|
||||
self.assertEquals(rv.json['layout'], [[rv.json['widget']['id']]])
|
||||
self.assertEquals(rv.json['new_row'], True)
|
||||
|
||||
rv2 = self.create_widget(dashboard, vis)
|
||||
self.assertEquals(dashboard.widgets.count(), 2)
|
||||
self.assertEquals(rv2.json['layout'],
|
||||
[[rv.json['widget']['id'], rv2.json['widget']['id']]])
|
||||
self.assertEquals(rv2.json['new_row'], False)
|
||||
|
||||
rv3 = self.create_widget(dashboard, vis)
|
||||
self.assertEquals(rv3.json['new_row'], True)
|
||||
rv4 = self.create_widget(dashboard, vis, width=2)
|
||||
self.assertEquals(rv4.json['layout'],
|
||||
[[rv.json['widget']['id'], rv2.json['widget']['id']],
|
||||
[rv3.json['widget']['id']],
|
||||
[rv4.json['widget']['id']]])
|
||||
self.assertEquals(rv4.json['new_row'], True)
|
||||
|
||||
def test_wont_create_widget_for_visualization_you_dont_have_access_to(self):
|
||||
dashboard = self.factory.create_dashboard()
|
||||
vis = self.factory.create_visualization()
|
||||
@ -86,8 +64,3 @@ class WidgetAPITest(BaseTestCase):
|
||||
self.assertEquals(rv.status_code, 200)
|
||||
dashboard = models.Dashboard.get_by_slug_and_org(widget.dashboard.slug, widget.dashboard.org)
|
||||
self.assertEquals(dashboard.widgets.count(), 0)
|
||||
self.assertEquals(dashboard.layout, '[]')
|
||||
|
||||
# TODO: test how it updates the layout
|
||||
|
||||
|
||||
|
@ -476,26 +476,6 @@ class TestEvents(BaseTestCase):
|
||||
self.assertDictEqual(event.additional_properties, additional_properties)
|
||||
|
||||
|
||||
class TestWidgetDeleteInstance(BaseTestCase):
|
||||
def test_delete_removes_from_layout(self):
|
||||
widget = self.factory.create_widget()
|
||||
widget2 = self.factory.create_widget(dashboard=widget.dashboard)
|
||||
db.session.flush()
|
||||
widget.dashboard.layout = json.dumps([[widget.id, widget2.id]])
|
||||
widget.delete()
|
||||
self.assertEquals(json.dumps([[widget2.id]]), widget.dashboard.layout)
|
||||
|
||||
def test_delete_removes_empty_rows(self):
|
||||
widget = self.factory.create_widget()
|
||||
widget2 = self.factory.create_widget(dashboard=widget.dashboard)
|
||||
db.session.flush()
|
||||
widget.dashboard.layout = json.dumps([[widget.id, widget2.id]])
|
||||
db.session.flush()
|
||||
widget.delete()
|
||||
widget2.delete()
|
||||
self.assertEquals("[]", widget.dashboard.layout)
|
||||
|
||||
|
||||
def _set_up_dashboard_test(d):
|
||||
d.g1 = d.factory.create_group(name='First', permissions=['create', 'view'])
|
||||
d.g2 = d.factory.create_group(name='Second', permissions=['create', 'view'])
|
||||
|
Loading…
Reference in New Issue
Block a user