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