From 6a423f3792117d234f22cf3c4f832b6bcafc58d4 Mon Sep 17 00:00:00 2001 From: Hugh Miles Date: Sun, 18 Feb 2018 21:54:32 -0800 Subject: [PATCH 1/8] Updated welcome landing page --- .../profile/components/RecentActivity.jsx | 4 +- .../profile/components/TableLoader.jsx | 39 ++++----- superset/assets/javascripts/welcome/App.jsx | 85 ++++++++++++++----- superset/assets/javascripts/welcome/index.jsx | 7 +- superset/views/__init__.py | 1 + superset/views/core.py | 44 ++-------- superset/views/utils.py | 53 ++++++++++++ 7 files changed, 155 insertions(+), 78 deletions(-) create mode 100644 superset/views/utils.py diff --git a/superset/assets/javascripts/profile/components/RecentActivity.jsx b/superset/assets/javascripts/profile/components/RecentActivity.jsx index 476f6d6180a41..89eb59724de62 100644 --- a/superset/assets/javascripts/profile/components/RecentActivity.jsx +++ b/superset/assets/javascripts/profile/components/RecentActivity.jsx @@ -12,8 +12,8 @@ export default class RecentActivity extends React.PureComponent { render() { const mutator = function (data) { return data.map(row => ({ - action: row.action, - item: {row.item_title}, + name: {row.item_title}, + type: row.action, time: moment.utc(row.time).fromNow(), _time: row.time, })); diff --git a/superset/assets/javascripts/profile/components/TableLoader.jsx b/superset/assets/javascripts/profile/components/TableLoader.jsx index b14d6f6865a84..1e67426eea592 100644 --- a/superset/assets/javascripts/profile/components/TableLoader.jsx +++ b/superset/assets/javascripts/profile/components/TableLoader.jsx @@ -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 loading; } return ( - -
- - {this.state.data.map((row, i) => ( - - {columns.map((col) => { - if (row.hasOwnProperty('_' + col)) { - return ( - ); - } - return ; - })} - - ))} -
- {row[col]} - {row[col]}
-
-
+ + {this.state.data.map((row, i) => ( + + {columns.map((col) => { + if (row.hasOwnProperty('_' + col)) { + return ( + ); + } + return ; + })} + + ))} +
+ {row[col]} + {row[col]}
); } } diff --git a/superset/assets/javascripts/welcome/App.jsx b/superset/assets/javascripts/welcome/App.jsx index 78674c48f0e9e..844350780ba43 100644 --- a/superset/assets/javascripts/welcome/App.jsx +++ b/superset/assets/javascripts/welcome/App.jsx @@ -1,8 +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 } from 'react-bootstrap'; +import RecentActivity from '../profile/components/RecentActivity'; +import Favorites from '../profile/components/Favorites'; import DashboardTable from './DashboardTable'; +const propTypes = { + user: PropTypes.object.isRequired, +}; + export default class App extends React.PureComponent { constructor(props) { super(props); @@ -17,24 +23,65 @@ export default class App extends React.PureComponent { render() { return (
- - -

Dashboards

- - - -
-
- -
+ + + + +

Recent Viewed

+
+
+ +
+
+ + + +

Favorites

+
+
+ +
+
+ + + +

Dashboards

+
+
+ +
+
+ + + +

Datasources

+
+
+ {/* */} +
+
+ + + +

Datasources

+
+
+ {/* */} +
+
+ + + +

Queries

+
+
+ {/* */} +
+
+
); } } + +App.propTypes = propTypes; diff --git a/superset/assets/javascripts/welcome/index.jsx b/superset/assets/javascripts/welcome/index.jsx index 3994b9908b5ec..df0f774a81bf5 100644 --- a/superset/assets/javascripts/welcome/index.jsx +++ b/superset/assets/javascripts/welcome/index.jsx @@ -10,8 +10,13 @@ appSetup(); const container = document.getElementById('app'); const bootstrap = JSON.parse(container.getAttribute('data-bootstrap')); +const user = { + ...bootstrap.user, +}; ReactDOM.render( - , + , container, ); diff --git a/superset/views/__init__.py b/superset/views/__init__.py index ab93a55ce7c13..e3ca717a1769f 100644 --- a/superset/views/__init__.py +++ b/superset/views/__init__.py @@ -3,3 +3,4 @@ from . import core # noqa from . import sql_lab # noqa from . import annotations # noqa +from . import utils # noqa diff --git a/superset/views/core.py b/superset/views/core.py index c1c62796e081c..7ba4979753a75 100755 --- a/superset/views/core.py +++ b/superset/views/core.py @@ -4,7 +4,6 @@ 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.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 @@ -46,12 +44,14 @@ from superset.models.sql_lab import Query from superset.sql_parse import SupersetQuery from superset.utils import has_access, merge_extra_filters, QueryStatus +from .utils import bootstrap_user_data from .base import ( api, BaseSupersetView, CsvResponse, DeleteMixin, generate_download_headers, get_error_msg, get_user_roles, json_error_response, SupersetFilter, SupersetModelView, YamlExportMixin, ) + config = app.config stats_logger = config.get('STATS_LOGGER') log_this = models.Log.log_this @@ -2555,9 +2555,12 @@ def welcome(self): """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 +2574,15 @@ def profile(self, username): """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), '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), ) diff --git a/superset/views/utils.py b/superset/views/utils.py new file mode 100644 index 0000000000000..9c648a76bdff5 --- /dev/null +++ b/superset/views/utils.py @@ -0,0 +1,53 @@ +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(given_username=None): + if given_username: + username = given_username + else: + 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 + ] + + return { + '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, + } From ea330d1228bd30d1b98bf7879aecc12ed4863268 Mon Sep 17 00:00:00 2001 From: Hugh Miles Date: Thu, 22 Feb 2018 21:35:59 -0500 Subject: [PATCH 2/8] fixed test and linting --- superset/assets/spec/javascripts/welcome/App_spec.jsx | 11 ++++++----- superset/views/core.py | 4 ++-- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/superset/assets/spec/javascripts/welcome/App_spec.jsx b/superset/assets/spec/javascripts/welcome/App_spec.jsx index 472c0e22e7ef7..d42f9804ed994 100644 --- a/superset/assets/spec/javascripts/welcome/App_spec.jsx +++ b/superset/assets/spec/javascripts/welcome/App_spec.jsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Panel, Col, Row } from 'react-bootstrap'; +import { Panel, Col, Row, Tab } from 'react-bootstrap'; import { shallow } from 'enzyme'; import { describe, it } from 'mocha'; import { expect } from 'chai'; @@ -13,10 +13,11 @@ describe('App', () => { React.isValidElement(), ).to.equal(true); }); - it('renders 2 Col', () => { + it('renders 6 Tab, Panel, Row, and Col components', () => { 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); + expect(wrapper.find(Tab)).to.have.length(6); + expect(wrapper.find(Panel)).to.have.length(6); + expect(wrapper.find(Row)).to.have.length(6); + expect(wrapper.find(Col)).to.have.length(6); }); }); diff --git a/superset/views/core.py b/superset/views/core.py index 7ba4979753a75..605332e68fe16 100755 --- a/superset/views/core.py +++ b/superset/views/core.py @@ -44,13 +44,13 @@ from superset.models.sql_lab import Query from superset.sql_parse import SupersetQuery from superset.utils import has_access, merge_extra_filters, QueryStatus -from .utils import bootstrap_user_data + from .base import ( api, BaseSupersetView, CsvResponse, DeleteMixin, 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') From b0a8ec306652dff6e66ee02160db819c69f25eeb Mon Sep 17 00:00:00 2001 From: Hugh Miles Date: Fri, 23 Feb 2018 10:23:23 -0800 Subject: [PATCH 3/8] linting --- superset/views/core.py | 1 - 1 file changed, 1 deletion(-) diff --git a/superset/views/core.py b/superset/views/core.py index 605332e68fe16..4c0c1f49eb7e5 100755 --- a/superset/views/core.py +++ b/superset/views/core.py @@ -44,7 +44,6 @@ from superset.models.sql_lab import Query from superset.sql_parse import SupersetQuery from superset.utils import has_access, merge_extra_filters, QueryStatus - from .base import ( api, BaseSupersetView, CsvResponse, DeleteMixin, generate_download_headers, get_error_msg, get_user_roles, From 7c412ecb30ae49b5b21bda3855e328f9aa22f02c Mon Sep 17 00:00:00 2001 From: Hugh Miles Date: Fri, 23 Feb 2018 13:10:27 -0800 Subject: [PATCH 4/8] addressing comments --- .../profile/components/RecentActivity.jsx | 3 +- superset/assets/javascripts/welcome/App.jsx | 52 +++++++------------ superset/views/__init__.py | 1 - superset/views/core.py | 2 +- superset/views/utils.py | 41 ++++++++++----- 5 files changed, 48 insertions(+), 51 deletions(-) diff --git a/superset/assets/javascripts/profile/components/RecentActivity.jsx b/superset/assets/javascripts/profile/components/RecentActivity.jsx index 89eb59724de62..7099a08649095 100644 --- a/superset/assets/javascripts/profile/components/RecentActivity.jsx +++ b/superset/assets/javascripts/profile/components/RecentActivity.jsx @@ -10,6 +10,7 @@ const propTypes = { export default class RecentActivity extends React.PureComponent { render() { + const rowLimit = 50; const mutator = function (data) { return data.map(row => ({ name: {row.item_title}, @@ -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}`} /> ); diff --git a/superset/assets/javascripts/welcome/App.jsx b/superset/assets/javascripts/welcome/App.jsx index 844350780ba43..8f63719ea5128 100644 --- a/superset/assets/javascripts/welcome/App.jsx +++ b/superset/assets/javascripts/welcome/App.jsx @@ -1,9 +1,10 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { Panel, Row, Col, Tabs, Tab } from 'react-bootstrap'; +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, @@ -24,60 +25,43 @@ export default class App extends React.PureComponent { return (
- + -

Recent Viewed

+

{t('Recently Viewed')}


- + -

Favorites

+

{t('Favorites')}


- + -

Dashboards

+

{t('Dashboards')}

+ + +

- - - -

Datasources

-
-
- {/* */} -
-
- - - -

Datasources

-
-
- {/* */} -
-
- - - -

Queries

-
-
- {/* */} -
-
); diff --git a/superset/views/__init__.py b/superset/views/__init__.py index e3ca717a1769f..ab93a55ce7c13 100644 --- a/superset/views/__init__.py +++ b/superset/views/__init__.py @@ -3,4 +3,3 @@ from . import core # noqa from . import sql_lab # noqa from . import annotations # noqa -from . import utils # noqa diff --git a/superset/views/core.py b/superset/views/core.py index 4c0c1f49eb7e5..b71f73173248c 100755 --- a/superset/views/core.py +++ b/superset/views/core.py @@ -2575,7 +2575,7 @@ def profile(self, username): username = g.user.username payload = { - 'user': bootstrap_user_data(username), + 'user': bootstrap_user_data(username, include_perms=True), 'common': self.common_bootsrap_payload(), } diff --git a/superset/views/utils.py b/superset/views/utils.py index 9c648a76bdff5..ea11d3f019428 100644 --- a/superset/views/utils.py +++ b/superset/views/utils.py @@ -11,9 +11,9 @@ from superset import db -def bootstrap_user_data(given_username=None): - if given_username: - username = given_username +def bootstrap_user_data(username=None, include_perms=False): + if username: + username = username else: username = g.user.username @@ -22,6 +22,29 @@ def bootstrap_user_data(given_username=None): .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: @@ -40,14 +63,4 @@ def bootstrap_user_data(given_username=None): if perm.permission and perm.view_menu ] - return { - '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, - } + return roles, permissions From 32198bd24f62940841daccfc7733df78581d19de Mon Sep 17 00:00:00 2001 From: Hugh Miles Date: Fri, 23 Feb 2018 17:20:44 -0800 Subject: [PATCH 5/8] fix test --- superset/assets/spec/javascripts/welcome/App_spec.jsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/superset/assets/spec/javascripts/welcome/App_spec.jsx b/superset/assets/spec/javascripts/welcome/App_spec.jsx index d42f9804ed994..d51963a348aca 100644 --- a/superset/assets/spec/javascripts/welcome/App_spec.jsx +++ b/superset/assets/spec/javascripts/welcome/App_spec.jsx @@ -15,9 +15,9 @@ describe('App', () => { }); it('renders 6 Tab, Panel, Row, and Col components', () => { const wrapper = shallow(); - expect(wrapper.find(Tab)).to.have.length(6); - expect(wrapper.find(Panel)).to.have.length(6); - expect(wrapper.find(Row)).to.have.length(6); - expect(wrapper.find(Col)).to.have.length(6); + expect(wrapper.find(Tab)).to.have.length(3); + expect(wrapper.find(Panel)).to.have.length(3); + expect(wrapper.find(Row)).to.have.length(3); + expect(wrapper.find(Col)).to.have.length(3); }); }); From 6f50101bf74ad432964958656c44ae6d104ecb18 Mon Sep 17 00:00:00 2001 From: Hugh Miles Date: Sun, 25 Feb 2018 20:01:38 -0800 Subject: [PATCH 6/8] fix test --- superset/assets/spec/javascripts/welcome/App_spec.jsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/superset/assets/spec/javascripts/welcome/App_spec.jsx b/superset/assets/spec/javascripts/welcome/App_spec.jsx index d51963a348aca..c555f4da52d4e 100644 --- a/superset/assets/spec/javascripts/welcome/App_spec.jsx +++ b/superset/assets/spec/javascripts/welcome/App_spec.jsx @@ -13,11 +13,10 @@ describe('App', () => { React.isValidElement(), ).to.equal(true); }); - it('renders 6 Tab, Panel, Row, and Col components', () => { + it('renders 4 Tab, Panel, and Row components', () => { const wrapper = shallow(); expect(wrapper.find(Tab)).to.have.length(3); expect(wrapper.find(Panel)).to.have.length(3); expect(wrapper.find(Row)).to.have.length(3); - expect(wrapper.find(Col)).to.have.length(3); }); }); From 04f15fdea06f48d9306f9a3e3396269af177ebbb Mon Sep 17 00:00:00 2001 From: Hugh Miles Date: Mon, 26 Feb 2018 11:22:17 -0800 Subject: [PATCH 7/8] remove unneeded var --- superset/assets/spec/javascripts/welcome/App_spec.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/superset/assets/spec/javascripts/welcome/App_spec.jsx b/superset/assets/spec/javascripts/welcome/App_spec.jsx index c555f4da52d4e..1fc2987a57275 100644 --- a/superset/assets/spec/javascripts/welcome/App_spec.jsx +++ b/superset/assets/spec/javascripts/welcome/App_spec.jsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Panel, Col, Row, Tab } from 'react-bootstrap'; +import { Panel, Row, Tab } from 'react-bootstrap'; import { shallow } from 'enzyme'; import { describe, it } from 'mocha'; import { expect } from 'chai'; From cbc3ac05ee75ee19fc5131a6ca55d906693e8bdc Mon Sep 17 00:00:00 2001 From: Hugh Miles Date: Mon, 26 Feb 2018 15:16:19 -0800 Subject: [PATCH 8/8] add magic comments --- superset/views/utils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/superset/views/utils.py b/superset/views/utils.py index ea11d3f019428..b1f3fa2db7ffe 100644 --- a/superset/views/utils.py +++ b/superset/views/utils.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- from __future__ import absolute_import from __future__ import division from __future__ import print_function