Skip to content

Commit

Permalink
Add: query snippets feature
Browse files Browse the repository at this point in the history
  • Loading branch information
arikfr committed Aug 22, 2016
1 parent b8eca28 commit 10f5ecd
Show file tree
Hide file tree
Showing 16 changed files with 298 additions and 6 deletions.
8 changes: 8 additions & 0 deletions migrations/0025_add_notification_destination.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from redash.models import db, QuerySnippet

if __name__ == '__main__':
with db.database.transaction():
if not QuerySnippet.table_exists():
QuerySnippet.create_table()

db.close_db(None)
1 change: 1 addition & 0 deletions rd_ui/app/app_layout.html
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ <h1><span class="zmdi zmdi-lock"></span></h1>
<script src="/scripts/controllers/query_view.js"></script>
<script src="/scripts/controllers/query_source.js"></script>
<script src="/scripts/controllers/users.js"></script>
<script src="/scripts/controllers/snippets.js"></script>
<script src="/scripts/visualizations/base.js"></script>
<script src="/scripts/visualizations/chart.js"></script>
<script src="/scripts/visualizations/cohort.js"></script>
Expand Down
10 changes: 9 additions & 1 deletion rd_ui/app/scripts/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,15 @@ angular.module('redash', [
$routeProvider.when('/groups', {
templateUrl: '/views/groups/list.html',
controller: 'GroupsCtrl'
})
});
$routeProvider.when('/query_snippets/:snippetId', {
templateUrl: '/views/query_snippets/show.html',
controller: 'SnippetCtrl'
});
$routeProvider.when('/query_snippets', {
templateUrl: '/views/query_snippets/list.html',
controller: 'SnippetsCtrl'
});
$routeProvider.when('/', {
templateUrl: '/views/index.html',
controller: 'IndexCtrl'
Expand Down
93 changes: 93 additions & 0 deletions rd_ui/app/scripts/controllers/snippets.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
(function() {
var SnippetsCtrl = function ($scope, $location, growl, Events, QuerySnippet) {
Events.record(currentUser, "view", "page", "query_snippets");
$scope.$parent.pageTitle = "Query Snippets";

$scope.gridConfig = {
isPaginationEnabled: true,
itemsByPage: 20,
maxSize: 8,
};

$scope.gridColumns = [
{
"label": "Trigger",
"cellTemplate": '<a href="query_snippets/{{dataRow.id}}">{{dataRow.trigger}}</a>'
},
{
"label": "Description",
"map": "description"
},
{
"label": "Snippet",
"map": "snippet"
},
{
'label': 'Created By',
'map': 'user.name'
},
{
'label': 'Updated At',
'cellTemplate': '<span am-time-ago="dataRow.created_at"></span>'
}
];

$scope.snippets = [];
QuerySnippet.query(function(snippets) {
$scope.snippets = snippets;
});
};

var SnippetCtrl = function ($scope, $routeParams, $http, $location, growl, Events, QuerySnippet) {
$scope.$parent.pageTitle = "Query Snippets";
$scope.snippetId = $routeParams.snippetId;
Events.record(currentUser, "view", "query_snippet", $scope.snippetId);

$scope.editorOptions = {
mode: 'snippets',
advanced: {
behavioursEnabled: true,
enableSnippets: false,
autoScrollEditorIntoView: true,
},
onLoad: function(editor) {
editor.$blockScrolling = Infinity;
editor.getSession().setUseWrapMode(true);
editor.setShowPrintMargin(false);
}
};

$scope.saveChanges = function() {
$scope.snippet.$save(function(snippet) {
growl.addSuccessMessage("Saved.");
if ($scope.snippetId === "new") {
$location.path('/query_snippets/' + snippet.id).replace();
}
}, function() {
growl.addErrorMessage("Failed saving snippet.");
});
}

$scope.delete = function() {
$scope.snippet.$delete(function() {
$location.path('/query_snippets');
growl.addSuccessMessage("Query snippet deleted.");
}, function() {
growl.addErrorMessage("Failed deleting query snippet.");
});
}

if ($scope.snippetId == 'new') {
$scope.snippet = new QuerySnippet({description: ""});
$scope.canEdit = true;
} else {
$scope.snippet = QuerySnippet.get({id: $scope.snippetId}, function(snippet) {
$scope.canEdit = currentUser.canEdit(snippet);
});
}
};

angular.module('redash.controllers')
.controller('SnippetsCtrl', ['$scope', '$location', 'growl', 'Events', 'QuerySnippet', SnippetsCtrl])
.controller('SnippetCtrl', ['$scope', '$routeParams', '$http', '$location', 'growl', 'Events', 'QuerySnippet', SnippetCtrl])
})();
1 change: 1 addition & 0 deletions rd_ui/app/scripts/directives/directives.js
Original file line number Diff line number Diff line change
Expand Up @@ -488,6 +488,7 @@
scope.groupsPage = _.string.startsWith($location.path(), '/groups');
scope.dsPage = _.string.startsWith($location.path(), '/data_sources');
scope.destinationsPage = _.string.startsWith($location.path(), '/destinations');
scope.snippetsPage = _.string.startsWith($location.path(), '/query_snippets');

scope.showGroupsLink = currentUser.hasPermission('list_users');
scope.showUsersLink = currentUser.hasPermission('list_users');
Expand Down
18 changes: 15 additions & 3 deletions rd_ui/app/scripts/directives/query_directives.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@
defineDummySnippets("sql");
defineDummySnippets("json");

function queryEditor() {
function queryEditor(QuerySnippet) {
return {
restrict: 'E',
scope: {
Expand All @@ -100,7 +100,19 @@
autoScrollEditorIntoView: true,
},
onLoad: function(editor) {
// Test for snippet manager
QuerySnippet.query(function(snippets) {
var snippetManager = ace.require("ace/snippets").snippetManager;
var m = {
snippetText: ''
};
m.snippets = snippetManager.parseSnippetFile(m.snippetText);
_.each(snippets, function(snippet) {
m.snippets.push(snippet.getSnippet());
});

snippetManager.register(m.snippets || [], m.scope);
});

editor.$blockScrolling = Infinity;
editor.getSession().setUseWrapMode(true);
editor.setShowPrintMargin(false);
Expand Down Expand Up @@ -314,7 +326,7 @@
.directive('queryLink', queryLink)
.directive('querySourceLink', ['$location', querySourceLink])
.directive('queryResultLink', queryResultLink)
.directive('queryEditor', queryEditor)
.directive('queryEditor', ['QuerySnippet', queryEditor])
.directive('queryRefreshSelect', queryRefreshSelect)
.directive('queryTimePicker', queryTimePicker)
.directive('queryFormatter', ['$http', 'growl', queryFormatter]);
Expand Down
21 changes: 20 additions & 1 deletion rd_ui/app/scripts/services/resources.js
Original file line number Diff line number Diff line change
Expand Up @@ -755,6 +755,24 @@
return resource;
};

var QuerySnippet = function ($resource) {
var resource = $resource('api/query_snippets/:id', {id: '@id'});
resource.prototype.getSnippet = function() {
var name = this.trigger;
if (this.description !== "") {
name = this.trigger + ": " + this.description;
}

return {
"name": name,
"content": this.snippet,
"tabTrigger": this.trigger
};
}

return resource;
};

var Widget = function ($resource, Query) {
var WidgetResource = $resource('api/widgets/:id', {id: '@id'});

Expand Down Expand Up @@ -785,5 +803,6 @@
.factory('AlertSubscription', ['$resource', AlertSubscription])
.factory('Widget', ['$resource', 'Query', Widget])
.factory('User', ['$resource', '$http', User])
.factory('Group', ['$resource', Group]);
.factory('Group', ['$resource', Group])
.factory('QuerySnippet', ['$resource', QuerySnippet]);
})();
1 change: 1 addition & 0 deletions rd_ui/app/vendor_scripts.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
<script src="/bower_components/ace-builds/src-min-noconflict/mode-sql.js"></script>
<script src="/bower_components/ace-builds/src-min-noconflict/mode-json.js"></script>
<script src="/bower_components/ace-builds/src-min-noconflict/mode-python.js"></script>
<script src="/bower_components/ace-builds/src-min-noconflict/mode-snippets.js"></script>
<script src="/bower_components/ace-builds/src-min-noconflict/ext-language_tools.js"></script>
<script src="/bower_components/angular/angular.js"></script>
<script src="/bower_components/angular-sanitize/angular-sanitize.js"></script>
Expand Down
2 changes: 1 addition & 1 deletion rd_ui/app/views/app_header.html
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@
<a href="data_sources" title="Data Sources"><i class="fa fa-database"></i></a>
</li>
<li ng-show="currentUser.hasPermission('list_users')">
<a href="users" title="Users"><i class="fa fa-users"></i></a>
<a href="users" title="Settings"><i class="fa fa-cog"></i></a>
</li>
<li class="dropdown" dropdown>
<a href="#" class="dropdown-toggle" dropdown-toggle><span ng-bind="currentUser.name"></span> <span
Expand Down
1 change: 1 addition & 0 deletions rd_ui/app/views/directives/settings_screen.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
<li ng-class="{'active': usersPage }" ng-if="showUsersLink"><a href="users">Users</a></li>
<li ng-class="{'active': groupsPage }" ng-if="showGroupsLink"><a href="groups">Groups</a></li>
<li ng-class="{'active': destinationsPage }" ng-if="showDestinationsLink"><a href="destinations">Alert Destinations</a></li>
<li ng-class="{'active': snippetsPage }"><a href="query_snippets">Query Snippets</a></li>
</ul>

<div ng-transclude>
Expand Down
13 changes: 13 additions & 0 deletions rd_ui/app/views/query_snippets/list.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<settings-screen>
<div class="row voffset1">
<div class="col-md-12">
<p>
<a href="query_snippets/new" class="btn btn-default"><i class="fa fa-plus"></i> New Snippet</a>
</p>

<smart-table rows="snippets" columns="gridColumns"
config="gridConfig"
class="table table-condensed table-hover"></smart-table>
</div>
</div>
</settings-screen>
36 changes: 36 additions & 0 deletions rd_ui/app/views/query_snippets/show.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<settings-screen>
<!--<h2 class="p-l-5">{{snippet.trigger}}</h2>-->

<div class="">
<!--<pre>-->
<!--{{snippet | json}}-->
<!--</pre>-->

<form name="snippetForm" class="form">
<div class="form-group">
<label>Trigger</label>
<input type="string" class="form-control" ng-model="snippet.trigger" ng-disabled="!canEdit" required>
</div>

<div class="form-group">
<label>Description</label>
<input type="string" class="form-control" ng-model="snippet.description" ng-disabled="!canEdit">
</div>

<div class="form-group">
<label>Snippet</label>
<pre ng-if="!canEdit">{{snippet.snippet}}</pre>
<div ui-ace="editorOptions" ng-model="snippet.snippet" style="height:300px" ng-if="canEdit"></div>
</div>

<div class="form-group" ng-if="canEdit">
<button class="btn btn-primary" ng-disabled="!snippetForm.$valid" ng-click="saveChanges()">Save</button>
<button class="btn btn-danger" ng-if="snippet.id" ng-click="delete()">Delete</button>
</div>
<small ng-if="snippet.user">
Created by: {{snippet.user.name}}
</small>
</form>

</div>
</settings-screen>
4 changes: 4 additions & 0 deletions redash/handlers/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from redash.handlers.groups import GroupListResource, GroupResource, GroupMemberListResource, GroupMemberResource, \
GroupDataSourceListResource, GroupDataSourceResource
from redash.handlers.destinations import DestinationTypeListResource, DestinationResource, DestinationListResource
from redash.handlers.query_snippets import QuerySnippetListResource, QuerySnippetResource


class ApiExt(Api):
Expand Down Expand Up @@ -90,3 +91,6 @@ def json_representation(data, code, headers=None):
api.add_org_resource(DestinationTypeListResource, '/api/destinations/types', endpoint='destination_types')
api.add_org_resource(DestinationResource, '/api/destinations/<destination_id>', endpoint='destination')
api.add_org_resource(DestinationListResource, '/api/destinations', endpoint='destinations')

api.add_org_resource(QuerySnippetResource, '/api/query_snippets/<snippet_id>', endpoint='query_snippet')
api.add_org_resource(QuerySnippetListResource, '/api/query_snippets', endpoint='query_snippets')
64 changes: 64 additions & 0 deletions redash/handlers/query_snippets.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
from flask import request
from funcy import project

from redash import models
from redash.permissions import require_admin_or_owner
from redash.handlers.base import BaseResource, require_fields, get_object_or_404


class QuerySnippetResource(BaseResource):
def get(self, snippet_id):
snippet = get_object_or_404(models.QuerySnippet.get_by_id_and_org, snippet_id, self.current_org)
return snippet.to_dict()

def post(self, snippet_id):
req = request.get_json(True)
params = project(req, ('trigger', 'description', 'snippet'))
snippet = get_object_or_404(models.QuerySnippet.get_by_id_and_org, snippet_id, self.current_org)
require_admin_or_owner(snippet.user.id)

snippet.update_instance(**params)

self.record_event({
'action': 'edit',
'object_id': snippet.id,
'object_type': 'query_snippet'
})

return snippet.to_dict()

def delete(self, snippet_id):
snippet = get_object_or_404(models.QuerySnippet.get_by_id_and_org, snippet_id, self.current_org)
require_admin_or_owner(snippet.user.id)
snippet.delete_instance()

self.record_event({
'action': 'delete',
'object_id': snippet.id,
'object_type': 'query_snippet'
})


class QuerySnippetListResource(BaseResource):
def post(self):
req = request.get_json(True)
require_fields(req, ('trigger', 'description', 'snippet'))

snippet = models.QuerySnippet.create(
trigger=req['trigger'],
description=req['description'],
snippet=req['snippet'],
user=self.current_user,
org=self.current_org
)

self.record_event({
'action': 'create',
'object_id': snippet.id,
'object_type': 'query_snippet'
})

return snippet.to_dict()

def get(self):
return [snippet.to_dict() for snippet in models.QuerySnippet.all(org=self.current_org)]
2 changes: 2 additions & 0 deletions redash/handlers/static.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ def register_static_routes(rules):
'/users/<pk>',
'/destinations',
'/destinations/<pk>',
'/query_snippets',
'/query_snippets/<pk>',
'/groups',
'/groups/<pk>',
'/groups/<pk>/data_sources',
Expand Down
Loading

0 comments on commit 10f5ecd

Please sign in to comment.