New Landing Page v1.0 (#4463)

* Updated welcome landing page

* fixed test and linting

* linting

* addressing comments

* fix test

* fix test

* remove unneeded var

* add magic comments
This commit is contained in:
Hugh A. Miles II 2018-02-26 17:02:41 -08:00 committed by Maxime Beauchemin
parent 56f65158a2
commit 11ea83ecf1
7 changed files with 158 additions and 84 deletions

View File

@ -10,10 +10,11 @@ const propTypes = {
export default class RecentActivity extends React.PureComponent {
render() {
const rowLimit = 50;
const mutator = function (data) {
return data.map(row => ({
action: row.action,
item: <a href={row.item_url}>{row.item_title}</a>,
name: <a href={row.item_url}>{row.item_title}</a>,
type: row.action,
time: moment.utc(row.time).fromNow(),
_time: row.time,
}));
@ -24,7 +25,7 @@ export default class RecentActivity extends React.PureComponent {
className="table table-condensed"
mutator={mutator}
sortable
dataEndpoint={`/superset/recent_activity/${this.props.user.userId}/`}
dataEndpoint={`/superset/recent_activity/${this.props.user.userId}/?limit=${rowLimit}`}
/>
</div>
);

View File

@ -1,9 +1,10 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Table, Tr, Td } from 'reactable';
import { Collapse } from 'react-bootstrap';
import $ from 'jquery';
import '../../../stylesheets/reactable-pagination.css';
const propTypes = {
dataEndpoint: PropTypes.string.isRequired,
mutator: PropTypes.func,
@ -29,7 +30,7 @@ export default class TableLoader extends React.PureComponent {
}
render() {
const tableProps = Object.assign({}, this.props);
let columns = this.props.columns;
let { columns } = this.props;
if (!columns && this.state.data.length > 0) {
columns = Object.keys(this.state.data[0]).filter(col => col[0] !== '_');
}
@ -40,25 +41,21 @@ export default class TableLoader extends React.PureComponent {
return <img alt="loading" width="25" src="/static/assets/images/loading.gif" />;
}
return (
<Collapse in transitionAppear >
<div>
<Table {...tableProps}>
{this.state.data.map((row, i) => (
<Tr key={i}>
{columns.map((col) => {
if (row.hasOwnProperty('_' + col)) {
return (
<Td key={col} column={col} value={row['_' + col]}>
{row[col]}
</Td>);
}
return <Td key={col} column={col}>{row[col]}</Td>;
})}
</Tr>
))}
</Table>
</div>
</Collapse>
<Table {...tableProps} className="table" itemsPerPage={50} style={{ textTransform: 'capitalize' }}>
{this.state.data.map((row, i) => (
<Tr key={i}>
{columns.map((col) => {
if (row.hasOwnProperty('_' + col)) {
return (
<Td key={col} column={col} value={row['_' + col]}>
{row[col]}
</Td>);
}
return <Td key={col} column={col}>{row[col]}</Td>;
})}
</Tr>
))}
</Table>
);
}
}

View File

@ -1,7 +1,14 @@
import React from 'react';
import { Panel, Row, Col, FormControl } from 'react-bootstrap';
import PropTypes from 'prop-types';
import { Panel, Row, Col, Tabs, Tab, FormControl } from 'react-bootstrap';
import RecentActivity from '../profile/components/RecentActivity';
import Favorites from '../profile/components/Favorites';
import DashboardTable from './DashboardTable';
import { t } from '../locales';
const propTypes = {
user: PropTypes.object.isRequired,
};
export default class App extends React.PureComponent {
constructor(props) {
@ -17,24 +24,48 @@ export default class App extends React.PureComponent {
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>
<Tabs defaultActiveKey={1} id="uncontrolled-tab-example">
<Tab eventKey={1} title={t('Recently Viewed')}>
<Panel>
<Row>
<Col md={8}><h2>{t('Recently Viewed')}</h2></Col>
</Row>
<hr />
<RecentActivity user={this.props.user} />
</Panel>
</Tab>
<Tab eventKey={2} title={t('Favorites')}>
<Panel>
<Row>
<Col md={8}><h2>{t('Favorites')}</h2></Col>
</Row>
<hr />
<Favorites user={this.props.user} />
</Panel>
</Tab>
<Tab eventKey={3} title={t('Dashboards')}>
<Panel>
<Row>
<Col md={8}><h2>{t('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>
</Tab>
</Tabs>
</div>
);
}
}
App.propTypes = propTypes;

View File

@ -10,8 +10,13 @@ appSetup();
const container = document.getElementById('app');
const bootstrap = JSON.parse(container.getAttribute('data-bootstrap'));
const user = {
...bootstrap.user,
};
ReactDOM.render(
<App />,
<App
user={user}
/>,
container,
);

View File

@ -1,5 +1,5 @@
import React from 'react';
import { Panel, Col, Row } from 'react-bootstrap';
import { Panel, Row, Tab } from 'react-bootstrap';
import { shallow } from 'enzyme';
import { describe, it } from 'mocha';
import { expect } from 'chai';
@ -13,10 +13,10 @@ describe('App', () => {
React.isValidElement(<App {...mockedProps} />),
).to.equal(true);
});
it('renders 2 Col', () => {
it('renders 4 Tab, Panel, and Row components', () => {
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);
expect(wrapper.find(Tab)).to.have.length(3);
expect(wrapper.find(Panel)).to.have.length(3);
expect(wrapper.find(Row)).to.have.length(3);
});
});

View File

@ -4,7 +4,6 @@ from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
from collections import defaultdict
from datetime import datetime, timedelta
import json
import logging
@ -21,7 +20,6 @@ from flask_appbuilder import expose, SimpleFormView
from flask_appbuilder.actions import action
from flask_appbuilder.models.sqla.interface import SQLAInterface
from flask_appbuilder.security.decorators import has_access_api
from flask_appbuilder.security.sqla import models as ab_models
from flask_babel import gettext as __
from flask_babel import lazy_gettext as _
import pandas as pd
@ -51,6 +49,7 @@ from .base import (
generate_download_headers, get_error_msg, get_user_roles,
json_error_response, SupersetFilter, SupersetModelView, YamlExportMixin,
)
from .utils import bootstrap_user_data
config = app.config
stats_logger = config.get('STATS_LOGGER')
@ -2555,9 +2554,12 @@ class Superset(BaseSupersetView):
"""Personalized welcome page"""
if not g.user or not g.user.get_id():
return redirect(appbuilder.get_url_for_login)
payload = {
'user': bootstrap_user_data(),
'common': self.common_bootsrap_payload(),
}
return self.render_template(
'superset/basic.html',
entry='welcome',
@ -2571,44 +2573,15 @@ class Superset(BaseSupersetView):
"""User profile page"""
if not username and g.user:
username = g.user.username
user = (
db.session.query(ab_models.User)
.filter_by(username=username)
.one()
)
roles = {}
permissions = defaultdict(set)
for role in user.roles:
perms = set()
for perm in role.permissions:
if perm.permission and perm.view_menu:
perms.add(
(perm.permission.name, perm.view_menu.name),
)
if perm.permission.name in ('datasource_access', 'database_access'):
permissions[perm.permission.name].add(perm.view_menu.name)
roles[role.name] = [
[perm.permission.name, perm.view_menu.name]
for perm in role.permissions
if perm.permission and perm.view_menu
]
payload = {
'user': {
'username': user.username,
'firstName': user.first_name,
'lastName': user.last_name,
'userId': user.id,
'isActive': user.is_active(),
'createdOn': user.created_on.isoformat(),
'email': user.email,
'roles': roles,
'permissions': permissions,
},
'user': bootstrap_user_data(username, include_perms=True),
'common': self.common_bootsrap_payload(),
}
return self.render_template(
'superset/basic.html',
title=user.username + "'s profile",
title=username + "'s profile",
entry='profile',
bootstrap_data=json.dumps(payload, default=utils.json_iso_dttm_ser),
)

67
superset/views/utils.py Normal file
View File

@ -0,0 +1,67 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
from collections import defaultdict
from flask import g
from flask_appbuilder.security.sqla import models as ab_models
from superset import db
def bootstrap_user_data(username=None, include_perms=False):
if username:
username = username
else:
username = g.user.username
user = (
db.session.query(ab_models.User)
.filter_by(username=username)
.one()
)
payload = {
'username': user.username,
'firstName': user.first_name,
'lastName': user.last_name,
'userId': user.id,
'isActive': user.is_active(),
'createdOn': user.created_on.isoformat(),
'email': user.email,
}
if include_perms:
roles, permissions = get_permissions(user)
payload['roles'] = roles
payload['permissions'] = permissions
return payload
def get_permissions(user):
if not user.roles:
raise AttributeError('User object does not have roles')
roles = {}
permissions = defaultdict(set)
for role in user.roles:
perms = set()
for perm in role.permissions:
if perm.permission and perm.view_menu:
perms.add(
(perm.permission.name, perm.view_menu.name),
)
if perm.permission.name in ('datasource_access',
'database_access'):
permissions[perm.permission.name].add(perm.view_menu.name)
roles[role.name] = [
[perm.permission.name, perm.view_menu.name]
for perm in role.permissions
if perm.permission and perm.view_menu
]
return roles, permissions