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 ( + Loading...); + } +} +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), )