views.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375
  1. # pylint: disable=C,R,W
  2. from datetime import datetime
  3. import json
  4. import logging
  5. from flask import flash, Markup, redirect
  6. from flask_appbuilder import CompactCRUDMixin, expose
  7. from flask_appbuilder.models.sqla.interface import SQLAInterface
  8. from flask_appbuilder.security.decorators import has_access
  9. from flask_babel import gettext as __
  10. from flask_babel import lazy_gettext as _
  11. from superset import appbuilder, db, security_manager, utils
  12. from superset.connectors.base.views import DatasourceModelView
  13. from superset.connectors.connector_registry import ConnectorRegistry
  14. from superset.views.base import (
  15. BaseSupersetView, DatasourceFilter, DeleteMixin,
  16. get_datasource_exist_error_msg, ListWidgetWithCheckboxes, SupersetModelView,
  17. validate_json, YamlExportMixin,
  18. )
  19. from . import models
  20. class DruidColumnInlineView(CompactCRUDMixin, SupersetModelView): # noqa
  21. datamodel = SQLAInterface(models.DruidColumn)
  22. list_title = _('List Druid Column')
  23. show_title = _('Show Druid Column')
  24. add_title = _('Add Druid Column')
  25. edit_title = _('Edit Druid Column')
  26. list_widget = ListWidgetWithCheckboxes
  27. edit_columns = [
  28. 'column_name', 'verbose_name', 'description', 'dimension_spec_json', 'datasource',
  29. 'groupby', 'filterable', 'count_distinct', 'sum', 'min', 'max']
  30. add_columns = edit_columns
  31. list_columns = [
  32. 'column_name', 'verbose_name', 'type', 'groupby', 'filterable', 'count_distinct',
  33. 'sum', 'min', 'max']
  34. can_delete = False
  35. page_size = 500
  36. label_columns = {
  37. 'column_name': _('Column'),
  38. 'type': _('Type'),
  39. 'datasource': _('Datasource'),
  40. 'groupby': _('Groupable'),
  41. 'filterable': _('Filterable'),
  42. 'count_distinct': _('Count Distinct'),
  43. 'sum': _('Sum'),
  44. 'min': _('Min'),
  45. 'max': _('Max'),
  46. 'verbose_name': _('Verbose Name'),
  47. 'description': _('Description'),
  48. }
  49. description_columns = {
  50. 'filterable': _(
  51. 'Whether this column is exposed in the `Filters` section '
  52. 'of the explore view.'),
  53. 'dimension_spec_json': utils.markdown(
  54. 'this field can be used to specify '
  55. 'a `dimensionSpec` as documented [here]'
  56. '(http://druid.io/docs/latest/querying/dimensionspecs.html). '
  57. 'Make sure to input valid JSON and that the '
  58. '`outputName` matches the `column_name` defined '
  59. 'above.',
  60. True),
  61. }
  62. def pre_update(self, col):
  63. # If a dimension spec JSON is given, ensure that it is
  64. # valid JSON and that `outputName` is specified
  65. if col.dimension_spec_json:
  66. try:
  67. dimension_spec = json.loads(col.dimension_spec_json)
  68. except ValueError as e:
  69. raise ValueError('Invalid Dimension Spec JSON: ' + str(e))
  70. if not isinstance(dimension_spec, dict):
  71. raise ValueError('Dimension Spec must be a JSON object')
  72. if 'outputName' not in dimension_spec:
  73. raise ValueError('Dimension Spec does not contain `outputName`')
  74. if 'dimension' not in dimension_spec:
  75. raise ValueError('Dimension Spec is missing `dimension`')
  76. # `outputName` should be the same as the `column_name`
  77. if dimension_spec['outputName'] != col.column_name:
  78. raise ValueError(
  79. '`outputName` [{}] unequal to `column_name` [{}]'
  80. .format(dimension_spec['outputName'], col.column_name))
  81. def post_update(self, col):
  82. col.refresh_metrics()
  83. def post_add(self, col):
  84. self.post_update(col)
  85. appbuilder.add_view_no_menu(DruidColumnInlineView)
  86. class DruidMetricInlineView(CompactCRUDMixin, SupersetModelView): # noqa
  87. datamodel = SQLAInterface(models.DruidMetric)
  88. list_title = _('List Druid Metric')
  89. show_title = _('Show Druid Metric')
  90. add_title = _('Add Druid Metric')
  91. edit_title = _('Edit Druid Metric')
  92. list_columns = ['metric_name', 'verbose_name', 'metric_type']
  93. edit_columns = [
  94. 'metric_name', 'description', 'verbose_name', 'metric_type', 'json',
  95. 'datasource', 'd3format', 'is_restricted', 'warning_text']
  96. add_columns = edit_columns
  97. page_size = 500
  98. validators_columns = {
  99. 'json': [validate_json],
  100. }
  101. description_columns = {
  102. 'metric_type': utils.markdown(
  103. 'use `postagg` as the metric type if you are defining a '
  104. '[Druid Post Aggregation]'
  105. '(http://druid.io/docs/latest/querying/post-aggregations.html)',
  106. True),
  107. 'is_restricted': _('Whether the access to this metric is restricted '
  108. 'to certain roles. Only roles with the permission '
  109. "'metric access on XXX (the name of this metric)' "
  110. 'are allowed to access this metric'),
  111. }
  112. label_columns = {
  113. 'metric_name': _('Metric'),
  114. 'description': _('Description'),
  115. 'verbose_name': _('Verbose Name'),
  116. 'metric_type': _('Type'),
  117. 'json': _('JSON'),
  118. 'datasource': _('Druid Datasource'),
  119. 'warning_text': _('Warning Message'),
  120. 'is_restricted': _('Is Restricted'),
  121. }
  122. def post_add(self, metric):
  123. if metric.is_restricted:
  124. security_manager.merge_perm('metric_access', metric.get_perm())
  125. def post_update(self, metric):
  126. if metric.is_restricted:
  127. security_manager.merge_perm('metric_access', metric.get_perm())
  128. appbuilder.add_view_no_menu(DruidMetricInlineView)
  129. class DruidClusterModelView(SupersetModelView, DeleteMixin, YamlExportMixin): # noqa
  130. datamodel = SQLAInterface(models.DruidCluster)
  131. list_title = _('List Druid Cluster')
  132. show_title = _('Show Druid Cluster')
  133. add_title = _('Add Druid Cluster')
  134. edit_title = _('Edit Druid Cluster')
  135. add_columns = [
  136. 'verbose_name', 'coordinator_host', 'coordinator_port',
  137. 'coordinator_endpoint', 'broker_host', 'broker_port',
  138. 'broker_endpoint', 'cache_timeout', 'cluster_name',
  139. ]
  140. edit_columns = add_columns
  141. list_columns = ['cluster_name', 'metadata_last_refreshed']
  142. search_columns = ('cluster_name',)
  143. label_columns = {
  144. 'cluster_name': _('Cluster'),
  145. 'coordinator_host': _('Coordinator Host'),
  146. 'coordinator_port': _('Coordinator Port'),
  147. 'coordinator_endpoint': _('Coordinator Endpoint'),
  148. 'broker_host': _('Broker Host'),
  149. 'broker_port': _('Broker Port'),
  150. 'broker_endpoint': _('Broker Endpoint'),
  151. 'verbose_name': _('Verbose Name'),
  152. 'cache_timeout': _('Cache Timeout'),
  153. 'metadata_last_refreshed': _('Metadata Last Refreshed'),
  154. }
  155. description_columns = {
  156. 'cache_timeout': _(
  157. 'Duration (in seconds) of the caching timeout for this cluster. '
  158. 'A timeout of 0 indicates that the cache never expires. '
  159. 'Note this defaults to the global timeout if undefined.'),
  160. }
  161. def pre_add(self, cluster):
  162. security_manager.merge_perm('database_access', cluster.perm)
  163. def pre_update(self, cluster):
  164. self.pre_add(cluster)
  165. def _delete(self, pk):
  166. DeleteMixin._delete(self, pk)
  167. appbuilder.add_view(
  168. DruidClusterModelView,
  169. name='Druid Clusters',
  170. label=__('Druid Clusters'),
  171. icon='fa-cubes',
  172. category='Sources',
  173. category_label=__('Sources'),
  174. category_icon='fa-database',
  175. )
  176. class DruidDatasourceModelView(DatasourceModelView, DeleteMixin, YamlExportMixin): # noqa
  177. datamodel = SQLAInterface(models.DruidDatasource)
  178. list_title = _('List Druid Datasource')
  179. show_title = _('Show Druid Datasource')
  180. add_title = _('Add Druid Datasource')
  181. edit_title = _('Edit Druid Datasource')
  182. list_columns = [
  183. 'datasource_link', 'cluster', 'changed_by_', 'modified']
  184. order_columns = ['datasource_link', 'modified']
  185. related_views = [DruidColumnInlineView, DruidMetricInlineView]
  186. edit_columns = [
  187. 'datasource_name', 'cluster', 'description', 'owner',
  188. 'is_hidden',
  189. 'filter_select_enabled', 'fetch_values_from',
  190. 'default_endpoint', 'offset', 'cache_timeout']
  191. search_columns = (
  192. 'datasource_name', 'cluster', 'description', 'owner',
  193. )
  194. add_columns = edit_columns
  195. show_columns = add_columns + ['perm', 'slices']
  196. page_size = 500
  197. base_order = ('datasource_name', 'asc')
  198. description_columns = {
  199. 'slices': _(
  200. 'The list of charts associated with this table. By '
  201. 'altering this datasource, you may change how these associated '
  202. 'charts behave. '
  203. 'Also note that charts need to point to a datasource, so '
  204. 'this form will fail at saving if removing charts from a '
  205. 'datasource. If you want to change the datasource for a chart, '
  206. "overwrite the chart from the 'explore view'"),
  207. 'offset': _('Timezone offset (in hours) for this datasource'),
  208. 'description': Markup(
  209. 'Supports <a href="'
  210. 'https://daringfireball.net/projects/markdown/">markdown</a>'),
  211. 'fetch_values_from': _(
  212. 'Time expression to use as a predicate when retrieving '
  213. 'distinct values to populate the filter component. '
  214. 'Only applies when `Enable Filter Select` is on. If '
  215. 'you enter `7 days ago`, the distinct list of values in '
  216. 'the filter will be populated based on the distinct value over '
  217. 'the past week'),
  218. 'filter_select_enabled': _(
  219. "Whether to populate the filter's dropdown in the explore "
  220. "view's filter section with a list of distinct values fetched "
  221. 'from the backend on the fly'),
  222. 'default_endpoint': _(
  223. 'Redirects to this endpoint when clicking on the datasource '
  224. 'from the datasource list'),
  225. 'cache_timeout': _(
  226. 'Duration (in seconds) of the caching timeout for this datasource. '
  227. 'A timeout of 0 indicates that the cache never expires. '
  228. 'Note this defaults to the cluster timeout if undefined.'),
  229. }
  230. base_filters = [['id', DatasourceFilter, lambda: []]]
  231. label_columns = {
  232. 'slices': _('Associated Charts'),
  233. 'datasource_link': _('Data Source'),
  234. 'cluster': _('Cluster'),
  235. 'description': _('Description'),
  236. 'owner': _('Owner'),
  237. 'is_hidden': _('Is Hidden'),
  238. 'filter_select_enabled': _('Enable Filter Select'),
  239. 'default_endpoint': _('Default Endpoint'),
  240. 'offset': _('Time Offset'),
  241. 'cache_timeout': _('Cache Timeout'),
  242. 'datasource_name': _('Datasource Name'),
  243. 'fetch_values_from': _('Fetch Values From'),
  244. 'changed_by_': _('Changed By'),
  245. 'modified': _('Modified'),
  246. }
  247. def pre_add(self, datasource):
  248. with db.session.no_autoflush:
  249. query = (
  250. db.session.query(models.DruidDatasource)
  251. .filter(models.DruidDatasource.datasource_name ==
  252. datasource.datasource_name,
  253. models.DruidDatasource.cluster_name ==
  254. datasource.cluster.id)
  255. )
  256. if db.session.query(query.exists()).scalar():
  257. raise Exception(get_datasource_exist_error_msg(
  258. datasource.full_name))
  259. def post_add(self, datasource):
  260. datasource.refresh_metrics()
  261. security_manager.merge_perm('datasource_access', datasource.get_perm())
  262. if datasource.schema:
  263. security_manager.merge_perm('schema_access', datasource.schema_perm)
  264. def post_update(self, datasource):
  265. self.post_add(datasource)
  266. def _delete(self, pk):
  267. DeleteMixin._delete(self, pk)
  268. appbuilder.add_view(
  269. DruidDatasourceModelView,
  270. 'Druid Datasources',
  271. label=__('Druid Datasources'),
  272. category='Sources',
  273. category_label=__('Sources'),
  274. icon='fa-cube')
  275. class Druid(BaseSupersetView):
  276. """The base views for Superset!"""
  277. @has_access
  278. @expose('/refresh_datasources/')
  279. def refresh_datasources(self, refreshAll=True):
  280. """endpoint that refreshes druid datasources metadata"""
  281. session = db.session()
  282. DruidCluster = ConnectorRegistry.sources['druid'].cluster_class
  283. for cluster in session.query(DruidCluster).all():
  284. cluster_name = cluster.cluster_name
  285. try:
  286. cluster.refresh_datasources(refreshAll=refreshAll)
  287. except Exception as e:
  288. flash(
  289. "Error while processing cluster '{}'\n{}".format(
  290. cluster_name, utils.error_msg_from_exception(e)),
  291. 'danger')
  292. logging.exception(e)
  293. return redirect('/druidclustermodelview/list/')
  294. cluster.metadata_last_refreshed = datetime.now()
  295. flash(
  296. _('Refreshed metadata from cluster [{}]').format(
  297. cluster.cluster_name),
  298. 'info')
  299. session.commit()
  300. return redirect('/druiddatasourcemodelview/list/')
  301. @has_access
  302. @expose('/scan_new_datasources/')
  303. def scan_new_datasources(self):
  304. """
  305. Calling this endpoint will cause a scan for new
  306. datasources only and add them.
  307. """
  308. return self.refresh_datasources(refreshAll=False)
  309. appbuilder.add_view_no_menu(Druid)
  310. appbuilder.add_link(
  311. 'Scan New Datasources',
  312. label=__('Scan New Datasources'),
  313. href='/druid/scan_new_datasources/',
  314. category='Sources',
  315. category_label=__('Sources'),
  316. category_icon='fa-database',
  317. icon='fa-refresh')
  318. appbuilder.add_link(
  319. 'Refresh Druid Metadata',
  320. label=__('Refresh Druid Metadata'),
  321. href='/druid/refresh_datasources/',
  322. category='Sources',
  323. category_label=__('Sources'),
  324. category_icon='fa-database',
  325. icon='fa-cog')
  326. appbuilder.add_separator('Sources')