Merge pull request #6805 from lyft/xtinec--fix-sticky-tooltip

Fix sticky tooltips on nvd3 vizzes
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index dcc79f7..9f0fd5c 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -410,6 +410,10 @@
 }
 ```
 
+`superset/config.py` contains `DEFAULT_FEATURE_FLAGS` which will be overwritten by
+those specified under FEATURE_FLAGS in `superset_config.py`. For example, `DEFAULT_FEATURE_FLAGS = { 'FOO': True, 'BAR': False }` in `superset/config.py` and `FEATURE_FLAGS = { 'BAR': True, 'BAZ': True }` in `superset_config.py` will result
+in combined feature flags of `{ 'FOO': True, 'BAR': True, 'BAZ': True }`.
+
 ## Linting
 
 Lint the project with:
diff --git a/install/helm/superset/templates/ingress.yaml b/install/helm/superset/templates/ingress.yaml
index 884c38b..b0888e4 100644
--- a/install/helm/superset/templates/ingress.yaml
+++ b/install/helm/superset/templates/ingress.yaml
@@ -14,7 +14,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 #
-{{- if .Values.ingress.enabled -}}
+{{ if .Values.ingress.enabled -}}
 {{- $fullName := include "superset.fullname" . -}}
 {{- $ingressPath := .Values.ingress.path -}}
 apiVersion: extensions/v1beta1
diff --git a/superset/__init__.py b/superset/__init__.py
index 791e20b..500bcaa 100644
--- a/superset/__init__.py
+++ b/superset/__init__.py
@@ -208,6 +208,18 @@
 
 results_backend = app.config.get('RESULTS_BACKEND')
 
+# Merge user defined feature flags with default feature flags
+feature_flags = app.config.get('DEFAULT_FEATURE_FLAGS')
+feature_flags.update(app.config.get('FEATURE_FLAGS') or {})
+
+
+def is_feature_enabled(feature):
+    """
+    Utility function for checking whether a feature is turned on
+    """
+    return feature_flags.get(feature)
+
+
 # Registering sources
 module_datasource_map = app.config.get('DEFAULT_MODULE_DS_MAP')
 module_datasource_map.update(app.config.get('ADDITIONAL_MODULE_DS_MAP'))
diff --git a/superset/assets/src/components/RefreshLabel.jsx b/superset/assets/src/components/RefreshLabel.jsx
index cd5a6e0..855dc66 100644
--- a/superset/assets/src/components/RefreshLabel.jsx
+++ b/superset/assets/src/components/RefreshLabel.jsx
@@ -18,49 +18,26 @@
  */
 import React from 'react';
 import PropTypes from 'prop-types';
-import { Label } from 'react-bootstrap';
 import TooltipWrapper from './TooltipWrapper';
 
+import './RefreshLabel.less';
+
 const propTypes = {
   onClick: PropTypes.func,
-  className: PropTypes.string,
   tooltipContent: PropTypes.string.isRequired,
 };
 
 class RefreshLabel extends React.PureComponent {
-  constructor(props) {
-    super(props);
-    this.state = {
-      hovered: false,
-    };
-  }
-
-  mouseOver() {
-    this.setState({ hovered: true });
-  }
-
-  mouseOut() {
-    this.setState({ hovered: false });
-  }
-
   render() {
-    const labelStyle = this.state.hovered ? 'primary' : 'default';
-    const tooltip = 'Click to ' + this.props.tooltipContent;
     return (
       <TooltipWrapper
-        tooltip={tooltip}
+        tooltip={this.props.tooltipContent}
         label="cache-desc"
       >
-        <Label
-          className={this.props.className}
-          bsStyle={labelStyle}
-          style={{ fontSize: '13px', marginRight: '5px', cursor: 'pointer' }}
+        <i
+          className="RefreshLabel fa fa-refresh pointer"
           onClick={this.props.onClick}
-          onMouseOver={this.mouseOver.bind(this)}
-          onMouseOut={this.mouseOut.bind(this)}
-        >
-          <i className="fa fa-refresh" />
-        </Label>
+        />
       </TooltipWrapper>);
   }
 }
diff --git a/superset/assets/src/components/RefreshLabel.less b/superset/assets/src/components/RefreshLabel.less
new file mode 100644
index 0000000..3bf895b
--- /dev/null
+++ b/superset/assets/src/components/RefreshLabel.less
@@ -0,0 +1,27 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+@import "../../stylesheets/less/cosmo/variables.less";
+
+.RefreshLabel:hover {
+  color: @brand-primary;
+}
+
+.RefreshLabel {
+  color: @gray-light;
+}
diff --git a/superset/assets/src/components/TableSelector.css b/superset/assets/src/components/TableSelector.css
new file mode 100644
index 0000000..b4636de
--- /dev/null
+++ b/superset/assets/src/components/TableSelector.css
@@ -0,0 +1,24 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+.TableSelector .fa-refresh {
+  padding-top: 7px
+}
+.TableSelector .refresh-col {
+  padding-left: 0px;
+}
diff --git a/superset/assets/src/components/TableSelector.jsx b/superset/assets/src/components/TableSelector.jsx
index 6a0c401..9031d1a 100644
--- a/superset/assets/src/components/TableSelector.jsx
+++ b/superset/assets/src/components/TableSelector.jsx
@@ -20,12 +20,13 @@
 import PropTypes from 'prop-types';
 import Select from 'react-virtualized-select';
 import createFilterOptions from 'react-select-fast-filter-options';
-import { ControlLabel, Col, Label } from 'react-bootstrap';
+import { ControlLabel, Col, Label, Row } from 'react-bootstrap';
 import { t } from '@superset-ui/translation';
 import { SupersetClient } from '@superset-ui/connection';
 
 import AsyncSelect from './AsyncSelect';
 import RefreshLabel from './RefreshLabel';
+import './TableSelector.css';
 
 const propTypes = {
   dbId: PropTypes.number.isRequired,
@@ -196,8 +197,16 @@
         {db.database_name}
       </span>);
   }
-  renderDatabaseSelect() {
+  renderSelectRow(select, refreshBtn) {
     return (
+      <Row>
+        <Col md={11}>{select}</Col>
+        <Col md={1} className="refresh-col">{refreshBtn}</Col>
+      </Row>
+    );
+  }
+  renderDatabaseSelect() {
+    return this.renderSelectRow(
       <AsyncSelect
         dataEndpoint={
           '/databaseasync/api/' +
@@ -225,30 +234,26 @@
   renderSchema() {
     return (
       <div className="m-t-5">
-        <div className="row">
-          <div className="col-md-11 col-xs-11 p-r-2">
-            <Select
-              name="select-schema"
-              placeholder={t('Select a schema (%s)', this.state.schemaOptions.length)}
-              options={this.state.schemaOptions}
-              value={this.props.schema}
-              valueRenderer={o => (
-                <div>
-                  <span className="text-muted">{t('Schema:')}</span> {o.label}
-                </div>
-              )}
-              isLoading={this.state.schemaLoading}
-              autosize={false}
-              onChange={this.changeSchema}
-            />
-          </div>
-          <div className="col-md-1 col-xs-1 p-l-0 p-t-8">
-            <RefreshLabel
-              onClick={() => this.onDatabaseChange({ id: this.props.dbId }, true)}
-              tooltipContent={t('force refresh schema list')}
-            />
-          </div>
-        </div>
+        {this.renderSelectRow(
+          <Select
+            name="select-schema"
+            placeholder={t('Select a schema (%s)', this.state.schemaOptions.length)}
+            options={this.state.schemaOptions}
+            value={this.props.schema}
+            valueRenderer={o => (
+              <div>
+                <span className="text-muted">{t('Schema:')}</span> {o.label}
+              </div>
+            )}
+            isLoading={this.state.schemaLoading}
+            autosize={false}
+            onChange={this.changeSchema}
+          />,
+          <RefreshLabel
+            onClick={() => this.onDatabaseChange({ id: this.props.dbId }, true)}
+            tooltipContent={t('Force refresh schema list')}
+          />,
+        )}
       </div>
     );
   }
@@ -262,43 +267,37 @@
       tableSelectDisabled = true;
     }
     const options = this.addOptionIfMissing(this.state.tableOptions, this.state.tableName);
+    const select = this.props.schema ? (
+      <Select
+        name="select-table"
+        ref="selectTable"
+        isLoading={this.state.tableLoading}
+        placeholder={t('Select table or type table name')}
+        autosize={false}
+        onChange={this.changeTable}
+        filterOptions={this.state.filterOptions}
+        options={options}
+        value={this.state.tableName}
+      />) : (
+        <Select
+          async
+          name="async-select-table"
+          ref="selectTable"
+          placeholder={tableSelectPlaceholder}
+          disabled={tableSelectDisabled}
+          autosize={false}
+          onChange={this.changeTable}
+          value={this.state.tableName}
+          loadOptions={this.getTableNamesBySubStr}
+        />);
     return (
       <div className="m-t-5">
-        <div className="row">
-          <div className="col-md-11 col-xs-11 p-r-2">
-            {this.props.schema ? (
-              <Select
-                name="select-table"
-                ref="selectTable"
-                isLoading={this.state.tableLoading}
-                placeholder={t('Select table or type table name')}
-                autosize={false}
-                onChange={this.changeTable}
-                filterOptions={this.state.filterOptions}
-                options={options}
-                value={this.state.tableName}
-              />
-            ) : (
-              <Select
-                async
-                name="async-select-table"
-                ref="selectTable"
-                placeholder={tableSelectPlaceholder}
-                disabled={tableSelectDisabled}
-                autosize={false}
-                onChange={this.changeTable}
-                value={this.state.tableName}
-                loadOptions={this.getTableNamesBySubStr}
-              />
-            )}
-          </div>
-          <div className="col-md-1 col-xs-1 p-l-0 p-t-8">
-            <RefreshLabel
-              onClick={() => this.changeSchema({ value: this.props.schema }, true)}
-              tooltipContent={t('force refresh table list')}
-            />
-          </div>
-        </div>
+        {this.renderSelectRow(
+          select,
+          <RefreshLabel
+            onClick={() => this.changeSchema({ value: this.props.schema }, true)}
+            tooltipContent={t('Force refresh table list')}
+          />)}
       </div>);
   }
   renderSeeTableLabel() {
@@ -318,21 +317,22 @@
       </div>);
   }
   render() {
-    if (this.props.horizontal) {
-      return (
-        <div>
-          <Col md={4}>{this.renderDatabaseSelect()}</Col>
-          <Col md={4}>{this.renderSchema()}</Col>
-          <Col md={4}>{this.renderTable()}</Col>
-        </div>);
-    }
     return (
-      <div>
-        <div>{this.renderDatabaseSelect()}</div>
-        <div className="m-t-5">{this.renderSchema()}</div>
-        {this.props.sqlLabMode && this.renderSeeTableLabel()}
-        <div className="m-t-5">{this.renderTable()}</div>
-      </div>);
+      <div className="TableSelector">
+        {this.props.horizontal ?
+          <div>
+            <Col md={4}>{this.renderDatabaseSelect()}</Col>
+            <Col md={4}>{this.renderSchema()}</Col>
+            <Col md={4}>{this.renderTable()}</Col>
+          </div> :
+          <div>
+            <div>{this.renderDatabaseSelect()}</div>
+            <div className="m-t-5">{this.renderSchema()}</div>
+            {this.props.sqlLabMode && this.renderSeeTableLabel()}
+            <div className="m-t-5">{this.renderTable()}</div>
+          </div>}
+      </div>
+    );
   }
 }
 TableSelector.propTypes = propTypes;
diff --git a/superset/config.py b/superset/config.py
index a598d76..93720c3 100644
--- a/superset/config.py
+++ b/superset/config.py
@@ -187,9 +187,12 @@
 # ---------------------------------------------------
 # Feature flags
 # ---------------------------------------------------
-# Feature flags that are on by default go here. Their
-# values can be overridden by those in super_config.py
-FEATURE_FLAGS = {}
+# Feature flags that are set by default go here. Their values can be
+# overwritten by those specified under FEATURE_FLAGS in super_config.py
+# For example, DEFAULT_FEATURE_FLAGS = { 'FOO': True, 'BAR': False } here
+# and FEATURE_FLAGS = { 'BAR': True, 'BAZ': True } in superset_config.py
+# will result in combined feature flags of { 'FOO': True, 'BAR': True, 'BAZ': True }
+DEFAULT_FEATURE_FLAGS = {}
 
 # ---------------------------------------------------
 # Image and file configuration
diff --git a/superset/migrations/versions/a33a03f16c4a_add_extra_column_to_savedquery.py b/superset/migrations/versions/a33a03f16c4a_add_extra_column_to_savedquery.py
index 95518e5..07e0b05 100644
--- a/superset/migrations/versions/a33a03f16c4a_add_extra_column_to_savedquery.py
+++ b/superset/migrations/versions/a33a03f16c4a_add_extra_column_to_savedquery.py
@@ -1,3 +1,19 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
 """Add extra column to SavedQuery
 
 Licensed to the Apache Software Foundation (ASF) under one or more
diff --git a/superset/views/base.py b/superset/views/base.py
index 6b71ff0..f0f5fbf 100644
--- a/superset/views/base.py
+++ b/superset/views/base.py
@@ -31,7 +31,7 @@
 import simplejson as json
 import yaml
 
-from superset import conf, db, security_manager
+from superset import conf, db, feature_flags, security_manager
 from superset.exceptions import SupersetException, SupersetSecurityException
 from superset.translations.utils import get_language_pack
 from superset.utils import core as utils
@@ -157,7 +157,7 @@
             'conf': {k: conf.get(k) for k in FRONTEND_CONF_KEYS},
             'locale': locale,
             'language_pack': get_language_pack(locale),
-            'feature_flags': conf.get('FEATURE_FLAGS'),
+            'feature_flags': feature_flags,
         }
 
 
diff --git a/tests/base_tests.py b/tests/base_tests.py
index f3029c0..1f3b4c5 100644
--- a/tests/base_tests.py
+++ b/tests/base_tests.py
@@ -19,10 +19,10 @@
 import unittest
 
 from flask_appbuilder.security.sqla import models as ab_models
-from mock import Mock
+from mock import Mock, patch
 import pandas as pd
 
-from superset import app, db, security_manager
+from superset import app, db, is_feature_enabled, security_manager
 from superset.connectors.druid.models import DruidCluster, DruidDatasource
 from superset.connectors.sqla.models import SqlaTable
 from superset.models import core as models
@@ -185,3 +185,11 @@
         if raise_on_error and 'error' in resp:
             raise Exception('run_sql failed')
         return resp
+
+    @patch.dict('superset.feature_flags', {'FOO': True}, clear=True)
+    def test_existing_feature_flags(self):
+        self.assertTrue(is_feature_enabled('FOO'))
+
+    @patch.dict('superset.feature_flags', {}, clear=True)
+    def test_nonexistent_feature_flags(self):
+        self.assertFalse(is_feature_enabled('FOO'))