Skip to content

Commit

Permalink
add ability to add query to dashboard from query page (re getredash#154)
Browse files Browse the repository at this point in the history
  • Loading branch information
alison985 authored and Marina Samuel committed May 25, 2018
1 parent 7c25d18 commit 4b403de
Show file tree
Hide file tree
Showing 10 changed files with 205 additions and 2 deletions.
23 changes: 23 additions & 0 deletions client/app/pages/queries/add-to-dashboard.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<div class="modal-header">
<button type="button" class="close" aria-label="Close" ng-click="$ctrl.close()"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title">Add to Dashboard</h4>
</div>
<div class="modal-body">

<form name="alertForm" class="form">
<div class="form-group">
<!-- <label>Limit Dashboard search to dashboards I've created? </label>
<input type="checkbox" ng-model="$ctrl.limitToUsersDashboards" on-change="$ctrl.dashboardList == $select.search" ng-disabled="$ctrl.saveInProgress" /> -->
<label>Choose the dashboard to add this query to:</label>
<ui-select ng-model="$ctrl.dashboardList" reset-search-input="false" on-select="$ctrl.onDashboardSelected($item)" ng-disabled="$ctrl.saveInProgress">
<ui-select-match placeholder="Search a dashboard by name">{{$select.selected.name}}</ui-select-match>
<ui-select-choices repeat="q in $ctrl.dashboards"
refresh="$ctrl.searchDashboards($select.search, $ctrl.limitToUsersDashboards)"
refresh-delay="0">
<div ng-bind-html="$ctrl.trustAsHtml(q.name | highlight: $select.search)"></div>
</ui-select-choices>
</ui-select>
</div>
</form>

</div>
72 changes: 72 additions & 0 deletions client/app/pages/queries/add-to-dashboard.js
Original file line number Diff line number Diff line change
@@ -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);
}
1 change: 1 addition & 0 deletions client/app/pages/queries/query.html
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,7 @@ <h3>
<rd-tab ng-if="!query.visualizations.length" tab-id="table" name="Table" base-path="query.getUrl(sourceMode)"></rd-tab>
<rd-tab tab-id="{{vis.id}}" name="{{vis.name}}" base-path="query.getUrl(sourceMode)" ng-repeat="vis in query.visualizations | orderBy:'id'">
<span class="remove" ng-click="deleteVisualization($event, vis)" ng-if="canEdit && !($first && (vis.type === 'TABLE'))"> &times;</span>
<span class="btn btn-xs btn-success" ng-click="openAddToDashboardForm(vis)"> +</span>
</rd-tab>
<li class="rd-tab"><a ng-click="openVisualizationEditor()" ng-if="sourceMode && canEdit">&plus; New Visualization</a></li>
</ul>
Expand Down
12 changes: 12 additions & 0 deletions client/app/pages/queries/view.js
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,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({
Expand Down
1 change: 1 addition & 0 deletions client/app/services/dashboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ function Dashboard($resource, $http, currentUser, Widget, dashboardGridOptions)
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,
Expand Down
3 changes: 2 additions & 1 deletion redash/handlers/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 EventsResource
from redash.handlers.queries import QueryForkResource, QueryRefreshResource, QueryListResource, QueryRecentResource, QuerySearchResource, QueryResource, MyQueriesResource, QueryVersionListResource, ChangeResource
Expand Down Expand Up @@ -50,6 +50,7 @@ def json_representation(data, code, headers=None):
api.add_org_resource(DashboardResource, '/api/dashboards/<dashboard_slug>', endpoint='dashboard')
api.add_org_resource(PublicDashboardResource, '/api/dashboards/public/<token>', endpoint='public_dashboard')
api.add_org_resource(DashboardShareResource, '/api/dashboards/<dashboard_id>/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')
Expand Down
19 changes: 18 additions & 1 deletion redash/handlers/dashboards.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,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):
Expand Down Expand Up @@ -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', '')
group_ids = self.current_user.group_ids
if group_ids == None and request.args.get('test',False):
group_ids = [2] # the array that's used for test factory users
return [q.to_dict() for q in models.Dashboard.search(term, user_id, group_ids, include_drafts=include_drafts)]

24 changes: 24 additions & 0 deletions redash/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1381,6 +1381,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
Expand Down
40 changes: 40 additions & 0 deletions tests/handlers/test_dashboards.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,18 @@
from redash.permissions import ACCESS_TYPE_MODIFY


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'
Expand Down Expand Up @@ -151,3 +163,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)


12 changes: 12 additions & 0 deletions tests/handlers/test_widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -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, '[]')


0 comments on commit 4b403de

Please sign in to comment.