mirror of
https://github.com/valitydev/redash.git
synced 2024-11-06 17:15:17 +00:00
Add: query snippets feature
This commit is contained in:
parent
b8eca28e20
commit
10f5ecdb00
8
migrations/0025_add_notification_destination.py
Normal file
8
migrations/0025_add_notification_destination.py
Normal file
@ -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)
|
@ -78,6 +78,7 @@
|
||||
<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>
|
||||
|
@ -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'
|
||||
|
93
rd_ui/app/scripts/controllers/snippets.js
Normal file
93
rd_ui/app/scripts/controllers/snippets.js
Normal file
@ -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])
|
||||
})();
|
@ -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');
|
||||
|
@ -75,7 +75,7 @@
|
||||
defineDummySnippets("sql");
|
||||
defineDummySnippets("json");
|
||||
|
||||
function queryEditor() {
|
||||
function queryEditor(QuerySnippet) {
|
||||
return {
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
@ -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);
|
||||
@ -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]);
|
||||
|
@ -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'});
|
||||
|
||||
@ -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]);
|
||||
})();
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
|
13
rd_ui/app/views/query_snippets/list.html
Normal file
13
rd_ui/app/views/query_snippets/list.html
Normal file
@ -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
rd_ui/app/views/query_snippets/show.html
Normal file
36
rd_ui/app/views/query_snippets/show.html
Normal file
@ -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>
|
@ -17,6 +17,7 @@ from redash.handlers.widgets import WidgetResource, WidgetListResource
|
||||
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):
|
||||
@ -90,3 +91,6 @@ api.add_org_resource(WidgetResource, '/api/widgets/<int:widget_id>', endpoint='w
|
||||
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
redash/handlers/query_snippets.py
Normal file
64
redash/handlers/query_snippets.py
Normal file
@ -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)]
|
@ -81,6 +81,8 @@ rules = ['/admin/<anything>/<whatever>',
|
||||
'/users/<pk>',
|
||||
'/destinations',
|
||||
'/destinations/<pk>',
|
||||
'/query_snippets',
|
||||
'/query_snippets/<pk>',
|
||||
'/groups',
|
||||
'/groups/<pk>',
|
||||
'/groups/<pk>/data_sources',
|
||||
|
@ -1196,6 +1196,35 @@ class AlertSubscription(ModelTimestampsMixin, BaseModel):
|
||||
return destination.notify(alert, query, user, new_state, app, host, options)
|
||||
|
||||
|
||||
class QuerySnippet(ModelTimestampsMixin, BaseModel, BelongsToOrgMixin):
|
||||
id = peewee.PrimaryKeyField()
|
||||
org = peewee.ForeignKeyField(Organization, related_name="query_snippets")
|
||||
trigger = peewee.CharField(unique=True)
|
||||
description = peewee.TextField()
|
||||
user = peewee.ForeignKeyField(User, related_name="query_snippets")
|
||||
snippet = peewee.TextField()
|
||||
|
||||
class Meta:
|
||||
db_table = 'query_snippets'
|
||||
|
||||
@classmethod
|
||||
def all(cls, org):
|
||||
return cls.select().where(cls.org==org)
|
||||
|
||||
def to_dict(self):
|
||||
d = {
|
||||
'id': self.id,
|
||||
'trigger': self.trigger,
|
||||
'description': self.description,
|
||||
'snippet': self.snippet,
|
||||
'user': self.user.to_dict(),
|
||||
'updated_at': self.updated_at,
|
||||
'created_at': self.created_at
|
||||
}
|
||||
|
||||
return d
|
||||
|
||||
|
||||
all_models = (Organization, Group, DataSource, DataSourceGroup, User, QueryResult, Query, Alert, Dashboard, Visualization, Widget, Event, NotificationDestination, AlertSubscription, ApiKey)
|
||||
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user