Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: schema browser and simple autocomplete #399

Merged
merged 12 commits into from
Apr 2, 2015
4 changes: 3 additions & 1 deletion rd_ui/app/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
<link rel="stylesheet" href="/bower_components/angular-ui-select/dist/select.css">
<link rel="stylesheet" href="/bower_components/pace/themes/pace-theme-minimal.css">
<link rel="stylesheet" href="/bower_components/font-awesome/css/font-awesome.css">
<link rel="stylesheet" href="/bower_components/codemirror/addon/hint/show-hint.css">
<link rel="stylesheet" href="/styles/redash.css">
<!-- endbuild -->
</head>
Expand Down Expand Up @@ -105,9 +106,10 @@
<script src="/bower_components/codemirror/lib/codemirror.js"></script>
<script src="/bower_components/codemirror/addon/edit/matchbrackets.js"></script>
<script src="/bower_components/codemirror/addon/edit/closebrackets.js"></script>
<script src="/bower_components/codemirror/addon/hint/show-hint.js"></script>
<script src="/bower_components/codemirror/addon/hint/anyword-hint.js"></script>
<script src="/bower_components/codemirror/mode/sql/sql.js"></script>
<script src="/bower_components/codemirror/mode/javascript/javascript.js"></script>
<script src="/bower_components/angular-ui-codemirror/ui-codemirror.js"></script>
<script src="/bower_components/highcharts/highcharts.js"></script>
<script src="/bower_components/highcharts/modules/exporting.js"></script>
<script src="/bower_components/gridster/dist/jquery.gridster.js"></script>
Expand Down
1 change: 0 additions & 1 deletion rd_ui/app/scripts/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ angular.module('redash', [
'redash.services',
'redash.renderers',
'redash.visualization',
'ui.codemirror',
'highchart',
'ui.select2',
'angular-growl',
Expand Down
23 changes: 23 additions & 0 deletions rd_ui/app/scripts/controllers/query_view.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,27 @@
}

$scope.query = $route.current.locals.query;

var updateSchema = function() {
$scope.hasSchema = false;
$scope.editorSize = "col-md-12";
var dataSourceId = $scope.query.data_source_id || $scope.dataSources[0].id;
DataSource.getSchema({id: dataSourceId}, function(data) {
if (data && data.length > 0) {
$scope.schema = data;
_.each(data, function(table) {
table.collapsed = true;
});

$scope.editorSize = "col-md-9";
$scope.hasSchema = true;
} else {
$scope.hasSchema = false;
$scope.editorSize = "col-md-12";
}
});
}

Events.record(currentUser, 'view', 'query', $scope.query.id);
getQueryResult();
$scope.queryExecuting = false;
Expand All @@ -27,6 +48,7 @@
$scope.canViewSource = currentUser.hasPermission('view_source');

$scope.dataSources = DataSource.get(function(dataSources) {
updateSchema();
$scope.query.data_source_id = $scope.query.data_source_id || dataSources[0].id;
});

Expand Down Expand Up @@ -126,6 +148,7 @@
});
}

updateSchema();
$scope.executeQuery();
};

