Skip to content

Commit

Permalink
User profile pages (favorites, created content, recent activity, secu…
Browse files Browse the repository at this point in the history
…rity & access) (#1615)

* Super

* User profile page

* Fixing python style

* Python unit tests

* Touchups and js tests

* Addressing comments
  • Loading branch information
mistercrunch committed Nov 20, 2016
1 parent 5ae98bc commit 7e1852e
Show file tree
Hide file tree
Showing 28 changed files with 903 additions and 12 deletions.
5 changes: 4 additions & 1 deletion superset/assets/javascripts/dashboard/components/Header.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@ class Header extends React.PureComponent {
return (
<div className="title">
<div className="pull-left">
<h1>{dashboard.dashboard_title}</h1>
<h1>
{dashboard.dashboard_title} &nbsp;
<span is class="favstar" class_name="Dashboard" obj_id={dashboard.id} />
</h1>
</div>
<div className="pull-right">
{!this.props.dashboard.context.standalone_mode &&
Expand Down
51 changes: 51 additions & 0 deletions superset/assets/javascripts/profile/components/App.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import React from 'react';
import { Col, Row, Tabs, Tab, Panel } from 'react-bootstrap';
import Favorites from './Favorites';
import UserInfo from './UserInfo';
import Security from './Security';
import RecentActivity from './RecentActivity';
import CreatedContent from './CreatedContent';

const propTypes = {
user: React.PropTypes.object.isRequired,
};

export default function App(props) {
return (
<div className="container app">
<Row>
<Col md={3}>
<UserInfo user={props.user} />
</Col>
<Col md={9}>
<Tabs id="options">
<Tab eventKey={1} title={<div><i className="fa fa-star" /> Favorites</div>}>
<Panel><Favorites user={props.user} /></Panel>
</Tab>
<Tab
eventKey={2}
title={
<div><i className="fa fa-paint-brush" /> Created Content</div>
}
>
<Panel>
<CreatedContent user={props.user} />
</Panel>
</Tab>
<Tab eventKey={3} title={<div><i className="fa fa-list" /> Recent Activity</div>}>
<Panel>
<RecentActivity user={props.user} />
</Panel>
</Tab>
<Tab eventKey={4} title={<div><i className="fa fa-lock" /> Security & Access</div>}>
<Panel>
<Security user={props.user} />
</Panel>
</Tab>
</Tabs>
</Col>
</Row>
</div>
);
}
App.propTypes = propTypes;
67 changes: 67 additions & 0 deletions superset/assets/javascripts/profile/components/CreatedContent.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import React from 'react';
import moment from 'moment';
import TableLoader from './TableLoader';

const propTypes = {
user: React.PropTypes.object.isRequired,
};

class CreatedContent extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
dashboardsLoading: true,
slicesLoading: true,
dashboards: [],
slices: [],
};
}
renderSliceTable() {
const mutator = (data) => data.map(slice => ({
slice: <a href={slice.url}>{slice.title}</a>,
favorited: moment.utc(slice.dttm).fromNow(),
_favorited: slice.dttm,
}));
return (
<TableLoader
dataEndpoint={`/superset/created_slices/${this.props.user.userId}/`}
className="table table-condensed"
columns={['slice', 'favorited']}
mutator={mutator}
noDataText="No slices"
sortable
/>
);
}
renderDashboardTable() {
const mutator = (data) => data.map(dash => ({
dashboard: <a href={dash.url}>{dash.title}</a>,
favorited: moment.utc(dash.dttm).fromNow(),
_favorited: dash.dttm,
}));
return (
<TableLoader
className="table table-condensed"
mutator={mutator}
dataEndpoint={`/superset/created_dashboards/${this.props.user.userId}/`}
noDataText="No dashboards"
columns={['dashboard', 'favorited']}
sortable
/>
);
}
render() {
return (
<div>
<h3>Dashboards</h3>
{this.renderDashboardTable()}
<hr />
<h3>Slices</h3>
{this.renderSliceTable()}
</div>
);
}
}
CreatedContent.propTypes = propTypes;

export default CreatedContent;
64 changes: 64 additions & 0 deletions superset/assets/javascripts/profile/components/Favorites.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import React from 'react';
import moment from 'moment';
import TableLoader from './TableLoader';

const propTypes = {
user: React.PropTypes.object.isRequired,
};

export default class Favorites extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
dashboardsLoading: true,
slicesLoading: true,
dashboards: [],
slices: [],
};
}
renderSliceTable() {
const mutator = (data) => data.map(slice => ({
slice: <a href={slice.url}>{slice.title}</a>,
favorited: moment.utc(slice.dttm).fromNow(),
_favorited: slice.dttm,
}));
return (
<TableLoader
dataEndpoint={`/superset/fave_slices/${this.props.user.userId}/`}
className="table table-condensed"
columns={['slice', 'favorited']}
mutator={mutator}
noDataText="No favorite slices yet, go click on stars!"
sortable
/>
);
}
renderDashboardTable() {
const mutator = (data) => data.map(dash => ({
dashboard: <a href={dash.url}>{dash.title}</a>,
favorited: moment.utc(dash.dttm).fromNow(),
}));
return (
<TableLoader
className="table table-condensed"
mutator={mutator}
dataEndpoint={`/superset/fave_dashboards/${this.props.user.userId}/`}
noDataText="No favorite dashboards yet, go click on stars!"
columns={['dashboard', 'favorited']}
sortable
/>
);
}
render() {
return (
<div>
<h3>Dashboards</h3>
{this.renderDashboardTable()}
<hr />
<h3>Slices</h3>
{this.renderSliceTable()}
</div>
);
}
}
Favorites.propTypes = propTypes;
44 changes: 44 additions & 0 deletions superset/assets/javascripts/profile/components/RecentActivity.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import React from 'react';
import TableLoader from './TableLoader';
import moment from 'moment';
import $ from 'jquery';

