diff --git a/.travis.yml b/.travis.yml
index d32057c8d7e59..89ca5b6e5e7a5 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 cfece7e6823b5..0000000000000
--- 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 0000000000000..78674c48f0e9e
--- /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 (
+
+
+
+ Dashboards
+
+
+
+
+
+
+
+
+ );
+ }
+}
diff --git a/superset/assets/javascripts/welcome/DashboardTable.jsx b/superset/assets/javascripts/welcome/DashboardTable.jsx
new file mode 100644
index 0000000000000..78d4bdd57b20d
--- /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 (
+
+ {this.state.dashboards.map(o => (
+
+
+ {o.dashboard_title}
+ |
+
+ {unsafe(o.creator)}
+ |
+
+ {unsafe(o.modified)}
+ |
+
))}
+
+ );
+ }
+ return (
+
);
+ }
+}
+DashboardTable.propTypes = propTypes;
diff --git a/superset/assets/javascripts/welcome/index.jsx b/superset/assets/javascripts/welcome/index.jsx
new file mode 100644
index 0000000000000..3994b9908b5ec
--- /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(
+ ,
+ 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 0000000000000..472c0e22e7ef7
--- /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(),
+ ).to.equal(true);
+ });
+ it('renders 2 Col', () => {
+ const wrapper = shallow();
+ 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 0000000000000..2a9727942d88b
--- /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(),
+ ).to.equal(true);
+ });
+ it('renders', () => {
+ const wrapper = mount();
+ 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 14c7519bcab94..c5a8ea7d4576f 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 ca1465e703b12..1dce5245f174a 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 02b2cf225c3a7..948cf0d49ede2 100644
--- a/superset/models/helpers.py
+++ b/superset/models/helpers.py
@@ -248,6 +248,11 @@ def _user_link(self, user):
url = '/superset/profile/{}/'.format(user.username)
return Markup('{}'.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 4db2cd3ab5e9a..0000000000000
--- a/superset/templates/superset/welcome.html
+++ /dev/null
@@ -1,31 +0,0 @@
-{% extends "superset/basic.html" %}
-
-{% block title %}{{ _("Welcome!") }}{% endblock %}
-
-{% block body %}
-
- {% include 'superset/flash_wrapper.html' %}
-
-
-
-
-
-
{{ _("Dashboards") }}
-
-
-
-
-
-
-

-
-
-
-
-{% endblock %}
-
diff --git a/superset/views/core.py b/superset/views/core.py
index 1f5cfd022f492..b23596436ba50 100755
--- a/superset/views/core.py
+++ b/superset/views/core.py
@@ -619,7 +619,10 @@ def download_dashboards(self):
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 @@ def welcome(self):
"""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//')
@@ -2510,7 +2520,6 @@ def profile(self, username):
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),
)