User profile pages (favorites, created content, recent activity, security & access) (#1615)

* Super

* User profile page

* Fixing python style

* Python unit tests

* Touchups and js tests

* Addressing comments
This commit is contained in:
Maxime Beauchemin
2016-11-19 21:23:44 -08:00
committed by GitHub
parent 5ae98bc7c9
commit 7e1852ee88
28 changed files with 903 additions and 12 deletions

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;