const propTypes = {
user: React.PropTypes.object,
};

export default class RecentActivity extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
recentActions: [],
};
}

componentWillMount() {
$.get(`/superset/recent_activity/${this.props.user.userId}/`, (data) => {
this.setState({ recentActions: data });
});
}
render() {
const mutator = function (data) {
return data.map(row => ({
action: row.action,
item: <a href={row.item_url}>{row.item_title}</a>,
time: moment.utc(row.time).fromNow(),
_time: row.time,
}));
};
return (
<div>
<TableLoader
className="table table-condensed"
mutator={mutator}
sortable
dataEndpoint={`/superset/recent_activity/${this.props.user.userId}/`}
/>
</div>
);
}
}
RecentActivity.propTypes = propTypes;
41 changes: 41 additions & 0 deletions superset/assets/javascripts/profile/components/Security.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import React from 'react';
import { Badge, Label } from 'react-bootstrap';

const propTypes = {
user: React.PropTypes.object.isRequired,
};
export default function Security({ user }) {
return (
<div>
<div className="roles">
<h4>
Roles <Badge>{Object.keys(user.roles).length}</Badge>
</h4>
{Object.keys(user.roles).map(role => <Label key={role}>{role}</Label>)}
<hr />
</div>
<div className="databases">
{user.permissions.database_access &&
<div>
<h4>
Databases <Badge>{user.permissions.database_access.length}</Badge>
</h4>
{user.permissions.database_access.map(role => <Label key={role}>{role}</Label>)}
<hr />
</div>
}
</div>
<div className="datasources">
{user.permissions.datasource_access &&
<div>
<h4>
Datasources <Badge>{user.permissions.datasource_access.length}</Badge>
</h4>
{user.permissions.datasource_access.map(role => <Label key={role}>{role}</Label>)}
</div>
}
</div>
</div>
);
}
Security.propTypes = propTypes;
64 changes: 64 additions & 0 deletions superset/assets/javascripts/profile/components/TableLoader.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import React from 'react';
import { Table, Tr, Td } from 'reactable';
import { Collapse } from 'react-bootstrap';
import $ from 'jquery';

const propTypes = {
dataEndpoint: React.PropTypes.string.isRequired,
mutator: React.PropTypes.func,
columns: React.PropTypes.arrayOf(React.PropTypes.string),
};

export default class TableLoader extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
isLoading: true,
data: [],
};
}
componentWillMount() {
$.get(this.props.dataEndpoint, (data) => {
let actualData = data;
if (this.props.mutator) {
actualData = this.props.mutator(data);
}
this.setState({ data: actualData, isLoading: false });
});
}
render() {
const tableProps = Object.assign({}, this.props);
let columns = this.props.columns;
if (!columns && this.state.data.length > 0) {
columns = Object.keys(this.state.data[0]).filter(col => col[0] !== '_');
}
delete tableProps.dataEndpoint;
delete tableProps.mutator;
delete tableProps.columns;
if (this.state.isLoading) {
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>
);
}
}
TableLoader.propTypes = propTypes;
48 changes: 48 additions & 0 deletions superset/assets/javascripts/profile/components/UserInfo.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import React from 'react';
import Gravatar from 'react-gravatar';
import moment from 'moment';
import { Panel } from 'react-bootstrap';

const propTypes = {
user: React.PropTypes.object.isRequired,
};
const UserInfo = ({ user }) => (
<div>
<a href="https://en.gravatar.com/">
<Gravatar
email={user.email}
width="100%"
height=""
alt="Profile picture provided by Gravatar"
className="img-rounded"
style={{ borderRadius: 15 }}
/>
</a>
<hr />
<Panel>
<h3>
<strong>{user.firstName} {user.lastName}</strong>
</h3>
<h4 className="username">
<i className="fa fa-user-o" /> {user.username}
</h4>
<hr />
<p>
<i className="fa fa-clock-o" /> joined {moment(user.createdOn, 'YYYYMMDD').fromNow()}
</p>
<p className="email">
<i className="fa fa-envelope-o" /> {user.email}
</p>
<p className="roles">
<i className="fa fa-lock" /> {Object.keys(user.roles).join(', ')}
</p>
<p>
<i className="fa fa-key" />&nbsp;
<span className="text-muted">id:</span>&nbsp;
<span className="user-id">{user.userId}</span>
</p>
</Panel>
</div>
);
UserInfo.propTypes = propTypes;
export default UserInfo;
Loading

0 comments on commit 7e1852e

Please sign in to comment.