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..5eca59975c
--- /dev/null
+++ b/client/app/pages/queries/add-to-dashboard.js
@@ -0,0 +1,72 @@
+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.search({
+ q: term,
+ user_id: currentUser.id,
+ // limit_to_users_dashboards: limitToUsersDashboards,
+ include_drafts: true,
+ }, (results) => {
+ this.dashboards = results;
+ });
+ };
+ },
+ bindings: {
+ resolve: '<',
+ close: '&',
+ dismiss: '&',
+ vis: '<',
+ },
+ template,
+};
+
+export default function (ngModule) {
+ ngModule.component('addToDashboardDialog', AddToDashboardForm);
+}
diff --git a/client/app/pages/queries/index.js b/client/app/pages/queries/index.js
index a0ce2a9206..ec8abcd617 100644
--- a/client/app/pages/queries/index.js
+++ b/client/app/pages/queries/index.js
@@ -11,6 +11,7 @@ import registerQuerySearchResultsPage from './queries-search-results-page';
import registerVisualizationEmbed from './visualization-embed';
import registerCompareQueryDialog from './compare-query-dialog';
import registerGetDataSourceVersion from './get-data-source-version';
+import registerAddToDashboard from './add-to-dashboard';
export default function (ngModule) {
registerQueryResultsLink(ngModule);
@@ -24,6 +25,7 @@ export default function (ngModule) {
registerApiKeyDialog(ngModule);
registerCompareQueryDialog(ngModule);
registerGetDataSourceVersion(ngModule);
+ registerAddToDashboard(ngModule);
return Object.assign({}, registerQuerySearchResultsPage(ngModule),
registerSourceView(ngModule),
diff --git a/client/app/pages/queries/query.html b/client/app/pages/queries/query.html
index 858f6487f7..f0e6c69cb1 100644
--- a/client/app/pages/queries/query.html
+++ b/client/app/pages/queries/query.html
@@ -253,7 +253,7 @@
×
+ ng-show="canEdit"> × +
+ New Visualization
diff --git a/client/app/pages/queries/view.js b/client/app/pages/queries/view.js
index 169832deb6..29a1a9d7c9 100644
--- a/client/app/pages/queries/view.js
+++ b/client/app/pages/queries/view.js
@@ -362,6 +362,18 @@ function QueryViewCtrl($scope, Events, $route, $routeParams, $location, $window,
});
};
+ $scope.openAddToDashboardForm = (vis) => {
+ $uibModal.open({
+ component: 'addToDashboardDialog',
+ size: 'sm',
+ resolve: {
+ query: $scope.query,
+ vis,
+ saveAddToDashboard: () => $scope.saveAddToDashboard,
+ },
+ });
+ };
+
$scope.showEmbedDialog = (query, visualization) => {
$uibModal.open({
component: 'embedCodeDialog',
diff --git a/client/app/services/dashboard.js b/client/app/services/dashboard.js
index ce37e1505d..7e6354a9a5 100644
--- a/client/app/services/dashboard.js
+++ b/client/app/services/dashboard.js
@@ -19,6 +19,7 @@ function Dashboard($resource, $http, currentUser, Widget) {
get: { method: 'GET', transformResponse: transform },
save: { method: 'POST', transformResponse: transform },
query: { method: 'GET', isArray: true, transformResponse: transform },
+ search: { method: 'GET', isArray: true, url: 'api/dashboards/search' },
recent: {
method: 'get',
isArray: true,
diff --git a/redash/handlers/api.py b/redash/handlers/api.py
index a6786c12d1..8e55e82873 100644
--- a/redash/handlers/api.py
+++ b/redash/handlers/api.py
@@ -6,7 +6,7 @@
from redash.handlers.base import org_scoped_rule
from redash.handlers.permissions import ObjectPermissionsListResource, CheckPermissionResource
from redash.handlers.alerts import AlertResource, AlertListResource, AlertSubscriptionListResource, AlertSubscriptionResource
-from redash.handlers.dashboards import DashboardListResource, RecentDashboardsResource, DashboardResource, DashboardShareResource, PublicDashboardResource
+from redash.handlers.dashboards import DashboardListResource, RecentDashboardsResource, DashboardResource, DashboardShareResource, PublicDashboardResource, SearchDashboardResource
from redash.handlers.data_sources import DataSourceTypeListResource, DataSourceListResource, DataSourceSchemaResource, DataSourceResource, DataSourcePauseResource, DataSourceTestResource, DataSourceVersionResource
from redash.handlers.events import EventResource
from redash.handlers.queries import (
@@ -52,6 +52,7 @@ def json_representation(data, code, headers=None):
api.add_org_resource(DashboardResource, '/api/dashboards/', endpoint='dashboard')
api.add_org_resource(PublicDashboardResource, '/api/dashboards/public/', endpoint='public_dashboard')
api.add_org_resource(DashboardShareResource, '/api/dashboards//share', endpoint='dashboard_share')
+api.add_org_resource(SearchDashboardResource, '/api/dashboards/search')
api.add_org_resource(DataSourceTypeListResource, '/api/data_sources/types', endpoint='data_source_types')
api.add_org_resource(DataSourceListResource, '/api/data_sources', endpoint='data_sources')
diff --git a/redash/handlers/dashboards.py b/redash/handlers/dashboards.py
index f47c59fb2b..f4eadb3da8 100644
--- a/redash/handlers/dashboards.py
+++ b/redash/handlers/dashboards.py
@@ -65,7 +65,6 @@ def post(self):
models.db.session.commit()
return dashboard.to_dict()
-
class DashboardResource(BaseResource):
@require_permission('list_dashboards')
def get(self, dashboard_slug=None):
@@ -241,3 +240,21 @@ def delete(self, dashboard_id):
'object_id': dashboard.id,
'object_type': 'dashboard',
})
+
+class SearchDashboardResource(BaseResource):
+ @require_permission('list_dashboards')
+ def get(self):
+ """
+ Searches for a dashboard.
+
+ Sends to models.py > Dashboard > search()
+ search(cls, term, user_id, group_ids, limit_to_users_dashboards=False, include_drafts=False)
+ """
+ term = request.args.get('q', '')
+ include_drafts = request.args.get('include_drafts') is not None
+ user_id = request.args.get('user_id', '')
+ # limit_to_users_dashboards = request.args.get('limit_to_users_dashboards', '')
+
+ # limit_to_users_dashboards=limit_to_users_dashboards,
+ return [q.to_dict() for q in models.Dashboard.search(term, user_id, self.current_user.group_ids, include_drafts=include_drafts)]
+
diff --git a/redash/models.py b/redash/models.py
index 6ef8b06d70..311fbdb5c4 100644
--- a/redash/models.py
+++ b/redash/models.py
@@ -1306,6 +1306,30 @@ def all(cls, org, group_ids, user_id):
return query
+ @classmethod
+ def search(cls, term, user_id, group_ids, include_drafts=False):
+ # limit_to_users_dashboards=False,
+ # TODO: This is very naive implementation of search, to be replaced with PostgreSQL full-text-search solution.
+ where = (Dashboard.name.ilike(u"%{}%".format(term)))
+
+ if term.isdigit():
+ where |= Dashboard.id == term
+
+ #if limit_to_users_dashboards:
+ # where &= Dashboard.user_id == user_id
+
+ where &= Dashboard.is_archived == False
+
+ if not include_drafts:
+ where &= Dashboard.is_draft == False
+
+ where &= DataSourceGroup.group_id.in_(group_ids)
+ dashboard_ids = (
+ db.session.query(Dashboard.id)
+ .filter(where)).distinct()
+
+ return Dashboard.query.filter(Dashboard.id.in_(dashboard_ids))
+
@classmethod
def recent(cls, org, group_ids, user_id, for_user=False, limit=20):
query = (Dashboard.query