superset-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From maximebeauche...@apache.org
Subject [incubator-superset] branch master updated: [explore] DatasourceControl to pick datasource in modal (#3210)
Date Tue, 01 Aug 2017 19:08:03 GMT
This is an automated email from the ASF dual-hosted git repository.

maximebeauchemin pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-superset.git


The following commit(s) were added to refs/heads/master by this push:
     new 62fcdf2  [explore] DatasourceControl to pick datasource in modal (#3210)
62fcdf2 is described below

commit 62fcdf2a92cc2fa93f77adbd650fc0a7031850a2
Author: Maxime Beauchemin <maximebeauchemin@gmail.com>
AuthorDate: Tue Aug 1 12:08:00 2017 -0700

    [explore] DatasourceControl to pick datasource in modal (#3210)
    
    * [explore] DatasourceControl to pick datasource in modal
    
    Makes it easier to change datasource, also makes it such that the list
    of all datasources doesn't need to be loaded upfront.
    
    * Adding more metadata
---
 superset/assets/javascripts/SqlLab/index.jsx       |   2 +-
 .../components/InfoTooltipWithTrigger.jsx          |   9 +-
 .../javascripts/explore/actions/exploreActions.js  |  38 -----
 .../javascripts/explore/components/Control.jsx     |   6 +-
 .../explore/components/ExploreViewContainer.jsx    |   3 -
 .../components/controls/DatasourceControl.jsx      | 157 +++++++++++++++++++++
 .../explore/components/controls/VizTypeControl.jsx |  20 ++-
 superset/assets/javascripts/explore/index.jsx      |   1 +
 .../javascripts/explore/reducers/exploreReducer.js |  19 ---
 .../assets/javascripts/explore/stores/controls.jsx |  20 +--
 .../explore/components/DatasourceControl_spec.jsx  |  32 +++++
 .../javascripts/explore/exploreActions_spec.js     |  38 -----
 .../reactable-pagination.css                       |   0
 superset/assets/stylesheets/superset.css           |   3 +
 superset/connectors/base/models.py                 |  24 ++++
 superset/connectors/druid/models.py                |   4 +
 superset/connectors/sqla/models.py                 |   4 +
 superset/views/core.py                             |   3 +-
 18 files changed, 257 insertions(+), 126 deletions(-)

diff --git a/superset/assets/javascripts/SqlLab/index.jsx b/superset/assets/javascripts/SqlLab/index.jsx
index e292c25..ba09924 100644
--- a/superset/assets/javascripts/SqlLab/index.jsx
+++ b/superset/assets/javascripts/SqlLab/index.jsx
@@ -11,7 +11,7 @@ import App from './components/App';
 import { appSetup } from '../common';
 
 import './main.css';
-import './reactable-pagination.css';
+import '../../stylesheets/reactable-pagination.css';
 import '../components/FilterableTable/FilterableTableStyles.css';
 
 appSetup();
diff --git a/superset/assets/javascripts/components/InfoTooltipWithTrigger.jsx b/superset/assets/javascripts/components/InfoTooltipWithTrigger.jsx
index 07b4db4..85bc7fb 100644
--- a/superset/assets/javascripts/components/InfoTooltipWithTrigger.jsx
+++ b/superset/assets/javascripts/components/InfoTooltipWithTrigger.jsx
@@ -8,18 +8,23 @@ const propTypes = {
   tooltip: PropTypes.string.isRequired,
   icon: PropTypes.string,
   className: PropTypes.string,
+  onClick: PropTypes.func,
 };
 const defaultProps = {
   icon: 'question-circle-o',
 };
 
-export default function InfoTooltipWithTrigger({ label, tooltip, icon, className }) {
+export default function InfoTooltipWithTrigger({ label, tooltip, icon, className, onClick
}) {
   return (
     <OverlayTrigger
       placement="right"
       overlay={<Tooltip id={`${slugify(label)}-tooltip`}>{tooltip}</Tooltip>}
     >
-      <i className={`fa fa-${icon} ${className}`} />
+      <i
+        className={`fa fa-${icon} ${className}`}
+        onClick={onClick}
+        style={{ cursor: onClick ? 'pointer' : null }}
+      />
     </OverlayTrigger>
   );
 }
diff --git a/superset/assets/javascripts/explore/actions/exploreActions.js b/superset/assets/javascripts/explore/actions/exploreActions.js
index 6d8ed83..d45acd5 100644
--- a/superset/assets/javascripts/explore/actions/exploreActions.js
+++ b/superset/assets/javascripts/explore/actions/exploreActions.js
@@ -16,11 +16,6 @@ export function setDatasource(datasource) {
   return { type: SET_DATASOURCE, datasource };
 }
 
-export const SET_DATASOURCES = 'SET_DATASOURCES';
-export function setDatasources(datasources) {
-  return { type: SET_DATASOURCES, datasources };
-}
-
 export const FETCH_DATASOURCE_STARTED = 'FETCH_DATASOURCE_STARTED';
 export function fetchDatasourceStarted() {
   return { type: FETCH_DATASOURCE_STARTED };
@@ -36,21 +31,6 @@ export function fetchDatasourceFailed(error) {
   return { type: FETCH_DATASOURCE_FAILED, error };
 }
 
-export const FETCH_DATASOURCES_STARTED = 'FETCH_DATASOURCES_STARTED';
-export function fetchDatasourcesStarted() {
-  return { type: FETCH_DATASOURCES_STARTED };
-}
-
-export const FETCH_DATASOURCES_SUCCEEDED = 'FETCH_DATASOURCES_SUCCEEDED';
-export function fetchDatasourcesSucceeded() {
-  return { type: FETCH_DATASOURCES_SUCCEEDED };
-}
-
-export const FETCH_DATASOURCES_FAILED = 'FETCH_DATASOURCES_FAILED';
-export function fetchDatasourcesFailed(error) {
-  return { type: FETCH_DATASOURCES_FAILED, error };
-}
-
 export const RESET_FIELDS = 'RESET_FIELDS';
 export function resetControls() {
   return { type: RESET_FIELDS };
@@ -83,24 +63,6 @@ export function fetchDatasourceMetadata(datasourceKey, alsoTriggerQuery
= false)
   };
 }
 
-export function fetchDatasources() {
-  return function (dispatch) {
-    dispatch(fetchDatasourcesStarted());
-    const url = '/superset/datasources/';
-    $.ajax({
-      type: 'GET',
-      url,
-      success: (data) => {
-        dispatch(setDatasources(data));
-        dispatch(fetchDatasourcesSucceeded());
-      },
-      error(error) {
-        dispatch(fetchDatasourcesFailed(error.responseJSON.error));
-      },
-    });
-  };
-}
-
 export const TOGGLE_FAVE_STAR = 'TOGGLE_FAVE_STAR';
 export function toggleFaveStar(isStarred) {
   return { type: TOGGLE_FAVE_STAR, isStarred };
diff --git a/superset/assets/javascripts/explore/components/Control.jsx b/superset/assets/javascripts/explore/components/Control.jsx
index d9aaea7..b0dce35 100644
--- a/superset/assets/javascripts/explore/components/Control.jsx
+++ b/superset/assets/javascripts/explore/components/Control.jsx
@@ -1,24 +1,26 @@
 import React from 'react';
 import PropTypes from 'prop-types';
 
+import BoundsControl from './controls/BoundsControl';
 import CheckboxControl from './controls/CheckboxControl';
+import DatasourceControl from './controls/DatasourceControl';
 import FilterControl from './controls/FilterControl';
 import HiddenControl from './controls/HiddenControl';
 import SelectControl from './controls/SelectControl';
 import TextAreaControl from './controls/TextAreaControl';
 import TextControl from './controls/TextControl';
 import VizTypeControl from './controls/VizTypeControl';
-import BoundsControl from './controls/BoundsControl';
 
 const controlMap = {
+  BoundsControl,
   CheckboxControl,
+  DatasourceControl,
   FilterControl,
   HiddenControl,
   SelectControl,
   TextAreaControl,
   TextControl,
   VizTypeControl,
-  BoundsControl,
 };
 const controlTypes = Object.keys(controlMap);
 
diff --git a/superset/assets/javascripts/explore/components/ExploreViewContainer.jsx b/superset/assets/javascripts/explore/components/ExploreViewContainer.jsx
index f015aa9..e8c9042 100644
--- a/superset/assets/javascripts/explore/components/ExploreViewContainer.jsx
+++ b/superset/assets/javascripts/explore/components/ExploreViewContainer.jsx
@@ -33,9 +33,6 @@ class ExploreViewContainer extends React.Component {
   }
 
   componentDidMount() {
-    if (!this.props.standalone) {
-      this.props.actions.fetchDatasources();
-    }
     window.addEventListener('resize', this.handleResize.bind(this));
     this.triggerQueryIfNeeded();
   }
diff --git a/superset/assets/javascripts/explore/components/controls/DatasourceControl.jsx
b/superset/assets/javascripts/explore/components/controls/DatasourceControl.jsx
new file mode 100644
index 0000000..20e12a5
--- /dev/null
+++ b/superset/assets/javascripts/explore/components/controls/DatasourceControl.jsx
@@ -0,0 +1,157 @@
+/* global notify */
+import React from 'react';
+import PropTypes from 'prop-types';
+import { Table } from 'reactable';
+import { Label, FormControl, Modal, OverlayTrigger, Tooltip } from 'react-bootstrap';
+
+import ControlHeader from '../ControlHeader';
+import InfoTooltipWithTrigger from '../../../components/InfoTooltipWithTrigger';
+
+const propTypes = {
+  description: PropTypes.string,
+  label: PropTypes.string,
+  name: PropTypes.string.isRequired,
+  onChange: PropTypes.func,
+  value: PropTypes.string.isRequired,
+  datasource: PropTypes.object.isRequired,
+};
+
+const defaultProps = {
+  onChange: () => {},
+};
+
+export default class DatasourceControl extends React.PureComponent {
+  constructor(props) {
+    super(props);
+    this.state = {
+      showModal: false,
+      filter: '',
+      loading: true,
+    };
+    this.toggleModal = this.toggleModal.bind(this);
+    this.changeSearch = this.changeSearch.bind(this);
+    this.setSearchRef = this.setSearchRef.bind(this);
+    this.onEnterModal = this.onEnterModal.bind(this);
+  }
+  onChange(vizType) {
+    this.props.onChange(vizType);
+    this.setState({ showModal: false });
+  }
+  onEnterModal() {
+    if (this.searchRef) {
+      this.searchRef.focus();
+    }
+    const url = '/superset/datasources/';
+    const that = this;
+    if (!this.state.datasources) {
+      $.ajax({
+        type: 'GET',
+        url,
+        success: (data) => {
+          const datasources = data.map(ds => ({
+            rawName: ds.name,
+            connection: ds.connection,
+            schema: ds.schema,
+            name: (
+              <a
+                href="#"
+                onClick={this.selectDatasource.bind(this, ds.uid)}
+                className="datasource-link"
+              >
+                {ds.name}
+              </a>),
+            type: ds.type,
+          }));
+
+          that.setState({ loading: false, datasources });
+        },
+        error() {
+          that.setState({ loading: false });
+          notify.error('Something went wrong while fetching the datasource list');
+        },
+      });
+    }
+  }
+  setSearchRef(searchRef) {
+    this.searchRef = searchRef;
+  }
+  toggleModal() {
+    this.setState({ showModal: !this.state.showModal });
+  }
+  changeSearch(event) {
+    this.setState({ filter: event.target.value });
+  }
+  selectDatasource(datasourceId) {
+    this.setState({ showModal: false });
+    this.props.onChange(datasourceId);
+  }
+  render() {
+    return (
+      <div>
+        <ControlHeader {...this.props} />
+        <OverlayTrigger
+          placement="right"
+          overlay={
+            <Tooltip id={'error-tooltip'}>Click to point to another datasource</Tooltip>
+          }
+        >
+          <Label onClick={this.toggleModal} style={{ cursor: 'pointer' }} className="m-r-3">
+            {this.props.datasource.name}
+          </Label>
+        </OverlayTrigger>
+        <InfoTooltipWithTrigger
+          tooltip="edit the datasource's configuration"
+          icon="edit"
+          label="edit datasource"
+          onClick={() => {
+            window.location = this.props.datasource.edit_url;
+          }}
+        />
+        <Modal
+          show={this.state.showModal}
+          onHide={this.toggleModal}
+          onEnter={this.onEnterModal}
+          onExit={this.setSearchRef}
+          bsSize="lg"
+        >
+          <Modal.Header closeButton>
+            <Modal.Title>Select a datasource</Modal.Title>
+          </Modal.Header>
+          <Modal.Body>
+            <div>
+              <FormControl
+                id="formControlsText"
+                inputRef={(ref) => { this.setSearchRef(ref); }}
+                type="text"
+                bsSize="sm"
+                value={this.state.filter}
+                placeholder="Search / Filter"
+                onChange={this.changeSearch}
+              />
+            </div>
+            {this.state.loading &&
+              <img
+                className="loading"
+                alt="Loading..."
+                src="/static/assets/images/loading.gif"
+              />
+            }
+            {this.state.datasources &&
+              <Table
+                columns={['name', 'type', 'schema', 'connection', 'creator']}
+                className="table table-condensed"
+                data={this.state.datasources}
+                itemsPerPage={20}
+                filterable={['rawName', 'type', 'connection', 'schema', 'creator']}
+                filterBy={this.state.filter}
+                hideFilterInput
+              />
+            }
+          </Modal.Body>
+        </Modal>
+      </div>);
+  }
+}
+
+DatasourceControl.propTypes = propTypes;
+DatasourceControl.defaultProps = defaultProps;
diff --git a/superset/assets/javascripts/explore/components/controls/VizTypeControl.jsx b/superset/assets/javascripts/explore/components/controls/VizTypeControl.jsx
index 0b454ac..0fc8266 100644
--- a/superset/assets/javascripts/explore/components/controls/VizTypeControl.jsx
+++ b/superset/assets/javascripts/explore/components/controls/VizTypeControl.jsx
@@ -1,6 +1,8 @@
 import React from 'react';
 import PropTypes from 'prop-types';
-import { Label, Row, Col, FormControl, Modal } from 'react-bootstrap';
+import {
+  Label, Row, Col, FormControl, Modal, OverlayTrigger,
+  Tooltip } from 'react-bootstrap';
 import visTypes from '../../stores/visTypes';
 import ControlHeader from '../ControlHeader';
 
@@ -85,13 +87,17 @@ export default class VizTypeControl extends React.PureComponent {
       <div>
         <ControlHeader
           {...this.props}
-          rightNode={
-            <a onClick={this.toggleModal}>edit</a>
-          }
         />
-        <Label onClick={this.toggleModal} style={{ cursor: 'pointer' }}>
-          {visTypes[this.props.value].label}
-        </Label>
+        <OverlayTrigger
+          placement="right"
+          overlay={
+            <Tooltip id={'error-tooltip'}>Click to change visualization type</Tooltip>
+          }
+        >
+          <Label onClick={this.toggleModal} style={{ cursor: 'pointer' }}>
+            {visTypes[this.props.value].label}
+          </Label>
+        </OverlayTrigger>
         <Modal
           show={this.state.showModal}
           onHide={this.toggleModal}
diff --git a/superset/assets/javascripts/explore/index.jsx b/superset/assets/javascripts/explore/index.jsx
index 39ecab0..0fe4fca 100644
--- a/superset/assets/javascripts/explore/index.jsx
+++ b/superset/assets/javascripts/explore/index.jsx
@@ -14,6 +14,7 @@ import ExploreViewContainer from './components/ExploreViewContainer';
 import { exploreReducer } from './reducers/exploreReducer';
 import { appSetup } from '../common';
 import './main.css';
+import '../../stylesheets/reactable-pagination.css';
 
 appSetup();
 initJQueryAjax();
diff --git a/superset/assets/javascripts/explore/reducers/exploreReducer.js b/superset/assets/javascripts/explore/reducers/exploreReducer.js
index cc21458..96e36e3 100644
--- a/superset/assets/javascripts/explore/reducers/exploreReducer.js
+++ b/superset/assets/javascripts/explore/reducers/exploreReducer.js
@@ -29,25 +29,6 @@ export const exploreReducer = function (state, action) {
     [actions.SET_DATASOURCE]() {
       return Object.assign({}, state, { datasource: action.datasource });
     },
-    [actions.FETCH_DATASOURCES_STARTED]() {
-      return Object.assign({}, state, { isDatasourcesLoading: true });
-    },
-
-    [actions.FETCH_DATASOURCES_SUCCEEDED]() {
-      return Object.assign({}, state, { isDatasourcesLoading: false });
-    },
-
-    [actions.FETCH_DATASOURCES_FAILED]() {
-      // todo(alanna) handle failure/error state
-      return Object.assign({}, state,
-        {
-          isDatasourcesLoading: false,
-          controlPanelAlert: action.error,
-        });
-    },
-    [actions.SET_DATASOURCES]() {
-      return Object.assign({}, state, { datasources: action.datasources });
-    },
     [actions.REMOVE_CONTROL_PANEL_ALERT]() {
       return Object.assign({}, state, { controlPanelAlert: null });
     },
diff --git a/superset/assets/javascripts/explore/stores/controls.jsx b/superset/assets/javascripts/explore/stores/controls.jsx
index 3d4151b..7ef5a3e 100644
--- a/superset/assets/javascripts/explore/stores/controls.jsx
+++ b/superset/assets/javascripts/explore/stores/controls.jsx
@@ -30,23 +30,13 @@ export const D3_TIME_FORMAT_OPTIONS = [
 
 export const controls = {
   datasource: {
-    type: 'SelectControl',
+    type: 'DatasourceControl',
     label: 'Datasource',
-    isLoading: true,
-    clearable: false,
     default: null,
-    validators: [v.nonEmpty],
-    mapStateToProps: (state) => {
-      const datasources = state.datasources || [];
-      return {
-        choices: datasources,
-        isLoading: datasources.length === 0,
-        rightNode: state.datasource ?
-          <a href={state.datasource.edit_url}>edit</a>
-          : null,
-      };
-    },
-    description: '',
+    description: null,
+    mapStateToProps: state => ({
+      datasource: state.datasource,
+    }),
   },
 
   viz_type: {
diff --git a/superset/assets/spec/javascripts/explore/components/DatasourceControl_spec.jsx
b/superset/assets/spec/javascripts/explore/components/DatasourceControl_spec.jsx
new file mode 100644
index 0000000..c46ded0
--- /dev/null
+++ b/superset/assets/spec/javascripts/explore/components/DatasourceControl_spec.jsx
@@ -0,0 +1,32 @@
+import React from 'react';
+import sinon from 'sinon';
+import { expect } from 'chai';
+import { describe, it, beforeEach } from 'mocha';
+import { shallow } from 'enzyme';
+import { Modal } from 'react-bootstrap';
+import DatasourceControl from '../../../../javascripts/explore/components/controls/DatasourceControl';
+
+const defaultProps = {
+  name: 'datasource',
+  label: 'Datasource',
+  value: '1__table',
+  datasource: {
+    name: 'birth_names',
+    type: 'table',
+    uid: '1__table',
+    id: 1,
+  },
+  onChange: sinon.spy(),
+};
+
+describe('DatasourceControl', () => {
+  let wrapper;
+
+  beforeEach(() => {
+    wrapper = shallow(<DatasourceControl {...defaultProps} />);
+  });
+
+  it('renders a Modal', () => {
+    expect(wrapper.find(Modal)).to.have.lengthOf(1);
+  });
+});
diff --git a/superset/assets/spec/javascripts/explore/exploreActions_spec.js b/superset/assets/spec/javascripts/explore/exploreActions_spec.js
index 86173be..9fa02e4 100644
--- a/superset/assets/spec/javascripts/explore/exploreActions_spec.js
+++ b/superset/assets/spec/javascripts/explore/exploreActions_spec.js
@@ -82,44 +82,6 @@ describe('fetching actions', () => {
     });
   });
 
-  describe('fetchDatasources', () => {
-    const makeRequest = () => {
-      request = actions.fetchDatasources();
-      request(dispatch);
-    };
-
-    it('calls fetchDatasourcesStarted', () => {
-      makeRequest();
-      expect(dispatch.args[0][0].type).to.equal(actions.FETCH_DATASOURCES_STARTED);
-    });
-
-    it('makes the ajax request', () => {
-      makeRequest();
-      expect(ajaxStub.calledOnce).to.be.true;
-    });
-
-    it('calls correct url', () => {
-      const url = '/superset/datasources/';
-      makeRequest();
-      expect(ajaxStub.getCall(0).args[0].url).to.equal(url);
-    });
-
-    it('calls correct actions on error', () => {
-      ajaxStub.yieldsTo('error', { responseJSON: { error: 'error text' } });
-      makeRequest();
-      expect(dispatch.callCount).to.equal(2);
-      expect(dispatch.getCall(1).args[0].type).to.equal(actions.FETCH_DATASOURCES_FAILED);
-    });
-
-    it('calls correct actions on success', () => {
-      ajaxStub.yieldsTo('success', { data: '' });
-      makeRequest();
-      expect(dispatch.callCount).to.equal(3);
-      expect(dispatch.getCall(1).args[0].type).to.equal(actions.SET_DATASOURCES);
-      expect(dispatch.getCall(2).args[0].type).to.equal(actions.FETCH_DATASOURCES_SUCCEEDED);
-    });
-  });
-
   describe('fetchDashboards', () => {
     const userID = 1;
     const mockDashboardData = {
diff --git a/superset/assets/javascripts/SqlLab/reactable-pagination.css b/superset/assets/stylesheets/reactable-pagination.css
similarity index 100%
rename from superset/assets/javascripts/SqlLab/reactable-pagination.css
rename to superset/assets/stylesheets/reactable-pagination.css
diff --git a/superset/assets/stylesheets/superset.css b/superset/assets/stylesheets/superset.css
index ef12c7e..2004133 100644
--- a/superset/assets/stylesheets/superset.css
+++ b/superset/assets/stylesheets/superset.css
@@ -228,6 +228,9 @@ div.widget .slice_container {
 .m-r-5 {
     margin-right: 5px;
 }
+.m-r-3 {
+    margin-right: 3px;
+}
 .m-t-5 {
     margin-top: 5px;
 }
diff --git a/superset/connectors/base/models.py b/superset/connectors/base/models.py
index b32bb92..593c722 100644
--- a/superset/connectors/base/models.py
+++ b/superset/connectors/base/models.py
@@ -69,6 +69,16 @@ class BaseDatasource(AuditMixinNullable, ImportMixin):
         return "timestamp"
 
     @property
+    def connection(self):
+        """String representing the context of the Datasource"""
+        return None
+
+    @property
+    def schema(self):
+        """String representing the schema of the Datasource (if it applies)"""
+        return None
+
+    @property
     def groupby_column_names(self):
         return sorted([c.column_name for c in self.columns if c.groupby])
 
@@ -108,6 +118,20 @@ class BaseDatasource(AuditMixinNullable, ImportMixin):
             key=lambda x: x[1])
 
     @property
+    def short_data(self):
+        """Data representation of the datasource sent to the frontend"""
+        return {
+            'edit_url': self.url,
+            'id': self.id,
+            'uid': self.uid,
+            'schema': self.schema,
+            'name': self.name,
+            'type': self.type,
+            'connection': self.connection,
+            'creator': str(self.created_by),
+        }
+
+    @property
     def data(self):
         """Data representation of the datasource sent to the frontend"""
         order_by_choices = []
diff --git a/superset/connectors/druid/models.py b/superset/connectors/druid/models.py
index cc85a92..6f88dd1 100644
--- a/superset/connectors/druid/models.py
+++ b/superset/connectors/druid/models.py
@@ -355,6 +355,10 @@ class DruidDatasource(Model, BaseDatasource):
         return self.cluster
 
     @property
+    def connection(self):
+        return str(self.database)
+
+    @property
     def num_cols(self):
         return [c.column_name for c in self.columns if c.is_num]
 
diff --git a/superset/connectors/sqla/models.py b/superset/connectors/sqla/models.py
index 0d06bfb..b836a15 100644
--- a/superset/connectors/sqla/models.py
+++ b/superset/connectors/sqla/models.py
@@ -193,6 +193,10 @@ class SqlaTable(Model, BaseDatasource):
         return self.name
 
     @property
+    def connection(self):
+        return str(self.database)
+
+    @property
     def description_markeddown(self):
         return utils.markdown(self.description)
 
diff --git a/superset/views/core.py b/superset/views/core.py
index d5a3126..e73ddc8 100755
--- a/superset/views/core.py
+++ b/superset/views/core.py
@@ -700,7 +700,8 @@ class Superset(BaseSupersetView):
     @expose("/datasources/")
     def datasources(self):
         datasources = ConnectorRegistry.get_all_datasources(db.session)
-        datasources = [(str(o.id) + '__' + o.type, repr(o)) for o in datasources]
+        datasources = [o.short_data for o in datasources]
+        datasources = sorted(datasources, key=lambda o: o['name'])
         return self.json_response(datasources)
 
     @has_access_api

-- 
To stop receiving notification emails like this one, please contact
['"commits@superset.apache.org" <commits@superset.apache.org>'].

Mime
View raw message