mirror of
https://github.com/apache/superset.git
synced 2026-04-17 23:25:05 +00:00
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:
committed by
GitHub
parent
5ae98bc7c9
commit
7e1852ee88
51
superset/assets/javascripts/profile/components/App.jsx
Normal file
51
superset/assets/javascripts/profile/components/App.jsx
Normal 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;
|
||||
@@ -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
superset/assets/javascripts/profile/components/Favorites.jsx
Normal file
64
superset/assets/javascripts/profile/components/Favorites.jsx
Normal 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;
|
||||
@@ -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
superset/assets/javascripts/profile/components/Security.jsx
Normal file
41
superset/assets/javascripts/profile/components/Security.jsx
Normal 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;
|
||||
@@ -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
superset/assets/javascripts/profile/components/UserInfo.jsx
Normal file
48
superset/assets/javascripts/profile/components/UserInfo.jsx
Normal 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" />
|
||||
<span className="text-muted">id:</span>
|
||||
<span className="user-id">{user.userId}</span>
|
||||
</p>
|
||||
</Panel>
|
||||
</div>
|
||||
);
|
||||
UserInfo.propTypes = propTypes;
|
||||
export default UserInfo;
|
||||
Reference in New Issue
Block a user