Expand Down
80 changes: 67 additions & 13 deletions rd_ui/app/scripts/directives/query_directives.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
restrict: 'E',
template: '<span ng-show="query.id && canViewSource">\
<a ng-show="!sourceMode"\
ng-href="{{query.id}}/source#{{selectedTab}}">Show Source\
ng-href="/queries/{{query.id}}/source#{{selectedTab}}">Show Source\
</a>\
<a ng-show="sourceMode"\
ng-href="/queries/{{query.id}}#{{selectedTab}}">Hide Source\
Expand Down Expand Up @@ -63,26 +63,80 @@
restrict: 'E',
scope: {
'query': '=',
'lock': '='
'lock': '=',
'schema': '='
},
template: '<textarea\
ui-codemirror="editorOptions"\
ng-model="query.query">',
link: function($scope) {
$scope.editorOptions = {
template: '<textarea></textarea>',
link: {
pre: function ($scope, element) {
var textarea = element.children()[0];
var editorOptions = {
mode: 'text/x-sql',
lineWrapping: true,
lineNumbers: true,
readOnly: false,
matchBrackets: true,
autoCloseBrackets: true
};
autoCloseBrackets: true,
extraKeys: {"Ctrl-Space": "autocomplete"}
};

$scope.$watch('lock', function(locked) {
$scope.editorOptions.readOnly = locked ? 'nocursor' : false;
});
var additionalHints = [];

CodeMirror.commands.autocomplete = function(cm) {
var hinter = function(editor, options) {
var hints = CodeMirror.hint.anyword(editor, options);
var cur = editor.getCursor(), token = editor.getTokenAt(cur).string;

hints.list = _.union(hints.list, _.filter(additionalHints, function (h) {
return h.search(token) === 0;
}));

return hints;
};

// CodeMirror.showHint(cm, CodeMirror.hint.anyword);
CodeMirror.showHint(cm, hinter);
};

var codemirror = CodeMirror.fromTextArea(textarea, editorOptions);

codemirror.on('change', function(instance) {
var newValue = instance.getValue();

if (newValue !== $scope.query.query) {
$scope.$evalAsync(function() {
$scope.query.query = newValue;
});
}
});

$scope.$watch('query.query', function () {
if ($scope.query.query !== codemirror.getValue()) {
codemirror.setValue($scope.query.query);
}
});

$scope.$watch('schema', function (schema) {
if (schema) {
var keywords = [];
_.each(schema, function (table) {
keywords.push(table.name);
_.each(table.columns, function (c) {
keywords.push(c);
});
});

additionalHints = _.unique(keywords);
}
});

$scope.$watch('lock', function (locked) {
var readOnly = locked ? 'nocursor' : false;
codemirror.setOption('readOnly', readOnly);
});
}
}
}
};
}

