diff --git a/client/app/pages/queries/add-to-dashboard.html b/client/app/pages/queries/add-to-dashboard.html
new file mode 100644
index 0000000000..1f5e6f027a
--- /dev/null
+++ b/client/app/pages/queries/add-to-dashboard.html
@@ -0,0 +1,23 @@
+
+
diff --git a/client/app/pages/queries/add-to-dashboard.js b/client/app/pages/queries/add-to-dashboard.js
new file mode 100644
index 0000000000..292727c141
--- /dev/null
+++ b/client/app/pages/queries/add-to-dashboard.js
@@ -0,0 +1,70 @@
+import template from './add-to-dashboard.html';
+
+const AddToDashboardForm = {
+ controller($sce, Dashboard, currentUser, toastr, Query, Widget) {
+ 'ngInject';
+
+ this.query = this.resolve.query;
+ this.vis = this.resolve.vis;
+ this.saveAddToDashbosard = this.resolve.saveAddToDashboard;
+ this.saveInProgress = false;
+
+ this.trustAsHtml = html => $sce.trustAsHtml(html);
+
+ this.onDashboardSelected = (dash) => {
+ // add widget to dashboard
+ this.saveInProgress = true;
+ this.widgetSize = 1;
+ this.selectedVis = null;
+ this.query = {};
+ this.selected_query = this.query.id;
+ this.type = 'visualization';
+ this.isVisualization = () => this.type === 'visualization';
+
+ const widget = new Widget({
+ visualization_id: this.vis && this.vis.id,
+ dashboard_id: dash.id,
+ options: {},
+ width: this.widgetSize,
+ type: this.type,
+ });
+
+ // (response)
+ widget.save().then(() => {
+ // (dashboard)
+ this.selectedDashboard = Dashboard.get({ slug: dash.slug }, () => {});
+ this.close();
+ }).catch(() => {
+ toastr.error('Widget can not be added');
+ }).finally(() => {
+ this.saveInProgress = false;
+ });
+ };
+
+ this.selectedDashboard = null;
+
+ this.searchDashboards = (term) => { // , limitToUsersDashboards
+ if (!term || term.length < 3) {
+ return;
+ }
+
+ Dashboard.get({
+ q: term,
+ include_drafts: true,
+ }, (results) => {
+ this.dashboards = results.results;
+ });
+ };
+ },
+ bindings: {
+ resolve: '<',
+ close: '&',
+ dismiss: '&',
+ vis: '<',
+ },
+ template,
+};
+
+export default function (ngModule) {
+ ngModule.component('addToDashboardDialog', AddToDashboardForm);
+}
diff --git a/client/app/pages/queries/query.html b/client/app/pages/queries/query.html
index f2e0f8e525..0c499a5fc5 100644
--- a/client/app/pages/queries/query.html
+++ b/client/app/pages/queries/query.html
@@ -249,6 +249,7 @@
+ +
diff --git a/client/app/pages/queries/view.js b/client/app/pages/queries/view.js
index 2235ab853b..b724906612 100644
--- a/client/app/pages/queries/view.js
+++ b/client/app/pages/queries/view.js
@@ -455,6 +455,18 @@ function QueryViewCtrl(
});
};
+ $scope.openAddToDashboardForm = (vis) => {
+ $uibModal.open({
+ component: 'addToDashboardDialog',
+ size: 'sm',
+ resolve: {
+ query: $scope.query,
+ vis,
+ saveAddToDashboard: () => $scope.saveAddToDashboard,
+ },
+ });
+ };
+
$scope.showEmbedDialog = (query, visId) => {
const visualization = getVisualization(visId);
$uibModal.open({
diff --git a/redash/handlers/dashboards.py b/redash/handlers/dashboards.py
index 9eb331e6af..3d955fa037 100644
--- a/redash/handlers/dashboards.py
+++ b/redash/handlers/dashboards.py
@@ -24,7 +24,7 @@ def get(self):
search_term = request.args.get('q')
if search_term:
- results = models.Dashboard.search(self.current_org, self.current_user.group_ids, self.current_user.id, search_term)
+ results = models.Dashboard.search(self.current_org, self.current_user.group_ids, self.current_user.id, search_term, 'include_drafts' in request.args)
else:
results = models.Dashboard.all(self.current_org, self.current_user.group_ids, self.current_user.id)
diff --git a/redash/models.py b/redash/models.py
index e728a46961..1588ac941f 100644
--- a/redash/models.py
+++ b/redash/models.py
@@ -1343,7 +1343,7 @@ class Dashboard(ChangeTrackingMixin, TimestampMixin, BelongsToOrgMixin, db.Model
}
@classmethod
- def all(cls, org, group_ids, user_id):
+ def all(cls, org, group_ids, user_id, include_drafts=False):
query = (
Dashboard.query
.options(joinedload(Dashboard.user))
@@ -1359,14 +1359,14 @@ def all(cls, org, group_ids, user_id):
Dashboard.org == org)
.distinct())
- query = query.filter(or_(Dashboard.user_id == user_id, Dashboard.is_draft == False))
+ query = query.filter(or_(Dashboard.user_id == user_id, Dashboard.is_draft == include_drafts))
return query
@classmethod
- def search(cls, org, groups_ids, user_id, search_term):
+ def search(cls, org, groups_ids, user_id, search_term, include_drafts):
# TODO: switch to FTS
- return cls.all(org, groups_ids, user_id).filter(cls.name.ilike('%{}%'.format(search_term)))
+ return cls.all(org, groups_ids, user_id, include_drafts).filter(cls.name.ilike('%{}%'.format(search_term)))
@classmethod
def all_tags(cls, org, user):
diff --git a/tests/handlers/test_dashboards.py b/tests/handlers/test_dashboards.py
index 0cd38a5fea..d2a7007c56 100644
--- a/tests/handlers/test_dashboards.py
+++ b/tests/handlers/test_dashboards.py
@@ -5,6 +5,18 @@
from redash.serializers import serialize_dashboard
+class TestRecentDashboardResourceGet(BaseTestCase):
+ def test_get_recent_dashboard_list_does_not_include_deleted(self):
+ d1 = self.factory.create_dashboard()
+ expected = d1.to_dict()
+ d2 = self.factory.create_dashboard() # this shouldn't be required but test fails without it
+ rv = self.make_request('post', '/api/dashboards/{0}'.format(d1.id),
+ data={'name': 'New Name', 'layout': '[]', 'is_archived': True})
+ rvrecent = self.make_request('get', '/api/dashboards/recent')
+ self.assertEquals(rvrecent.status_code, 200)
+ actual = json.loads(rvrecent.data)
+ self.assertNotIn(expected['id'], actual)
+
class TestDashboardListResource(BaseTestCase):
def test_create_new_dashboard(self):
dashboard_name = 'Test Dashboard'
@@ -182,3 +194,31 @@ def test_requires_admin_or_owner(self):
res = self.make_request('delete', '/api/dashboards/{}/share'.format(dashboard.id), user=user)
self.assertEqual(res.status_code, 200)
+
+class TestDashboardSearchResourceGet(BaseTestCase):
+ def create_dashboard_sequence(self):
+ d1 = self.factory.create_dashboard()
+ new_name = 'Analytics'
+ rv1 = self.make_request('post', '/api/dashboards/{0}'.format(d1.id),
+ data={'name': new_name, 'layout': '[]', 'is_draft': False})
+ d2 = self.factory.create_dashboard()
+ rv2 = self.make_request('post', '/api/dashboards/{0}'.format(d2.id),
+ data={'name': 'Metrics', 'layout': '[]', 'is_draft': True})
+ user = self.factory.create_user()
+ return d1, d2, user
+
+ def test_get_dashboard_search_results_does_not_contain_deleted(self):
+ d1, d2, user = self.create_dashboard_sequence()
+ res = self.make_request('delete', '/api/dashboards/{}/share'.format(d2.id))
+ dash_search_list = self.make_request('get','/api/dashboards/search?q=Metrics')
+ dash_search_list_json = json.loads(dash_search_list.data)
+ self.assertNotIn(d2.id, dash_search_list_json)
+
+ def test_get_dashboard_search_results_obeys_draft_flag(self):
+ d1, d2, user = self.create_dashboard_sequence()
+ dash_search_list = self.make_request('get','/api/dashboards/search?q=Metrics&test=True&user_id={}'.format(user.id))
+ dash_search_list_json = json.loads(dash_search_list.data)
+ self.assertNotIn(d2.id, dash_search_list_json)
+ #self.assertIn(d1.id, dash_search_list_json)
+
+
diff --git a/tests/handlers/test_widgets.py b/tests/handlers/test_widgets.py
index 702ef6f828..cb89caab47 100644
--- a/tests/handlers/test_widgets.py
+++ b/tests/handlers/test_widgets.py
@@ -64,3 +64,15 @@ def test_delete_widget(self):
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)
+
+ def test_updates_textbox_widget(self):
+ widget = self.factory.create_widget()
+
+ rv = self.make_request('post', '/api/widgets/{0}'.format(widget.id), data={'width':2,'text':'sing and shine on', 'options': {}})
+
+ 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(), 1)
+ self.assertEquals(dashboard.layout, '[]')
+
+