You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by ma...@apache.org on 2018/01/08 06:13:09 UTC

[incubator-superset] branch master updated: Make Welcome page into a simple React app (#4147)

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 c49fb0a  Make Welcome page into a simple React app (#4147)
c49fb0a is described below

commit c49fb0aa9b489db76e1899d11d1492fbbcceef85
Author: Maxime Beauchemin <ma...@gmail.com>
AuthorDate: Sun Jan 7 22:13:06 2018 -0800

    Make Welcome page into a simple React app (#4147)
    
    * Make Welcome page into a simple React app
    
    This removes a dependency on datatables, we should be able to get rid
    of it as we re-write the Table and PivotTable viz
    
    * tests/lint
    
    * Bump node version to latest
---
 .travis.yml                                        |  2 +-
 superset/assets/javascripts/welcome.js             | 57 -----------------
 superset/assets/javascripts/welcome/App.jsx        | 40 ++++++++++++
 .../assets/javascripts/welcome/DashboardTable.jsx  | 71 ++++++++++++++++++++++
 superset/assets/javascripts/welcome/index.jsx      | 17 ++++++
 .../assets/spec/javascripts/welcome/App_spec.jsx   | 22 +++++++
 .../javascripts/welcome/DashboardTable_spec.jsx    | 31 ++++++++++
 superset/assets/stylesheets/superset.less          | 21 +++++++
 superset/assets/webpack.config.js                  |  2 +-
 superset/models/helpers.py                         |  5 ++
 superset/templates/superset/welcome.html           | 31 ----------
 superset/views/core.py                             | 15 ++++-
 12 files changed, 221 insertions(+), 93 deletions(-)

diff --git a/.travis.yml b/.travis.yml
index d32057c..89ca5b6 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -10,7 +10,7 @@ cache:
 env:
   global:
     - TRAVIS_CACHE=$HOME/.travis_cache/
-    - TRAVIS_NODE_VERSION="7.10.0"
+    - TRAVIS_NODE_VERSION="8.8.1"
   matrix:
     - TOX_ENV=flake8
     - TOX_ENV=javascript
diff --git a/superset/assets/javascripts/welcome.js b/superset/assets/javascripts/welcome.js
deleted file mode 100644
index cfece7e..0000000
--- a/superset/assets/javascripts/welcome.js
+++ /dev/null
@@ -1,57 +0,0 @@
-/* eslint no-unused-vars: 0 */
-import d3 from 'd3';
-import dt from 'datatables.net-bs';
-import 'datatables.net-bs/css/dataTables.bootstrap.css';
-
-import '../stylesheets/welcome.css';
-import { appSetup } from './common';
-
-appSetup();
-
-dt(window, $);
-
-function modelViewTable(selector, modelView, orderCol, order) {
-  // Builds a dataTable from a flask appbuilder api endpoint
-  let url = '/' + modelView.toLowerCase() + '/api/read';
-  url += '?_oc_' + modelView + '=' + orderCol;
-  url += '&_od_' + modelView + '=' + order;
-  $.getJSON(url, function (data) {
-    const columns = ['dashboard_link', 'creator', 'modified'];
-    const tableData = $.map(data.result, function (el) {
-      const row = $.map(columns, function (col) {
-        return el[col];
-      });
-      return [row];
-    });
-    const cols = $.map(columns, function (col) {
-      return { sTitle: data.label_columns[col] };
-    });
-    const panel = $(selector).parents('.panel');
-    panel.find('img.loading').remove();
-    $(selector).DataTable({
-      aaData: tableData,
-      aoColumns: cols,
-      bPaginate: true,
-      pageLength: 10,
-      bLengthChange: false,
-      aaSorting: [],
-      searching: true,
-      bInfo: false,
-    });
-    // Hack to move the searchbox in the right spot
-    const search = panel.find('.dataTables_filter input');
-    search.addClass('form-control').detach();
-    search.appendTo(panel.find('.search'));
-    panel.find('.dataTables_filter').remove();
-    // Hack to display the page navigator properly
-    panel.find('.col-sm-5').remove();
-    const nav = panel.find('.col-sm-7');
-    nav.removeClass('col-sm-7');
-    nav.addClass('col-sm-12');
-    $(selector).slideDown();
-    $('[data-toggle="tooltip"]').tooltip({ container: 'body' });
-  });
-}
-$(document).ready(function () {
-  modelViewTable('#dash_table', 'DashboardModelViewAsync', 'changed_on', 'desc');
-});
diff --git a/superset/assets/javascripts/welcome/App.jsx b/superset/assets/javascripts/welcome/App.jsx
new file mode 100644
index 0000000..78674c4
--- /dev/null
+++ b/superset/assets/javascripts/welcome/App.jsx
@@ -0,0 +1,40 @@
+import React from 'react';
+import { Panel, Row, Col, FormControl } from 'react-bootstrap';
+
+import DashboardTable from './DashboardTable';
+
+export default class App extends React.PureComponent {
+  constructor(props) {
+    super(props);
+    this.state = {
+      search: '',
+    };
+    this.onSearchChange = this.onSearchChange.bind(this);
+  }
+  onSearchChange(event) {
+    this.setState({ search: event.target.value });
+  }
+  render() {
+    return (
+      <div className="container welcome">
+        <Panel>
+          <Row>
+            <Col md={8}><h2>Dashboards</h2></Col>
+            <Col md={4}>
+              <FormControl
+                type="text"
+                bsSize="sm"
+                style={{ marginTop: '25px' }}
+                placeholder="Search"
+                value={this.state.search}
+                onChange={this.onSearchChange}
+              />
+            </Col>
+          </Row>
+          <hr />
+          <DashboardTable search={this.state.search} />
+        </Panel>
+      </div>
+    );
+  }
+}
diff --git a/superset/assets/javascripts/welcome/DashboardTable.jsx b/superset/assets/javascripts/welcome/DashboardTable.jsx
new file mode 100644
index 0000000..78d4bdd
--- /dev/null
+++ b/superset/assets/javascripts/welcome/DashboardTable.jsx
@@ -0,0 +1,71 @@
+/* eslint no-unused-vars: 0 */
+import React from 'react';
+import ReactDOM from 'react-dom';
+import PropTypes from 'prop-types';
+import { Table, Tr, Td, Thead, Th, unsafe } from 'reactable';
+
+import '../../stylesheets/reactable-pagination.css';
+
+const $ = window.$ = require('jquery');
+
+const propTypes = {
+  search: PropTypes.string,
+};
+
+export default class DashboardTable extends React.PureComponent {
+  constructor(props) {
+    super(props);
+    this.state = {
+      dashboards: false,
+    };
+  }
+  componentDidMount() {
+    const url = (
+      '/dashboardmodelviewasync/api/read' +
+      '?_oc_DashboardModelViewAsync=changed_on' +
+      '&_od_DashboardModelViewAsync=desc');
+    $.getJSON(url, (data) => {
+      this.setState({ dashboards: data.result });
+    });
+  }
+  render() {
+    if (this.state.dashboards) {
+      return (
+        <Table
+          className="table"
+          sortable={['dashboard', 'creator', 'modified']}
+          filterBy={this.props.search}
+          filterable={['dashboard', 'creator']}
+          itemsPerPage={50}
+          hideFilterInput
+          columns={[
+            { key: 'dashboard', label: 'Dashboard' },
+            { key: 'creator', label: 'Creator' },
+            { key: 'modified', label: 'Modified' },
+          ]}
+          defaultSort={{ column: 'modified', direction: 'desc' }}
+        >
+          {this.state.dashboards.map(o => (
+            <Tr key={o.id}>
+              <Td column="dashboard" value={o.dashboard_title}>
+                <a href={o.url}>{o.dashboard_title}</a>
+              </Td>
+              <Td column="creator" value={o.changed_by_name}>
+                {unsafe(o.creator)}
+              </Td>
+              <Td column="modified" value={o.changed_on} className="text-muted">
+                {unsafe(o.modified)}
+              </Td>
+            </Tr>))}
+        </Table>
+      );
+    }
+    return (
+      <img
+        className="loading"
+        alt="Loading..."
+        src="/static/assets/images/loading.gif"
+      />);
+  }
+}
+DashboardTable.propTypes = propTypes;
diff --git a/superset/assets/javascripts/welcome/index.jsx b/superset/assets/javascripts/welcome/index.jsx
new file mode 100644
index 0000000..3994b99
--- /dev/null
+++ b/superset/assets/javascripts/welcome/index.jsx
@@ -0,0 +1,17 @@
+/* eslint no-unused-vars: 0 */
+import React from 'react';
+import ReactDOM from 'react-dom';
+import { Panel, Row, Col, FormControl } from 'react-bootstrap';
+
+import { appSetup } from '../common';
+import App from './App';
+
+appSetup();
+
+const container = document.getElementById('app');
+const bootstrap = JSON.parse(container.getAttribute('data-bootstrap'));
+
+ReactDOM.render(
+  <App />,
+  container,
+);
diff --git a/superset/assets/spec/javascripts/welcome/App_spec.jsx b/superset/assets/spec/javascripts/welcome/App_spec.jsx
new file mode 100644
index 0000000..472c0e2
--- /dev/null
+++ b/superset/assets/spec/javascripts/welcome/App_spec.jsx
@@ -0,0 +1,22 @@
+import React from 'react';
+import { Panel, Col, Row } from 'react-bootstrap';
+import { shallow } from 'enzyme';
+import { describe, it } from 'mocha';
+import { expect } from 'chai';
+
+import App from '../../../javascripts/welcome/App';
+
+describe('App', () => {
+  const mockedProps = {};
+  it('is valid', () => {
+    expect(
+      React.isValidElement(<App {...mockedProps} />),
+    ).to.equal(true);
+  });
+  it('renders 2 Col', () => {
+    const wrapper = shallow(<App {...mockedProps} />);
+    expect(wrapper.find(Panel)).to.have.length(1);
+    expect(wrapper.find(Row)).to.have.length(1);
+    expect(wrapper.find(Col)).to.have.length(2);
+  });
+});
diff --git a/superset/assets/spec/javascripts/welcome/DashboardTable_spec.jsx b/superset/assets/spec/javascripts/welcome/DashboardTable_spec.jsx
new file mode 100644
index 0000000..2a97279
--- /dev/null
+++ b/superset/assets/spec/javascripts/welcome/DashboardTable_spec.jsx
@@ -0,0 +1,31 @@
+import React from 'react';
+import { mount } from 'enzyme';
+import { describe, it } from 'mocha';
+import { expect } from 'chai';
+
+import DashboardTable from '../../../javascripts/welcome/DashboardTable';
+
+const $ = window.$ = require('jquery');
+
+
+describe('DashboardTable', () => {
+  const mockedProps = {};
+  let stub;
+  beforeEach(() => {
+    stub = sinon.stub($, 'getJSON');
+  });
+  afterEach(() => {
+    stub.restore();
+  });
+
+  it('is valid', () => {
+    expect(
+      React.isValidElement(<DashboardTable {...mockedProps} />),
+    ).to.equal(true);
+  });
+  it('renders', () => {
+    const wrapper = mount(<DashboardTable {...mockedProps} />);
+    expect(stub.callCount).to.equal(1);
+    expect(wrapper.find('img')).to.have.length(1);
+  });
+});
diff --git a/superset/assets/stylesheets/superset.less b/superset/assets/stylesheets/superset.less
index 14c7519..c5a8ea7 100644
--- a/superset/assets/stylesheets/superset.less
+++ b/superset/assets/stylesheets/superset.less
@@ -402,3 +402,24 @@ g.annotation-container {
 .stroke-primary {
   stroke: @brand-primary;
 }
+.reactable-header-sortable{
+    position:relative;
+    padding-right: 40px;
+}
+
+.reactable-header-sortable::before{
+    font: normal normal normal 14px/1 FontAwesome;
+    content: "\f0dc";
+    position: absolute;
+    top: 17px;
+    right: 15px;
+    color: @brand-primary;
+}
+.reactable-header-sort-asc::before{
+    content: "\f0de";
+    color: @brand-primary;
+}
+.reactable-header-sort-desc::before{
+    content: "\f0dd";
+    color: @brand-primary;
+}
diff --git a/superset/assets/webpack.config.js b/superset/assets/webpack.config.js
index ca1465e..1dce524 100644
--- a/superset/assets/webpack.config.js
+++ b/superset/assets/webpack.config.js
@@ -21,7 +21,7 @@ const config = {
     explore: ['babel-polyfill', APP_DIR + '/javascripts/explore/index.jsx'],
     dashboard: ['babel-polyfill', APP_DIR + '/javascripts/dashboard/index.jsx'],
     sqllab: ['babel-polyfill', APP_DIR + '/javascripts/SqlLab/index.jsx'],
-    welcome: ['babel-polyfill', APP_DIR + '/javascripts/welcome.js'],
+    welcome: ['babel-polyfill', APP_DIR + '/javascripts/welcome/index.jsx'],
     profile: ['babel-polyfill', APP_DIR + '/javascripts/profile/index.jsx'],
   },
   output: {
diff --git a/superset/models/helpers.py b/superset/models/helpers.py
index 02b2cf2..948cf0d 100644
--- a/superset/models/helpers.py
+++ b/superset/models/helpers.py
@@ -248,6 +248,11 @@ class AuditMixinNullable(AuditMixin):
         url = '/superset/profile/{}/'.format(user.username)
         return Markup('<a href="{}">{}</a>'.format(url, escape(user) or ''))
 
+    def changed_by_name(self):
+        if self.created_by:
+            return escape('{}'.format(self.created_by))
+        return ''
+
     @renders('created_by')
     def creator(self):  # noqa
         return self._user_link(self.created_by)
diff --git a/superset/templates/superset/welcome.html b/superset/templates/superset/welcome.html
deleted file mode 100644
index 4db2cd3..0000000
--- a/superset/templates/superset/welcome.html
+++ /dev/null
@@ -1,31 +0,0 @@
-{% extends "superset/basic.html" %}
-
-{% block title %}{{ _("Welcome!") }}{% endblock %}
-
-{% block body %}
-<div class="container welcome">
-  {% include 'superset/flash_wrapper.html' %}
-  <div class="panel panel-default">
-    <div class="panel-heading">
-      <div class="panel-title">
-        <div class="row">
-          <div class="col-md-6">
-            <h2>{{ _("Dashboards") }}</h2>
-          </div>
-          <div class="col-md-6">
-            <div class="search-container pull-right">
-              <i class="fa fa-search"></i>
-              <span class="search"></span>
-            </div>
-          </div>
-        </div>
-      </div>
-    </div>
-    <div class="panel-body">
-      <img class="loading" src="/static/assets/images/loading.gif"/>
-      <table id="dash_table" class="table" width="100%"></table>
-    </div>
-  </div>
-</div>
-{% endblock %}
-
diff --git a/superset/views/core.py b/superset/views/core.py
index 1f5cfd0..b235964 100755
--- a/superset/views/core.py
+++ b/superset/views/core.py
@@ -619,7 +619,10 @@ appbuilder.add_view(
 
 
 class DashboardModelViewAsync(DashboardModelView):  # noqa
-    list_columns = ['dashboard_link', 'creator', 'modified', 'dashboard_title']
+    list_columns = [
+        'id', 'dashboard_link', 'creator', 'modified', 'dashboard_title',
+        'changed_on', 'url', 'changed_by_name',
+    ]
     label_columns = {
         'dashboard_link': _('Dashboard'),
         'dashboard_title': _('Title'),
@@ -2463,8 +2466,15 @@ class Superset(BaseSupersetView):
         """Personalized welcome page"""
         if not g.user or not g.user.get_id():
             return redirect(appbuilder.get_url_for_login)
+        payload = {
+            'common': self.common_bootsrap_payload(),
+        }
         return self.render_template(
-            'superset/welcome.html', entry='welcome', utils=utils)
+            'superset/basic.html',
+            entry='welcome',
+            title='Superset',
+            bootstrap_data=json.dumps(payload, default=utils.json_iso_dttm_ser),
+        )
 
     @has_access
     @expose('/profile/<username>/')
@@ -2510,7 +2520,6 @@ class Superset(BaseSupersetView):
         return self.render_template(
             'superset/basic.html',
             title=user.username + "'s profile",
-            navbar_container=True,
             entry='profile',
             bootstrap_data=json.dumps(payload, default=utils.json_iso_dttm_ser),
         )

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