From e86058a7447329b929a6eeac5b0ecadba1ecabd7 Mon Sep 17 00:00:00 2001 From: Alison Date: Sat, 24 Jun 2017 15:49:47 -0500 Subject: [PATCH] Expose data source version info in UI (re #89) --- client/app/pages/data-sources/show.js | 21 ++++++++++++++++ .../pages/queries/get-data-source-version.js | 19 +++++++++++++++ client/app/pages/queries/query.html | 3 +-- client/app/pages/queries/view.js | 1 + client/app/services/data-source.js | 3 +++ redash/cli/data_sources.py | 24 +++++++++++++++++++ redash/handlers/api.py | 3 ++- redash/handlers/data_sources.py | 13 ++++++++++ redash/query_runner/__init__.py | 23 ++++++++++++++++++ redash/query_runner/mysql.py | 2 ++ redash/query_runner/pg.py | 4 ++++ 11 files changed, 113 insertions(+), 3 deletions(-) create mode 100644 client/app/pages/queries/get-data-source-version.js diff --git a/client/app/pages/data-sources/show.js b/client/app/pages/data-sources/show.js index bd7e9106fd..62f75987ca 100644 --- a/client/app/pages/data-sources/show.js +++ b/client/app/pages/data-sources/show.js @@ -52,11 +52,32 @@ function DataSourceCtrl( }); } + function getDataSourceVersion(callback) { + Events.record('test', 'data_source_version', $scope.dataSource.id); + + DataSource.version({ id: $scope.dataSource.id }, (httpResponse) => { + if (httpResponse.ok) { + const versionNumber = httpResponse.message; + toastr.success(`Success. Version: ${versionNumber}`); + } else { + toastr.error(httpResponse.message, 'Version Test Failed:', { timeOut: 10000 }); + } + callback(); + }, (httpResponse) => { + logger('Failed to get data source version: ', httpResponse.status, httpResponse.statusText, httpResponse); + toastr.error('Unknown error occurred while performing data source version test. Please try again later.', 'Data Source Version Test Failed:', { timeOut: 10000 }); + callback(); + }); + } + $scope.actions = [ { name: 'Delete', class: 'btn-danger', callback: deleteDataSource }, { name: 'Test Connection', class: 'btn-default', callback: testConnection, disableWhenDirty: true, }, + { + name: 'Test Data Source Version', class: 'btn-default', callback: getDataSourceVersion, disableWhenDirty: true, + }, ]; } diff --git a/client/app/pages/queries/get-data-source-version.js b/client/app/pages/queries/get-data-source-version.js new file mode 100644 index 0000000000..90df5cb61c --- /dev/null +++ b/client/app/pages/queries/get-data-source-version.js @@ -0,0 +1,19 @@ +function GetDataSourceVersionCtrl(Events, toastr, $scope, DataSource, $route) { + 'ngInject'; + + this.getDataSourceVersion = DataSource.version({ + id: $route.current.locals.query.data_source_id, + }); +} + +const GetDataSourceVersionInfo = { + bindings: { + onRefresh: '&', + }, + controller: GetDataSourceVersionCtrl, + template: '{{ $ctrl.getDataSourceVersion.message }}', +}; + +export default function (ngModule) { + ngModule.component('getDataSourceVersion', GetDataSourceVersionInfo); +} diff --git a/client/app/pages/queries/query.html b/client/app/pages/queries/query.html index deeb2c9ddc..a0d23c044a 100644 --- a/client/app/pages/queries/query.html +++ b/client/app/pages/queries/query.html @@ -86,8 +86,7 @@

{{dataSource.type_name}} documentation - {{dataSource.type_name}} - + diff --git a/client/app/pages/queries/view.js b/client/app/pages/queries/view.js index e8f90f68f2..965a80c2aa 100644 --- a/client/app/pages/queries/view.js +++ b/client/app/pages/queries/view.js @@ -296,6 +296,7 @@ function QueryViewCtrl( } $scope.dataSource = find($scope.dataSources, ds => ds.id === $scope.query.data_source_id); + document.getElementById('data-source-version').innerHTML = ''; }; $scope.setVisualizationTab = (visualization) => { diff --git a/client/app/services/data-source.js b/client/app/services/data-source.js index 39d8a78ade..d26c37b947 100644 --- a/client/app/services/data-source.js +++ b/client/app/services/data-source.js @@ -21,6 +21,9 @@ function DataSource($q, $resource, $http) { isArray: false, url: 'api/data_sources/:id/test', }, + version: { + method: 'GET', cache: false, isArray: false, url: 'api/data_sources/:id/version', + }, }; const DataSourceResource = $resource('api/data_sources/:id', { id: '@id' }, actions); diff --git a/redash/cli/data_sources.py b/redash/cli/data_sources.py index 79827132a0..0b7b18ff5d 100644 --- a/redash/cli/data_sources.py +++ b/redash/cli/data_sources.py @@ -67,6 +67,30 @@ def test(name, organization='default'): print("Couldn't find data source named: {}".format(name)) exit(1) +@manager.command() +@click.argument('name') +@click.option('--org', 'organization', default='default', + help="The organization the user belongs to " + "(leave blank for 'default').") +def get_data_source_version(name, organization='default'): + """Get version of data source connection by issuing a trivial query.""" + try: + org = models.Organization.get_by_slug(organization) + data_source = models.DataSource.query.filter( + models.DataSource.name == name, + models.DataSource.org == org).one() + print("Testing get connection data source version: {} (id={})".format( + name, data_source.id)) + try: + info = data_source.query_runner.get_data_source_version() + except Exception as e: + print("Failure: {}".format(e)) + exit(1) + else: + print(info) + except NoResultFound: + print("Couldn't find data source named: {}".format(name)) + exit(1) @manager.command() @click.argument('name', default=None, required=False) diff --git a/redash/handlers/api.py b/redash/handlers/api.py index a178e39a0e..64c0a0d13f 100644 --- a/redash/handlers/api.py +++ b/redash/handlers/api.py @@ -7,7 +7,7 @@ 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.data_sources import DataSourceTypeListResource, DataSourceListResource, DataSourceSchemaResource, DataSourceResource, DataSourcePauseResource, DataSourceTestResource +from redash.handlers.data_sources import DataSourceTypeListResource, DataSourceListResource, DataSourceSchemaResource, DataSourceResource, DataSourcePauseResource, DataSourceTestResource, DataSourceVersionResource from redash.handlers.events import EventResource from redash.handlers.queries import ( MyQueriesResource, QueryForkResource, QueryListResource, @@ -59,6 +59,7 @@ def json_representation(data, code, headers=None): api.add_org_resource(DataSourceSchemaResource, '/api/data_sources//schema') api.add_org_resource(DataSourcePauseResource, '/api/data_sources//pause') api.add_org_resource(DataSourceTestResource, '/api/data_sources//test') +api.add_org_resource(DataSourceVersionResource, '/api/data_sources//version') api.add_org_resource(DataSourceResource, '/api/data_sources/', endpoint='data_source') api.add_org_resource(GroupListResource, '/api/groups', endpoint='groups') diff --git a/redash/handlers/data_sources.py b/redash/handlers/data_sources.py index 230673aea2..99a31d378f 100644 --- a/redash/handlers/data_sources.py +++ b/redash/handlers/data_sources.py @@ -192,3 +192,16 @@ def post(self, data_source_id): return {"message": unicode(e), "ok": False} else: return {"message": "success", "ok": True} + +class DataSourceVersionResource(BaseResource): + def get(self, data_source_id): + data_source = get_object_or_404(models.DataSource.get_by_id_and_org, data_source_id, self.current_org) + require_access(data_source.groups, self.current_user, view_only) + try: + version_info = data_source.query_runner.get_data_source_version() + except Exception as e: + return {"message": unicode(e), "ok": False} + else: + return {"message": version_info, "ok": True} + + diff --git a/redash/query_runner/__init__.py b/redash/query_runner/__init__.py index 61ddb257c5..c839d92493 100644 --- a/redash/query_runner/__init__.py +++ b/redash/query_runner/__init__.py @@ -52,6 +52,7 @@ class NotSupported(Exception): class BaseQueryRunner(object): noop_query = None default_doc_url = None + data_source_version_query = None def __init__(self, configuration): self.syntax = 'sql' @@ -77,6 +78,28 @@ def annotate_query(cls): def configuration_schema(cls): return {} + def get_data_source_version(self): + if self.data_source_version_query is None: + raise NotImplementedError + data, error = self.run_query(self.data_source_version_query, None) + + if error is not None: + raise Exception(error) + + try: + version = json.loads(data)['rows'][0]['version'] + except KeyError as e: + raise Exception(e) + + if self.data_source_version_post_process == "split by space take second": + version = version.split(" ")[1] + elif self.data_source_version_post_process == "split by space take last": + version = version.split(" ")[-1] + elif self.data_source_version_post_process == "none": + version = version + + return version + def test_connection(self): if self.noop_query is None: raise NotImplementedError() diff --git a/redash/query_runner/mysql.py b/redash/query_runner/mysql.py index 8b3fed0300..525dd3d054 100644 --- a/redash/query_runner/mysql.py +++ b/redash/query_runner/mysql.py @@ -30,6 +30,8 @@ class Mysql(BaseSQLQueryRunner): noop_query = "SELECT 1" default_doc_url = 'https://dev.mysql.com/doc/refman/5.7/en/' + data_source_version_query = "select version()" + data_source_version_post_process = "none" @classmethod def configuration_schema(cls): diff --git a/redash/query_runner/pg.py b/redash/query_runner/pg.py index 1b723dde8f..5e6544101c 100644 --- a/redash/query_runner/pg.py +++ b/redash/query_runner/pg.py @@ -48,6 +48,8 @@ def _wait(conn, timeout=None): class PostgreSQL(BaseSQLQueryRunner): noop_query = "SELECT 1" default_doc_url = "https://www.postgresql.org/docs/current/" + data_source_version_query = "select version()" + data_source_version_post_process = "split by space take second" @classmethod def configuration_schema(cls): @@ -187,6 +189,8 @@ def run_query(self, query, user): class Redshift(PostgreSQL): default_doc_url = ("http://docs.aws.amazon.com/redshift/latest/" "dg/cm_chap_SQLCommandRef.html") + data_source_version_query = "select version()" + data_source_version_post_process = "split by space take last" @classmethod def type(cls):