function queryFormatter($http) {
Expand Down
7 changes: 6 additions & 1 deletion rd_ui/app/scripts/services/resources.js
Original file line number Diff line number Diff line change
Expand Up @@ -480,7 +480,12 @@


var DataSource = function ($resource) {
var DataSourceResource = $resource('/api/data_sources/:id', {id: '@id'}, {'get': {'method': 'GET', 'cache': true, 'isArray': true}});
var actions = {
'get': {'method': 'GET', 'cache': true, 'isArray': true},
'getSchema': {'method': 'GET', 'cache': true, 'isArray': true, 'url': '/api/data_sources/:id/schema'}
};

var DataSourceResource = $resource('/api/data_sources/:id', {id: '@id'}, actions);

return DataSourceResource;
}
Expand Down
14 changes: 13 additions & 1 deletion rd_ui/app/styles/redash.css
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ li.widget:hover {
/* CodeMirror */
.CodeMirror {
border: 1px solid #eee;
height: auto;
/*height: auto;*/
min-height: 300px;
margin-bottom: 10px;
}
Expand Down Expand Up @@ -308,6 +308,18 @@ counter-renderer counter-name {
height: 100%;
}

.schema-browser {
height: 300px;
overflow: scroll;
}

div.table-name {
overflow: scroll;
text-overflow: ellipsis;
white-space: nowrap;
cursor: pointer;
}

/*
bootstrap's hidden-xs class adds display:block when not hidden
use this class when you need to keep the original display value
Expand Down
34 changes: 23 additions & 11 deletions rd_ui/app/views/query.html
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,9 @@ <h2>

<hr>

<div class="row">
<div class="col-lg-12">
<div ng-show="sourceMode">
<div class="row" ng-if="sourceMode">
<div ng-class="editorSize">
<div>
<p>
<button type="button" class="btn btn-primary btn-xs" ng-disabled="queryExecuting" ng-click="executeQuery()">
<span class="glyphicon glyphicon-play"></span> Execute
Expand All @@ -77,27 +77,39 @@ <h2>
</button>
</span>
</p>
</div>

<!-- code editor -->
<div ng-show="sourceMode">
<p>
<query-editor query="query" lock="queryFormatting"></query-editor>
<query-editor query="query" schema="schema" lock="queryFormatting"></query-editor>
</p>
<hr>
</div>
</div>
<div class="col-md-3" ng-show="hasSchema">
<div>
<input type="text" placeholder="Search schema..." class="form-control" ng-model="schemaFilter">
</div>
<div class="schema-browser">
<div ng-repeat="table in schema | filter:schemaFilter">
<div class="table-name" ng-click="table.collapsed = !table.collapsed">
<i class="fa fa-table"></i> <strong><span title="{{table.name}}">{{table.name}}</span></strong>
</div>
<div collapse="table.collapsed">
<div ng-repeat="column in table.columns | filter:schemaFilter" style="padding-left:16px;">{{column}}</div>
</div>
</div>
</div>
</div>

</div>

</div>
<hr ng-if="sourceMode">
<div class="row">
<div class="col-lg-3 rd-hidden-xs">
<p>
<span class="glyphicon glyphicon-user"></span>
<span class="text-muted">Created By </span>
<strong>{{query.user.name}}</strong>
</p>
<p ng-if="query.user.id != query.last_modified_by.id">
<p ng-if="query.last_modified_by && query.user.id != query.last_modified_by.id">
<span class="glyphicon glyphicon-user"></span>
<span class="text-muted">Last Modified By </span>
<strong>{{query.last_modified_by.name}}</strong>
Expand Down Expand Up @@ -190,7 +202,7 @@ <h4 class="modal-title">Query Archive</h4>
<rd-tab tab-id="{{vis.id}}" name="{{vis.name}}" ng-if="vis.type!='TABLE'" ng-repeat="vis in query.visualizations">
<span class="remove" ng-click="deleteVisualization($event, vis)" ng-show="canEdit"> &times;</span>
</rd-tab>
<rd-tab tab-id="add" name="&plus; New" removeable="true" ng-show="canEdit"></rd-tab>
<rd-tab tab-id="add" name="&plus; New Visualization" removeable="true" ng-show="canEdit"></rd-tab>
<li ng-if="!sourceMode" class="rd-tab-btn"><button class="btn btn-sm btn-default" ng-click="executeQuery()" ng-disabled="queryExecuting" title="Refresh Dataset"><span class="glyphicon glyphicon-refresh"></span></button></li>
</ul>
</div>
Expand Down
2 changes: 1 addition & 1 deletion rd_ui/bower.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"es5-shim": "2.0.8",
"angular-moment": "0.2.0",
"moment": "2.1.0",
"angular-ui-codemirror": "0.0.5",
"codemirror": "4.8.0",
"highcharts": "3.0.10",
"underscore": "1.5.1",
"pivottable": "~1.1.1",
Expand Down
3 changes: 0 additions & 3 deletions redash/cache.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
from flask import make_response
from functools import update_wrapper

ONE_YEAR = 60 * 60 * 24 * 365.25

headers = {
Expand Down
10 changes: 9 additions & 1 deletion redash/controllers.py
Original file line number Diff line number Diff line change
Expand Up @@ -219,10 +219,18 @@ def post(self):

return datasource.to_dict()


api.add_resource(DataSourceListAPI, '/api/data_sources', endpoint='data_sources')


class DataSourceSchemaAPI(BaseResource):
def get(self, data_source_id):
data_source = models.DataSource.get_by_id(data_source_id)
schema = data_source.get_schema()

return schema

api.add_resource(DataSourceSchemaAPI, '/api/data_sources/<data_source_id>/schema')

class DashboardRecentAPI(BaseResource):
def get(self):
return [d.to_dict() for d in models.Dashboard.recent(current_user.id).limit(20)]
Expand Down
20 changes: 19 additions & 1 deletion redash/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
from flask.ext.login import UserMixin, AnonymousUserMixin
import psycopg2

from redash import utils, settings
from redash import utils, settings, redis_connection
from redash.query_runner import get_query_runner


class Database(object):
Expand Down Expand Up @@ -241,6 +242,23 @@ def to_dict(self):
'type': self.type
}

def get_schema(self, refresh=False):
key = "data_source:schema:{}".format(self.id)

cache = None
if not refresh:
cache = redis_connection.get(key)

if cache is None:
query_runner = get_query_runner(self.type, self.options)
schema = sorted(query_runner.get_schema(), key=lambda t: t['name'])

redis_connection.set(key, json.dumps(schema))
else:
schema = json.loads(cache)

return schema

@classmethod
def all(cls):
return cls.select().order_by(cls.id.asc())
Expand Down
3 changes: 3 additions & 0 deletions redash/query_runner/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ def configuration_schema(cls):
def run_query(self, query):
raise NotImplementedError()

def get_schema(self):
return []

@classmethod
def to_dict(cls):
return {
Expand Down
Loading