Skip to content

Commit

Permalink
Merge pull request #498 from EverythingMe/feature/ds_admin
Browse files Browse the repository at this point in the history
Feature: datasources web admin (closes #193)
  • Loading branch information
arikfr committed Jul 26, 2015
2 parents 5f5774d + 076710f commit c7d30c8
Show file tree
Hide file tree
Showing 27 changed files with 410 additions and 127 deletions.
63 changes: 0 additions & 63 deletions bin/test_multithreading.py

This file was deleted.

18 changes: 18 additions & 0 deletions migrations/0010_allow_deleting_datasources.py
@@ -0,0 +1,18 @@
from playhouse.migrate import PostgresqlMigrator, migrate

from redash.models import db

if __name__ == '__main__':
db.connect_db()
migrator = PostgresqlMigrator(db.database)

with db.database.transaction():
migrate(
migrator.drop_not_null('queries', 'data_source_id'),
)

db.close_db(None)




44 changes: 44 additions & 0 deletions migrations/0011_migrate_bigquery_to_json.py
@@ -0,0 +1,44 @@
from base64 import b64encode
import json
from redash.models import DataSource


def convert_p12_to_pem(p12file):
from OpenSSL import crypto
with open(p12file, 'rb') as f:
p12 = crypto.load_pkcs12(f.read(), "notasecret")

return crypto.dump_privatekey(crypto.FILETYPE_PEM, p12.get_privatekey())

if __name__ == '__main__':

for ds in DataSource.all():

if ds.type == 'bigquery':
options = json.loads(ds.options)

if 'jsonKeyFile' in options:
continue

new_options = {
'projectId': options['projectId'],
'jsonKeyFile': b64encode(json.dumps({
'client_email': options['serviceAccount'],
'private_key': convert_p12_to_pem(options['privateKey'])
}))
}

ds.options = json.dumps(new_options)
ds.save()
elif ds.type == 'google_spreadsheets':
options = json.loads(ds.options)
if 'jsonKeyFile' in options:
continue

with open(options['credentialsFilePath']) as f:
new_options = {
'jsonKeyFile': b64encode(f.read())
}

ds.options = json.dumps(new_options)
ds.save()
8 changes: 7 additions & 1 deletion rd_ui/app/index.html
Expand Up @@ -76,6 +76,9 @@
<li>
<a href="/alerts">Alerts</a>
</li>
<li ng-show="currentUser.hasPermission('admin')">
<a href="/data_sources">Data Sources</a>
</li>
</ul>
<form class="navbar-form navbar-left" role="search" ng-submit="searchQueries()">
<div class="form-group">
Expand Down Expand Up @@ -133,6 +136,7 @@
<script src="/bower_components/angular-ui-select/dist/select.js"></script>
<script src="/bower_components/underscore.string/lib/underscore.string.js"></script>
<script src="/bower_components/marked/lib/marked.js"></script>
<script src="/bower_components/angular-base64-upload/dist/angular-base64-upload.js"></script>
<script src="/scripts/ng_highchart.js"></script>
<script src="/scripts/ng_smart_table.js"></script>
<script src="/bower_components/angular-ui-bootstrap-bower/ui-bootstrap-tpls.js"></script>
Expand All @@ -154,6 +158,7 @@
<script src="/scripts/controllers/controllers.js"></script>
<script src="/scripts/controllers/dashboard.js"></script>
<script src="/scripts/controllers/admin_controllers.js"></script>
<script src="/scripts/controllers/data_sources.js"></script>
<script src="/scripts/controllers/query_view.js"></script>
<script src="/scripts/controllers/query_source.js"></script>
<script src="/scripts/visualizations/base.js"></script>
Expand All @@ -165,6 +170,7 @@
<script src="/scripts/visualizations/pivot.js"></script>
<script src="/scripts/directives/directives.js"></script>
<script src="/scripts/directives/query_directives.js"></script>
<script src="/scripts/directives/data_source_directives.js"></script>
<script src="/scripts/directives/dashboard_directives.js"></script>
<script src="/scripts/filters.js"></script>
<script src="/scripts/controllers/alerts.js"></script>
Expand All @@ -182,7 +188,7 @@

currentUser.hasPermission = function(permission) {
return this.permissions.indexOf(permission) != -1;
}
};

