diff --git a/setup.py b/setup.py index a915c0c..22a7eaa 100644 --- a/setup.py +++ b/setup.py @@ -23,7 +23,8 @@ 'redash.extensions': [ 'dockerflow = redash_stmo.dockerflow:dockerflow', 'datasource_health = redash_stmo.health:datasource_health', - 'datasource_link = redash_stmo.datasource_link:datasource_link' + 'datasource_link = redash_stmo.datasource_link:datasource_link', + 'datasource_version = redash_stmo.datasource_version:datasource_version', ], }, classifiers=[ diff --git a/src/redash_stmo/datasource_link.py b/src/redash_stmo/datasource_link.py index 0ae3f75..771c6f5 100644 --- a/src/redash_stmo/datasource_link.py +++ b/src/redash_stmo/datasource_link.py @@ -1,8 +1,9 @@ +from redash_stmo.resources import add_resource + from redash.models import DataSource -from redash.handlers.api import api from redash.handlers.base import BaseResource, get_object_or_404 from redash.permissions import require_access, view_only -from redash.query_runner import BaseQueryRunner, query_runners +from redash.query_runner import query_runners DATASOURCE_URLS = { "bigquery": "https://cloud.google.com/bigquery/docs/reference/legacy-sql", @@ -62,7 +63,4 @@ def datasource_link(app=None): "title": "Documentation URL", "default": DATASOURCE_URLS[runner_type]}) - # After api.init_app() is called, api.app should be set by Flask (but it's not) so that - # further calls to add_resource() are handled immediately for the given app. - api.app = app - api.add_org_resource(DataSourceLinkResource, '/api/data_sources//link') + add_resource(app, DataSourceLinkResource, '/api/data_sources//link') diff --git a/src/redash_stmo/datasource_version.py b/src/redash_stmo/datasource_version.py new file mode 100644 index 0000000..c020956 --- /dev/null +++ b/src/redash_stmo/datasource_version.py @@ -0,0 +1,63 @@ +import json +import logging + +from redash_stmo.resources import add_resource + +from redash.models import DataSource +from redash.handlers.base import BaseResource, get_object_or_404 +from redash.permissions import require_access, view_only + +logger = logging.getLogger(__name__) + + +DATASOURCE_VERSION_PARSE_INFO = { + "pg": { + "version_query": "select version()", + "delimiter": " ", + "index": 1 + }, + "redshift": { + "version_query": "select version()", + "delimiter": " ", + "index": -1 + }, + "mysql": { + "version_query": "select version()", + "delimiter": "-", + "index": 0 + } +} + +class DataSourceVersionResource(BaseResource): + def get(self, data_source_id): + data_source = get_object_or_404( + DataSource.get_by_id_and_org, + data_source_id, + self.current_org + ) + require_access(data_source.groups, self.current_user, view_only) + version_info = get_data_source_version(data_source.query_runner) + return {"version": version_info} + +def get_data_source_version(query_runner): + parse_info = DATASOURCE_VERSION_PARSE_INFO.get(query_runner.type()) + if parse_info is None: + return None + + data, error = query_runner.run_query(parse_info["version_query"], None) + if error is not None: + logger.error( + "Unable to run version query for %s: %s", query_runner.type(), error) + return None + try: + version = json.loads(data)['rows'][0]['version'] + except (KeyError, IndexError) as err: + logger.exception( + "Unable to parse data source version for %s: %s", query_runner.type(), err) + return None + + version = version.split(parse_info["delimiter"])[parse_info["index"]] + return version + +def datasource_version(app=None): + add_resource(app, DataSourceVersionResource, '/api/data_sources//version') diff --git a/src/redash_stmo/datasource_version/bundle/datasource_version.js b/src/redash_stmo/datasource_version/bundle/datasource_version.js new file mode 100644 index 0000000..602e80a --- /dev/null +++ b/src/redash_stmo/datasource_version/bundle/datasource_version.js @@ -0,0 +1,57 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { react2angular } from 'react2angular'; + +class DatasourceVersion extends React.Component { + static propTypes = { + clientConfig: PropTypes.object.isRequired, + datasourceId: PropTypes.number.isRequired, + } + + constructor(props) { + super(props); + this.state = { + version: '', + }; + } + + loadURLData() { + fetch(`${this.props.clientConfig.basePath}api/data_sources/${this.props.datasourceId}/version`) + .then((response) => { + if (response.status === 200) { + return response.json(); + } + return {}; + }) + .catch(error => { + console.error(`Error loading data source version: ${error}`); + return {}; + }) + .then((json) => { + this.setState({ version: json.version }); + }); + } + + componentDidMount() { + this.loadURLData(); + } + + componentDidUpdate(prevProps) { + if (this.props.datasourceId !== prevProps.datasourceId) { + this.loadURLData(); + } + } + + render() { + if (!this.state.version) { + return null; + } + return ( + {this.state.version} + ); + } +} + +export default function init(ngModule) { + ngModule.component('datasourceVersion', react2angular(DatasourceVersion, ['datasourceId'], ['clientConfig'])); +} diff --git a/src/redash_stmo/resources.py b/src/redash_stmo/resources.py new file mode 100644 index 0000000..1289f28 --- /dev/null +++ b/src/redash_stmo/resources.py @@ -0,0 +1,9 @@ +from redash.handlers import api + +def add_resource(app, resource, endpoint): + """ + After api.init_app() is called, api.app should be set by Flask (but it's not) so that + further calls to add_resource() are handled immediately for the given app. + """ + api.app = app + api.add_org_resource(resource, endpoint) diff --git a/tests/test_datasource_link.py b/tests/test_datasource_link.py index 852ec1c..eb2099e 100644 --- a/tests/test_datasource_link.py +++ b/tests/test_datasource_link.py @@ -5,7 +5,7 @@ from redash.models import DataSource from redash.query_runner.pg import PostgreSQL -from redash_stmo.datasource_link import datasource_link, BaseQueryRunner +from redash_stmo.datasource_link import datasource_link class TestDatasourceLink(BaseTestCase): diff --git a/tests/test_datasource_version.py b/tests/test_datasource_version.py new file mode 100644 index 0000000..2ec84b6 --- /dev/null +++ b/tests/test_datasource_version.py @@ -0,0 +1,70 @@ +import json +import mock + +from tests import BaseTestCase +from flask import Flask + +from redash.models import DataSource +from redash.query_runner.pg import PostgreSQL +from redash_stmo.datasource_version import datasource_version + + +class TestDatasourceVersion(BaseTestCase): + EXPECTED_DOC_URL = "www.example.com" + def setUp(self): + super(TestDatasourceVersion, self).setUp() + self.admin = self.factory.create_admin() + self.data_source = self.factory.create_data_source() + self.patched_run_query = self._setup_mock('redash.query_runner.pg.PostgreSQL.run_query') + self.patched_runner_type = self._setup_mock('redash.query_runner.pg.PostgreSQL.type') + datasource_version(self.app) + + def _setup_mock(self, function_to_patch): + patcher = mock.patch(function_to_patch) + patched_function = patcher.start() + self.addCleanup(patcher.stop) + return patched_function + + def _test_expected_version_returned(self, expected_version, version_string, runner_type): + self.patched_runner_type.return_value = runner_type + self.patched_run_query.return_value = (json.dumps({ + "rows": + [{ "version": version_string.format(version=expected_version) }] + }), None) + rv = self.make_request('get', '/api/data_sources/{}/version'.format(self.data_source.id), user=self.admin) + self.assertEqual(200, rv.status_code) + self.assertEqual(rv.json['version'], expected_version) + + def test_gets_postgres_version(self): + RUNNER_TYPE = "pg" + DATASOURCE_VERSION = "9.5.10" + VERSION_STRING = ( + "PostgreSQL {version} on x86_64-pc-linux-gnu, compiled by gcc " + "(GCC) 4.8.3 20140911 (Red Hat 4.8.3-9), 64-bit" + ) + self._test_expected_version_returned(DATASOURCE_VERSION, VERSION_STRING, RUNNER_TYPE) + + def test_gets_redshift_version(self): + RUNNER_TYPE = "redshift" + DATASOURCE_VERSION = "1.0.3688" + VERSION_STRING = ( + "PostgreSQL 8.0.2 on i686-pc-linux-gnu, compiled by GCC " + "gcc (GCC) 3.4.2 20041017 (Red Hat 3.4.2-6.fc3), Redshift {version}" + ) + self._test_expected_version_returned(DATASOURCE_VERSION, VERSION_STRING, RUNNER_TYPE) + + def test_gets_mysql_version(self): + RUNNER_TYPE = "mysql" + DATASOURCE_VERSION = "5.7.16" + VERSION_STRING = "{version}-log" + self._test_expected_version_returned(DATASOURCE_VERSION, VERSION_STRING, RUNNER_TYPE) + + def test_unexpected_json(self): + self.patched_runner_type.return_value = "pg" + self.patched_run_query.return_value = (json.dumps({ + "rows": + [{ "bad_json": "foo" }] + }), None) + rv = self.make_request('get', '/api/data_sources/{}/version'.format(self.data_source.id), user=self.admin) + self.assertEqual(200, rv.status_code) + self.assertEqual(rv.json['version'], None)