diff --git a/client/app/pages/data-sources/list.html b/client/app/pages/data-sources/list.html index 6e0bf7f2f0..35803214a1 100644 --- a/client/app/pages/data-sources/list.html +++ b/client/app/pages/data-sources/list.html @@ -7,7 +7,7 @@
{{dataSource.name}} -

{{dataSource.name}}

+

{{dataSource.name}}

- {{dataSource.type_name}}
diff --git a/client/app/pages/queries/query.html b/client/app/pages/queries/query.html index eb7fb64122..9102e1d7a9 100644 --- a/client/app/pages/queries/query.html +++ b/client/app/pages/queries/query.html @@ -140,6 +140,8 @@

+ {{dataSource.type_name}} documentation + {{dataSource.type_name}}
diff --git a/redash/models.py b/redash/models.py index f2b34ff30d..3879a9493f 100644 --- a/redash/models.py +++ b/redash/models.py @@ -564,11 +564,12 @@ def to_dict(self, all=False, with_permissions_for=None): 'type': self.type, 'syntax': self.query_runner.syntax, 'paused': self.paused, - 'pause_reason': self.pause_reason + 'pause_reason': self.pause_reason, + 'type_name': self.query_runner.name(), } + schema = get_configuration_schema_for_query_runner_type(self.type) if all: - schema = get_configuration_schema_for_query_runner_type(self.type) self.options.set_schema(schema) d['options'] = self.options.to_dict(mask_secrets=True) d['queue_name'] = self.queue_name @@ -580,6 +581,11 @@ def to_dict(self, all=False, with_permissions_for=None): DataSourceGroup.group == with_permissions_for, DataSourceGroup.data_source == self).one()[0] + doc_url = self.options.get('doc_url', schema['properties'].get( + 'doc_url', {}).get('default')) + if doc_url: + d['options'] = {'doc_url': doc_url} + return d def __unicode__(self): @@ -654,6 +660,8 @@ def add_group(self, group, view_only=False): db.session.add(dsg) return dsg + setattr(self, 'data_source_groups', dsg) + def remove_group(self, group): db.session.query(DataSourceGroup).filter( DataSourceGroup.group == group, diff --git a/redash/query_runner/__init__.py b/redash/query_runner/__init__.py index fff1408949..61ddb257c5 100644 --- a/redash/query_runner/__init__.py +++ b/redash/query_runner/__init__.py @@ -51,6 +51,7 @@ class NotSupported(Exception): class BaseQueryRunner(object): noop_query = None + default_doc_url = None def __init__(self, configuration): self.syntax = 'sql' diff --git a/redash/query_runner/big_query.py b/redash/query_runner/big_query.py index f765978585..d153f9280a 100644 --- a/redash/query_runner/big_query.py +++ b/redash/query_runner/big_query.py @@ -80,6 +80,7 @@ def _get_query_results(jobs, project_id, job_id, start_index): class BigQuery(BaseQueryRunner): noop_query = "SELECT 1" + default_doc_url = "https://cloud.google.com/bigquery/docs/reference/legacy-sql" @classmethod def enabled(cls): @@ -117,6 +118,11 @@ def configuration_schema(cls): 'maximumBillingTier': { "type": "number", "title": "Maximum Billing Tier" + }, + "doc_url": { + "type": "string", + "title": "Documentation URL", + "default": cls.default_doc_url } }, 'required': ['jsonKeyFile', 'projectId'], diff --git a/redash/query_runner/cass.py b/redash/query_runner/cass.py index ca8e4537fe..ac216ca3c2 100644 --- a/redash/query_runner/cass.py +++ b/redash/query_runner/cass.py @@ -27,6 +27,7 @@ def default(self, o): class Cassandra(BaseQueryRunner): noop_query = "SELECT dateof(now()) FROM system.local" + default_doc_url = "http://cassandra.apache.org/doc/latest/cql/index.html" @classmethod def enabled(cls): @@ -65,6 +66,11 @@ def configuration_schema(cls): 'type': 'number', 'title': 'Timeout', 'default': 10 + }, + "doc_url": { + "type": "string", + "title": "Documentation URL", + "default": cls.default_doc_url } }, 'required': ['keyspace', 'host'] diff --git a/redash/query_runner/dynamodb_sql.py b/redash/query_runner/dynamodb_sql.py index dfd48a5840..941cc1a013 100644 --- a/redash/query_runner/dynamodb_sql.py +++ b/redash/query_runner/dynamodb_sql.py @@ -33,6 +33,9 @@ class DynamoDBSQL(BaseSQLQueryRunner): + noop_query = "SELECT 1" + default_doc_url = "https://dql.readthedocs.io/en/latest/" + @classmethod def configuration_schema(cls): return { @@ -47,6 +50,11 @@ def configuration_schema(cls): }, "secret_key": { "type": "string", + }, + "doc_url": { + "type": "string", + "title": "Documentation URL", + "default": cls.default_doc_url } }, "required": ["access_key", "secret_key"], diff --git a/redash/query_runner/elasticsearch.py b/redash/query_runner/elasticsearch.py index a32f788321..5c694a8b27 100644 --- a/redash/query_runner/elasticsearch.py +++ b/redash/query_runner/elasticsearch.py @@ -44,7 +44,8 @@ class BaseElasticSearch(BaseQueryRunner): - DEBUG_ENABLED = False + DEBUG_ENABLED = True + default_doc_url = "https://www.elastic.co/guide/en/elasticsearch/reference/current/index.html" @classmethod def configuration_schema(cls): @@ -62,6 +63,11 @@ def configuration_schema(cls): 'basic_auth_password': { 'type': 'string', 'title': 'Basic Auth Password' + }, + "doc_url": { + "type": "string", + "title": "Documentation URL", + "default": cls.default_doc_url } }, "secret": ["basic_auth_password"], diff --git a/redash/query_runner/google_spreadsheets.py b/redash/query_runner/google_spreadsheets.py index 33be529249..6c13044e0f 100644 --- a/redash/query_runner/google_spreadsheets.py +++ b/redash/query_runner/google_spreadsheets.py @@ -108,6 +108,7 @@ def parse_worksheet(worksheet): columns.append({ 'name': column_name, 'friendly_name': column_name, + 'type': TYPE_STRING }) @@ -139,6 +140,9 @@ def request(self, *args, **kwargs): class GoogleSpreadsheet(BaseQueryRunner): + default_doc_url = ("http://redash.readthedocs.io/en/latest/" + "datasources.html#google-spreadsheets") + @classmethod def annotate_query(cls): return False @@ -159,6 +163,11 @@ def configuration_schema(cls): 'jsonKeyFile': { "type": "string", 'title': 'JSON Key File' + }, + "doc_url": { + "type": "string", + "title": "Documentation URL", + "default": cls.default_doc_url } }, 'required': ['jsonKeyFile'], diff --git a/redash/query_runner/graphite.py b/redash/query_runner/graphite.py index 023ec04940..e2aaff6643 100644 --- a/redash/query_runner/graphite.py +++ b/redash/query_runner/graphite.py @@ -42,6 +42,11 @@ def configuration_schema(cls): 'verify': { 'type': 'boolean', 'title': 'Verify SSL certificate' + }, + "doc_url": { + "type": "string", + "title": "Documentation URL", + "default": cls.default_doc_url } }, 'required': ['url'], diff --git a/redash/query_runner/hive_ds.py b/redash/query_runner/hive_ds.py index 73be16021b..5c0acb649b 100644 --- a/redash/query_runner/hive_ds.py +++ b/redash/query_runner/hive_ds.py @@ -36,6 +36,8 @@ class Hive(BaseSQLQueryRunner): noop_query = "SELECT 1" + default_doc_url = ("https://cwiki.apache.org/confluence/display/Hive/" + "LanguageManual") @classmethod def configuration_schema(cls): @@ -53,6 +55,11 @@ def configuration_schema(cls): }, "username": { "type": "string" + }, + "doc_url": { + "type": "string", + "title": "Documentation URL", + "default": cls.default_doc_url } }, "required": ["host"] diff --git a/redash/query_runner/impala_ds.py b/redash/query_runner/impala_ds.py index d3c42112ce..c1f817eb47 100644 --- a/redash/query_runner/impala_ds.py +++ b/redash/query_runner/impala_ds.py @@ -36,6 +36,8 @@ class Impala(BaseSQLQueryRunner): noop_query = "show schemas" + default_doc_url = ("http://www.cloudera.com/documentation/enterprise/" + "latest/topics/impala_langref.html") @classmethod def configuration_schema(cls): @@ -66,6 +68,11 @@ def configuration_schema(cls): }, "timeout": { "type": "number" + }, + "doc_url": { + "type": "string", + "title": "Documentation URL", + "default": cls.default_doc_url } }, "required": ["host"], diff --git a/redash/query_runner/influx_db.py b/redash/query_runner/influx_db.py index 8360a3af72..ea3d4932aa 100644 --- a/redash/query_runner/influx_db.py +++ b/redash/query_runner/influx_db.py @@ -50,6 +50,8 @@ def _transform_result(results): class InfluxDB(BaseQueryRunner): noop_query = "show measurements limit 1" + default_doc_url = ("https://docs.influxdata.com/influxdb/v1.0/" + "query_language/spec/") @classmethod def configuration_schema(cls): @@ -58,6 +60,11 @@ def configuration_schema(cls): 'properties': { 'url': { 'type': 'string' + }, + "doc_url": { + "type": "string", + "title": "Documentation URL", + "default": cls.default_doc_url } }, 'required': ['url'] diff --git a/redash/query_runner/jql.py b/redash/query_runner/jql.py index 37b1f345c6..e3f9743547 100644 --- a/redash/query_runner/jql.py +++ b/redash/query_runner/jql.py @@ -139,6 +139,8 @@ def get_dict_output_field_name(cls,field_name, member_name): class JiraJQL(BaseQueryRunner): noop_query = '{"queryType": "count"}' + default_doc_url = ("https://confluence.atlassian.com/jirasoftwarecloud/" + "advanced-searching-764478330.html") @classmethod def configuration_schema(cls): @@ -154,6 +156,11 @@ def configuration_schema(cls): }, 'password': { 'type': 'string' + }, + "doc_url": { + "type": "string", + "title": "Documentation URL", + "default": cls.default_doc_url } }, 'required': ['url', 'username', 'password'], diff --git a/redash/query_runner/mongodb.py b/redash/query_runner/mongodb.py index e572167ed2..81ae8d9a63 100644 --- a/redash/query_runner/mongodb.py +++ b/redash/query_runner/mongodb.py @@ -118,6 +118,9 @@ def parse_results(results): class MongoDB(BaseQueryRunner): + default_doc_url = ("https://docs.mongodb.com/manual/reference/operator/" + "query/") + @classmethod def configuration_schema(cls): return { @@ -135,6 +138,11 @@ def configuration_schema(cls): 'type': 'string', 'title': 'Replica Set Name' }, + "doc_url": { + "type": "string", + "title": "Documentation URL", + "default": cls.default_doc_url + } }, 'required': ['connectionString', 'dbName'] } diff --git a/redash/query_runner/mssql.py b/redash/query_runner/mssql.py index bcc810fe4d..dd746d4334 100644 --- a/redash/query_runner/mssql.py +++ b/redash/query_runner/mssql.py @@ -35,6 +35,7 @@ def default(self, o): class SqlServer(BaseSQLQueryRunner): noop_query = "SELECT 1" + default_doc_url = "https://msdn.microsoft.com/en-us/library/bb510741.aspx" @classmethod def configuration_schema(cls): @@ -68,6 +69,11 @@ def configuration_schema(cls): "db": { "type": "string", "title": "Database Name" + }, + "doc_url": { + "type": "string", + "title": "Documentation URL", + "default": cls.default_doc_url } }, "required": ["db"], diff --git a/redash/query_runner/mysql.py b/redash/query_runner/mysql.py index 3679b694db..8b3fed0300 100644 --- a/redash/query_runner/mysql.py +++ b/redash/query_runner/mysql.py @@ -29,6 +29,7 @@ class Mysql(BaseSQLQueryRunner): noop_query = "SELECT 1" + default_doc_url = 'https://dev.mysql.com/doc/refman/5.7/en/' @classmethod def configuration_schema(cls): @@ -79,6 +80,11 @@ def configuration_schema(cls): 'ssl_key': { 'type': 'string', 'title': 'Path to private key file (SSL)' + }, + "doc_url": { + "type": "string", + "title": "Documentation URL", + "default": cls.default_doc_url } }) diff --git a/redash/query_runner/oracle.py b/redash/query_runner/oracle.py index 5bb8f70f2f..12448029d5 100644 --- a/redash/query_runner/oracle.py +++ b/redash/query_runner/oracle.py @@ -31,8 +31,10 @@ logger = logging.getLogger(__name__) + class Oracle(BaseSQLQueryRunner): noop_query = "SELECT 1 FROM dual" + default_doc_url = "http://docs.oracle.com/database/121/SQLRF/toc.htm" @classmethod def get_col_type(cls, col_type, scale): @@ -65,6 +67,11 @@ def configuration_schema(cls): "servicename": { "type": "string", "title": "DSN Service Name" + }, + "doc_url": { + "type": "string", + "title": "Documentation URL", + "default": cls.default_doc_url } }, "required": ["servicename", "user", "password", "host", "port"], diff --git a/redash/query_runner/pg.py b/redash/query_runner/pg.py index fe3375b896..6cfb2976e9 100644 --- a/redash/query_runner/pg.py +++ b/redash/query_runner/pg.py @@ -47,6 +47,7 @@ def _wait(conn, timeout=None): class PostgreSQL(BaseSQLQueryRunner): noop_query = "SELECT 1" + default_doc_url = "https://www.postgresql.org/docs/current/" @classmethod def configuration_schema(cls): @@ -75,6 +76,11 @@ def configuration_schema(cls): "type": "string", "title": "SSL Mode", "default": "prefer" + }, + "doc_url": { + "type": "string", + "title": "Documentation URL", + "default": cls.default_doc_url } }, "order": ['host', 'port', 'user', 'password'], @@ -179,6 +185,9 @@ def run_query(self, query, user): class Redshift(PostgreSQL): + default_doc_url = ("http://docs.aws.amazon.com/redshift/latest/" + "dg/cm_chap_SQLCommandRef.html") + @classmethod def type(cls): return "redshift" @@ -223,6 +232,11 @@ def configuration_schema(cls): "type": "string", "title": "SSL Mode", "default": "prefer" + }, + "doc_url": { + "type": "string", + "title": "Documentation URL", + "default": cls.default_doc_url } }, "order": ['host', 'port', 'user', 'password'], diff --git a/redash/query_runner/presto.py b/redash/query_runner/presto.py index d910a7c56b..6fbaad8f7b 100644 --- a/redash/query_runner/presto.py +++ b/redash/query_runner/presto.py @@ -33,6 +33,7 @@ class Presto(BaseQueryRunner): noop_query = 'SHOW TABLES' + default_doc_url = 'https://prestodb.io/docs/current/' @classmethod def configuration_schema(cls): @@ -53,6 +54,11 @@ def configuration_schema(cls): }, 'username': { 'type': 'string' + }, + "doc_url": { + "type": "string", + "title": "Documentation URL", + "default": cls.default_doc_url } }, 'required': ['host'] diff --git a/redash/query_runner/python.py b/redash/query_runner/python.py index f8e69f96ce..275b44fa16 100644 --- a/redash/query_runner/python.py +++ b/redash/query_runner/python.py @@ -46,6 +46,9 @@ class Python(BaseQueryRunner): 'tuple', 'set', 'list', 'dict', 'bool', ) + default_doc_url = ("http://redash.readthedocs.io/en/latest/" + "datasources.html#python") + @classmethod def configuration_schema(cls): return { @@ -57,6 +60,11 @@ def configuration_schema(cls): }, 'additionalModulesPaths': { 'type': 'string' + }, + "doc_url": { + "type": "string", + "title": "Documentation URL", + "default": cls.default_doc_url } }, } diff --git a/redash/query_runner/script.py b/redash/query_runner/script.py index 75bee08cec..0b10f9ded5 100644 --- a/redash/query_runner/script.py +++ b/redash/query_runner/script.py @@ -6,6 +6,9 @@ class Script(BaseQueryRunner): + default_doc_url = ("http://redash.readthedocs.io/en/latest/" + "datasources.html#python") + @classmethod def annotate_query(cls): return False @@ -26,6 +29,11 @@ def configuration_schema(cls): 'shell': { 'type': 'boolean', 'title': 'Execute command through the shell' + }, + "doc_url": { + "type": "string", + "title": "Documentation URL", + "default": cls.default_doc_url } }, 'required': ['path'] diff --git a/redash/query_runner/sqlite.py b/redash/query_runner/sqlite.py index 2bab1f27c4..18bef51816 100644 --- a/redash/query_runner/sqlite.py +++ b/redash/query_runner/sqlite.py @@ -13,6 +13,7 @@ class Sqlite(BaseSQLQueryRunner): noop_query = "pragma quick_check" + default_doc_url = "http://sqlite.org/lang.html" @classmethod def configuration_schema(cls): @@ -22,6 +23,11 @@ def configuration_schema(cls): "dbpath": { "type": "string", "title": "Database Path" + }, + "doc_url": { + "type": "string", + "title": "Documentation URL", + "default": cls.default_doc_url } }, "required": ["dbpath"], diff --git a/redash/query_runner/treasuredata.py b/redash/query_runner/treasuredata.py index 2f68d6d7e5..441dea89eb 100644 --- a/redash/query_runner/treasuredata.py +++ b/redash/query_runner/treasuredata.py @@ -36,6 +36,7 @@ class TreasureData(BaseQueryRunner): noop_query = "SELECT 1" + default_doc_url = "https://docs.treasuredata.com/categories/hive" @classmethod def configuration_schema(cls): @@ -59,6 +60,11 @@ def configuration_schema(cls): 'type': 'boolean', 'title': 'Auto Schema Retrieval', 'default': False + }, + "doc_url": { + "type": "string", + "title": "Documentation URL", + "default": cls.default_doc_url } }, 'required': ['apikey','db'] diff --git a/redash/query_runner/url.py b/redash/query_runner/url.py index 8763b63ed2..5da7659390 100644 --- a/redash/query_runner/url.py +++ b/redash/query_runner/url.py @@ -3,6 +3,9 @@ class Url(BaseQueryRunner): + default_doc_url = ("http://redash.readthedocs.io/en/latest/" + "datasources.html#url") + @classmethod def configuration_schema(cls): return { @@ -11,6 +14,11 @@ def configuration_schema(cls): 'url': { 'type': 'string', 'title': 'URL base path' + }, + "doc_url": { + "type": "string", + "title": "Documentation URL", + "default": cls.default_doc_url } } } diff --git a/redash/query_runner/vertica.py b/redash/query_runner/vertica.py index 0bfe2b6611..97b28ebfcb 100644 --- a/redash/query_runner/vertica.py +++ b/redash/query_runner/vertica.py @@ -30,6 +30,10 @@ class Vertica(BaseSQLQueryRunner): noop_query = "SELECT 1" + default_doc_url = ( + "https://my.vertica.com/docs/8.0.x/HTML/index.htm#Authoring/" + "ConceptsGuide/Other/SQLOverview.htm%3FTocPath%3DSQL" + "%2520Reference%2520Manual%7C_____1") @classmethod def configuration_schema(cls): @@ -56,7 +60,12 @@ def configuration_schema(cls): "read_timeout": { "type": "number", "title": "Read Timeout" - }, + }, + "doc_url": { + "type": "string", + "title": "Documentation URL", + "default": cls.default_doc_url + } }, 'required': ['database'], 'secret': ['password'] diff --git a/tests/handlers/test_data_sources.py b/tests/handlers/test_data_sources.py index f07a2b3719..4590056fd4 100644 --- a/tests/handlers/test_data_sources.py +++ b/tests/handlers/test_data_sources.py @@ -60,7 +60,8 @@ def test_updates_data_source(self): new_name = 'New Name' new_options = {"dbname": "newdb"} rv = self.make_request('post', self.path, - data={'name': new_name, 'type': 'pg', 'options': new_options}, + data={'name': new_name, 'type': 'pg', 'options': new_options, + 'doc_url': None}, user=admin) self.assertEqual(rv.status_code, 200) @@ -101,7 +102,9 @@ def test_returns_400_when_configuration_invalid(self): def test_creates_data_source(self): admin = self.factory.create_admin() rv = self.make_request('post', '/api/data_sources', - data={'name': 'DS 1', 'type': 'pg', 'options': {"dbname": "redash"}}, user=admin) + data={'name': 'DS 1', 'type': 'pg', + 'options': {"dbname": "redash"}, + 'doc_url': None}, user=admin) self.assertEqual(rv.status_code, 200) diff --git a/tests/test_cli.py b/tests/test_cli.py index b46da2094e..550ab577e7 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -16,7 +16,7 @@ def test_interactive_new(self): result = runner.invoke( manager, ['ds', 'new'], - input="test\n%s\n\n\nexample.com\n\n\ntestdb\n" % (pg_i,)) + input="test\n%s\n\n\n\nexample.com\n\n\ntestdb\n" % (pg_i,)) self.assertFalse(result.exception) self.assertEqual(result.exit_code, 0) self.assertEqual(DataSource.query.count(), 1)