{{ analytics|safe }}
</script>
Expand Down
12 changes: 11 additions & 1 deletion rd_ui/app/scripts/app.js
Expand Up @@ -14,7 +14,8 @@ angular.module('redash', [
'smartTable.table',
'ngResource',
'ngRoute',
'ui.select'
'ui.select',
'naif.base64'
]).config(['$routeProvider', '$locationProvider', '$compileProvider', 'growlProvider',
function ($routeProvider, $locationProvider, $compileProvider, growlProvider) {
if (featureFlags.clientSideMetrics) {
Expand Down Expand Up @@ -90,6 +91,15 @@ angular.module('redash', [
controller: 'AlertCtrl'
});

$routeProvider.when('/data_sources/:dataSourceId', {
templateUrl: '/views/data_sources/edit.html',
controller: 'DataSourceCtrl'
});
$routeProvider.when('/data_sources', {
templateUrl: '/views/data_sources/list.html',
controller: 'DataSourcesCtrl'
});

$routeProvider.when('/', {
templateUrl: '/views/index.html',
controller: 'IndexCtrl'
Expand Down
2 changes: 1 addition & 1 deletion rd_ui/app/scripts/controllers/admin_controllers.js
Expand Up @@ -17,7 +17,7 @@
};

refresh();
}
};

angular.module('redash.admin_controllers', [])
.controller('AdminStatusCtrl', ['$scope', 'Events', '$http', '$timeout', AdminStatusCtrl])
Expand Down
47 changes: 47 additions & 0 deletions rd_ui/app/scripts/controllers/data_sources.js
@@ -0,0 +1,47 @@
(function () {
var DataSourcesCtrl = function ($scope, $location, growl, Events, DataSource) {
Events.record(currentUser, "view", "page", "admin/data_sources");
$scope.$parent.pageTitle = "Data Sources";

$scope.dataSources = DataSource.query();

$scope.openDataSource = function(datasource) {
$location.path('/data_sources/' + datasource.id);
};

$scope.deleteDataSource = function(event, datasource) {
event.stopPropagation();
Events.record(currentUser, "delete", "datasource", datasource.id);
datasource.$delete(function(resource) {
growl.addSuccessMessage("Data source deleted succesfully.");
this.$parent.dataSources = _.without(this.dataSources, resource);
}.bind(this), function(httpResponse) {
console.log("Failed to delete data source: ", httpResponse.status, httpResponse.statusText, httpResponse.data);
growl.addErrorMessage("Failed to delete data source.");
});
}
};

var DataSourceCtrl = function ($scope, $routeParams, $http, $location, Events, DataSource) {
Events.record(currentUser, "view", "page", "admin/data_source");
$scope.$parent.pageTitle = "Data Sources";

$scope.dataSourceId = $routeParams.dataSourceId;

if ($scope.dataSourceId == "new") {
$scope.dataSource = new DataSource({options: {}});
} else {
$scope.dataSource = DataSource.get({id: $routeParams.dataSourceId});
}

$scope.$watch('dataSource.id', function(id) {
if (id != $scope.dataSourceId && id !== undefined) {
$location.path('/data_sources/' + id).replace();
}
});
};

angular.module('redash.controllers')
.controller('DataSourcesCtrl', ['$scope', '$location', 'growl', 'Events', 'DataSource', DataSourcesCtrl])
.controller('DataSourceCtrl', ['$scope', '$routeParams', '$http', '$location', 'Events', 'DataSource', DataSourceCtrl])
})();
16 changes: 10 additions & 6 deletions rd_ui/app/scripts/controllers/query_view.js
Expand Up @@ -49,10 +49,13 @@
$scope.isQueryOwner = (currentUser.id === $scope.query.user.id) || currentUser.hasPermission('admin');
$scope.canViewSource = currentUser.hasPermission('view_source');

$scope.dataSources = DataSource.get(function(dataSources) {
$scope.dataSources = DataSource.query(function(dataSources) {
updateSchema();
$scope.query.data_source_id = $scope.query.data_source_id || dataSources[0].id;
$scope.dataSource = _.find(dataSources, function(ds) { return ds.id == $scope.query.data_source_id; });

if ($scope.query.isNew()) {
$scope.query.data_source_id = $scope.query.data_source_id || dataSources[0].id;
$scope.dataSource = _.find(dataSources, function(ds) { return ds.id == $scope.query.data_source_id; });
}
});

// in view mode, latest dataset is always visible
Expand Down Expand Up @@ -101,6 +104,9 @@
};

$scope.executeQuery = function() {
if (!$scope.query.query) {
return;
}
getQueryResult(0);
$scope.lockButton(true);
$scope.cancelling = false;
Expand Down Expand Up @@ -154,9 +160,7 @@

updateSchema();
$scope.dataSource = _.find($scope.dataSources, function(ds) { return ds.id == $scope.query.data_source_id; });
if ($scope.query.query) {
$scope.executeQuery();
}
$scope.executeQuery();
};

$scope.setVisualizationTab = function (visualization) {
Expand Down
76 changes: 76 additions & 0 deletions rd_ui/app/scripts/directives/data_source_directives.js
@@ -0,0 +1,76 @@
(function () {
'use strict';

var directives = angular.module('redash.directives');

// Angular strips data- from the directive, so data-source-form becomes sourceForm...
directives.directive('sourceForm', ['$http', 'growl', function ($http, growl) {
return {
restrict: 'E',
replace: true,
templateUrl: '/views/data_sources/form.html',
scope: {
'dataSource': '='
},
link: function ($scope) {
var setType = function(types) {
if ($scope.dataSource.type === undefined) {
$scope.dataSource.type = types[0].type;
return types[0];
}

$scope.type = _.find(types, function (t) {
return t.type == $scope.dataSource.type;
});
};

$scope.files = {};

$scope.$watchCollection('files', function() {
_.each($scope.files, function(v, k) {
if (v) {
$scope.dataSource.options[k] = v.base64;
}
});
});

$http.get('/api/data_sources/types').success(function (types) {
setType(types);

$scope.dataSourceTypes = types;

_.each(types, function (type) {
_.each(type.configuration_schema.properties, function (prop, name) {
if (name == 'password' || name == 'passwd') {
prop.type = 'password';
}

if (_.string.endsWith(name, "File")) {
prop.type = 'file';
}

prop.required = _.contains(type.configuration_schema.required, name);
});
});
});

$scope.$watch('dataSource.type', function(current, prev) {
if (prev !== current) {
if (prev !== undefined) {
$scope.dataSource.options = {};
}
setType($scope.dataSourceTypes);
}
});

$scope.saveChanges = function() {
$scope.dataSource.$save(function() {
growl.addSuccessMessage("Saved.");
}, function() {
growl.addErrorMessage("Failed saving.");
});
}
}
}
}]);
})();

0 comments on commit c7d30c8

Please sign in to comment.