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:
parent
56f65158a2
commit
11ea83ecf1
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
Loading…
Reference in New Issue