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
This commit is contained in:
parent
b9af019567
commit
c49fb0aa9b
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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,
|
||||
);
|
||||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 %}
|
||||
|
||||
|
|
@ -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),
|
||||
)
|
||||
|
|
|
|||
Loading…
Reference in New Issue