From 06e107c1f64c0603f810122203d77a8f8b3f7697 Mon Sep 17 00:00:00 2001 From: Marina Samuel Date: Thu, 16 Aug 2018 17:05:12 -0400 Subject: [PATCH] Closes #8: Add datasource link front- and backend extension code. --- MANIFEST.in | 1 + setup.py | 4 +- src/redash_stmo/datasource_link.py | 68 +++++++++++++++++++ .../datasource_link/bundle/datasource_link.js | 61 +++++++++++++++++ tests/test_datasource_link.py | 33 +++++++++ 5 files changed, 166 insertions(+), 1 deletion(-) create mode 100644 src/redash_stmo/datasource_link.py create mode 100644 src/redash_stmo/datasource_link/bundle/datasource_link.js create mode 100644 tests/test_datasource_link.py diff --git a/MANIFEST.in b/MANIFEST.in index 3527db6..f4cabef 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1 +1,2 @@ include AUTHORS.rst CHANGELOG.rst README.rst +recursive-include src/redash_stmo *.html *.js diff --git a/setup.py b/setup.py index 109c270..a915c0c 100644 --- a/setup.py +++ b/setup.py @@ -13,6 +13,7 @@ ], packages=find_packages(where='src'), package_dir={'': 'src'}, + include_package_data=True, description="Extensions to Redash by Mozilla", author='Mozilla Foundation', author_email='dev-webdev@lists.mozilla.org', @@ -21,7 +22,8 @@ entry_points={ 'redash.extensions': [ 'dockerflow = redash_stmo.dockerflow:dockerflow', - 'datasource_health = redash_stmo.health:datasource_health' + 'datasource_health = redash_stmo.health:datasource_health', + 'datasource_link = redash_stmo.datasource_link:datasource_link' ], }, classifiers=[ diff --git a/src/redash_stmo/datasource_link.py b/src/redash_stmo/datasource_link.py new file mode 100644 index 0000000..0ae3f75 --- /dev/null +++ b/src/redash_stmo/datasource_link.py @@ -0,0 +1,68 @@ +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 + +DATASOURCE_URLS = { + "bigquery": "https://cloud.google.com/bigquery/docs/reference/legacy-sql", + "Cassandra": "http://cassandra.apache.org/doc/latest/cql/index.html", + "dynamodb_sql": "https://dql.readthedocs.io/en/latest/", + "baseelasticsearch": "https://www.elastic.co/guide/en/elasticsearch/reference/current/index.html", + "google_spreadsheets": "http://redash.readthedocs.io/en/latest/datasources.html#google-spreadsheets", + "hive": "https://cwiki.apache.org/confluence/display/Hive/LanguageManual", + "impala": "http://www.cloudera.com/documentation/enterprise/latest/topics/impala_langref.html", + "influxdb": "https://docs.influxdata.com/influxdb/v1.0/query_language/spec/", + "jirajql": "https://confluence.atlassian.com/jirasoftwarecloud/advanced-searching-764478330.html", + "mongodb": "https://docs.mongodb.com/manual/reference/operator/query/", + "mssql": "https://msdn.microsoft.com/en-us/library/bb510741.aspx", + "mysql": "https://dev.mysql.com/doc/refman/5.7/en/", + "oracle": "http://docs.oracle.com/database/121/SQLRF/toc.htm", + "pg": "https://www.postgresql.org/docs/current/", + "redshift": "http://docs.aws.amazon.com/redshift/latest/dg/cm_chap_SQLCommandRef.html", + "presto": "https://prestodb.io/docs/current/", + "python": "http://redash.readthedocs.io/en/latest/datasources.html#python", + "insecure_script": "http://redash.readthedocs.io/en/latest/datasources.html#python", + "sqlite": "http://sqlite.org/lang.html", + "treasuredata": "https://docs.treasuredata.com/categories/hive", + "url": "http://redash.readthedocs.io/en/latest/datasources.html#url", + "vertica": ( + "https://my.vertica.com/docs/8.0.x/HTML/index.htm#Authoring/" + "ConceptsGuide/Other/SQLOverview.htm%3FTocPath%3DSQL" + "%2520Reference%2520Manual%7C_____1" + ) +} + + +class DataSourceLinkResource(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) + try: + result = { + "type_name": data_source.query_runner.name(), + "doc_url": data_source.options.get("doc_url", None) + } + except Exception as e: + return {"message": unicode(e), "ok": False} + else: + return {"message": result, "ok": True} + +def datasource_link(app=None): + for runner_type, runner_class in query_runners.items(): + if runner_type not in DATASOURCE_URLS: + continue + + runner_class.add_configuration_property("doc_url", { + "type": "string", + "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') diff --git a/src/redash_stmo/datasource_link/bundle/datasource_link.js b/src/redash_stmo/datasource_link/bundle/datasource_link.js new file mode 100644 index 0000000..60eebed --- /dev/null +++ b/src/redash_stmo/datasource_link/bundle/datasource_link.js @@ -0,0 +1,61 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { react2angular } from 'react2angular'; + +class DatasourceLink extends React.Component { + static propTypes = { + clientConfig: PropTypes.object.isRequired, + datasourceId: PropTypes.number.isRequired, + } + + constructor(props) { + super(props); + this.state = { + type_name: '', + doc_url: '', + }; + } + + loadURLData() { + fetch(`${this.props.clientConfig.basePath}api/data_sources/${this.props.datasourceId}/link`) + .then((response) => { + if (response.status === 200) { + return response.json(); + } + return {}; + }) + .catch(error => { + console.error(`Error loading data source URL: ${error}`); + return {}; + }) + .then((json) => { + const { type_name, doc_url } = json.message; + this.setState({ type_name, doc_url }); + }); + } + + componentDidMount() { + this.loadURLData(); + } + + componentDidUpdate(prevProps) { + if (this.props.datasourceId !== prevProps.datasourceId) { + this.loadURLData(); + } + } + + render() { + if (!this.state.doc_url) { + return null; + } + return ( + + {this.state.type_name} documentation + + ); + } +} + +export default function init(ngModule) { + ngModule.component('datasourceLink', react2angular(DatasourceLink, ['datasourceId'], ['clientConfig'])); +} diff --git a/tests/test_datasource_link.py b/tests/test_datasource_link.py new file mode 100644 index 0000000..852ec1c --- /dev/null +++ b/tests/test_datasource_link.py @@ -0,0 +1,33 @@ +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_link import datasource_link, BaseQueryRunner + + +class TestDatasourceLink(BaseTestCase): + EXPECTED_DOC_URL = "www.example.com" + def setUp(self): + super(TestDatasourceLink, self).setUp() + self.patched_query_runners = self._setup_mock('redash_stmo.datasource_link.query_runners') + self.patched_query_runners.return_value = {} + datasource_link(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_gets_datasource_link_and_type(self): + admin = self.factory.create_admin() + data_source = self.factory.create_data_source() + data_source.options["doc_url"] = self.EXPECTED_DOC_URL + + rv = self.make_request('get', '/api/data_sources/{}/link'.format(data_source.id), user=admin) + self.assertEqual(200, rv.status_code) + self.assertEqual(rv.json['message']['type_name'], data_source.query_runner.name()) + self.assertEqual(rv.json['message']["doc_url"], self.EXPECTED_DOC_URL)