diff --git a/ui/src/auth/Authorized.js b/ui/src/auth/Authorized.js new file mode 100644 index 0000000000..e8eefc1bd9 --- /dev/null +++ b/ui/src/auth/Authorized.js @@ -0,0 +1,89 @@ +import React, {PropTypes} from 'react' +import {connect} from 'react-redux' +import _ from 'lodash' + +export const VIEWER_ROLE = 'viewer' +export const EDITOR_ROLE = 'editor' +export const ADMIN_ROLE = 'admin' +export const SUPERADMIN_ROLE = 'superadmin' + +export const isUserAuthorized = (meRole, requiredRole) => { + switch (requiredRole) { + case VIEWER_ROLE: + return ( + meRole === VIEWER_ROLE || + meRole === EDITOR_ROLE || + meRole === ADMIN_ROLE || + meRole === SUPERADMIN_ROLE + ) + case EDITOR_ROLE: + return ( + meRole === EDITOR_ROLE || + meRole === ADMIN_ROLE || + meRole === SUPERADMIN_ROLE + ) + case ADMIN_ROLE: + return meRole === ADMIN_ROLE || meRole === SUPERADMIN_ROLE + case SUPERADMIN_ROLE: + return meRole === SUPERADMIN_ROLE + default: + return false + } +} + +export const getMeRole = me => { + return _.get(_.first(_.get(me, 'roles', [])), 'name', 'none') // TODO: TBD if 'none' should be returned if none +} + +const Authorized = ({ + children, + me, + isUsingAuth, + requiredRole, + replaceWith, + propsOverride, +}) => { + // if me response has not been received yet, render nothing + if (typeof isUsingAuth !== 'boolean') { + return null + } + + // React.isValidElement guards against multiple children wrapped by Authorized + const firstChild = React.isValidElement(children) ? children : children[0] + + const meRole = getMeRole(me) + + if (!isUsingAuth || isUserAuthorized(meRole, requiredRole)) { + return firstChild + } + + if (propsOverride) { + return React.cloneElement(firstChild, {...propsOverride}) + } + + return replaceWith || null +} + +const {arrayOf, bool, node, shape, string} = PropTypes + +Authorized.propTypes = { + isUsingAuth: bool, + replaceWith: node, + children: node.isRequired, + me: shape({ + roles: arrayOf( + shape({ + name: string.isRequired, + }) + ), + }), + requiredRole: string.isRequired, + propsOverride: shape(), +} + +const mapStateToProps = ({auth: {me, isUsingAuth}}) => ({ + me, + isUsingAuth, +}) + +export default connect(mapStateToProps)(Authorized) diff --git a/ui/src/dashboards/components/DashboardHeader.js b/ui/src/dashboards/components/DashboardHeader.js index 18b59d4d04..ce47d1504a 100644 --- a/ui/src/dashboards/components/DashboardHeader.js +++ b/ui/src/dashboards/components/DashboardHeader.js @@ -1,6 +1,8 @@ import React, {PropTypes} from 'react' import classnames from 'classnames' +import Authorized, {EDITOR_ROLE} from 'src/auth/Authorized' + import AutoRefreshDropdown from 'shared/components/AutoRefreshDropdown' import TimeRangeDropdown from 'shared/components/TimeRangeDropdown' import SourceIndicator from 'shared/components/SourceIndicator' @@ -46,13 +48,22 @@ const DashboardHeader = ({ /> : null} {dashboard - ? + ? + {activeDashboard} + + } + > + + :

{activeDashboard}

} @@ -61,10 +72,15 @@ const DashboardHeader = ({ {dashboard - ? + ? + + : null} {dashboard ?
{tableHeader} - + + +
None} - + }> + + )} diff --git a/ui/src/dashboards/components/TemplateControlBar.js b/ui/src/dashboards/components/TemplateControlBar.js index dfaa4751ac..558335ef6a 100644 --- a/ui/src/dashboards/components/TemplateControlBar.js +++ b/ui/src/dashboards/components/TemplateControlBar.js @@ -2,6 +2,8 @@ import React, {PropTypes} from 'react' import classnames from 'classnames' import calculateSize from 'calculate-size' +import Authorized, {EDITOR_ROLE} from 'src/auth/Authorized' + import Dropdown from 'shared/components/Dropdown' const minTempVarDropdownWidth = 146 @@ -75,13 +77,15 @@ const TemplateControlBar = ({ This dashboard does not have any Template Variables
} - + + + diff --git a/ui/src/shared/components/Dropdown.js b/ui/src/shared/components/Dropdown.js index 92efa21028..33d93aafb7 100644 --- a/ui/src/shared/components/Dropdown.js +++ b/ui/src/shared/components/Dropdown.js @@ -164,7 +164,7 @@ class Dropdown extends Component { > {item.text} - {actions.length > 0 + {actions && actions.length ?
{actions.map(action => { return ( diff --git a/ui/src/shared/components/LayoutCell.js b/ui/src/shared/components/LayoutCell.js index 3303efd35c..556638e1c9 100644 --- a/ui/src/shared/components/LayoutCell.js +++ b/ui/src/shared/components/LayoutCell.js @@ -1,6 +1,8 @@ import React, {Component, PropTypes} from 'react' import _ from 'lodash' +import Authorized, {EDITOR_ROLE} from 'src/auth/Authorized' + import LayoutCellMenu from 'shared/components/LayoutCellMenu' import LayoutCellHeader from 'shared/components/LayoutCellHeader' import {errorThrown} from 'shared/actions/errors' @@ -52,17 +54,19 @@ class LayoutCell extends Component { return (
- + + + - + + +
}
diff --git a/ui/src/shared/components/LayoutRenderer.js b/ui/src/shared/components/LayoutRenderer.js index 42bcc22307..1faf1cca6e 100644 --- a/ui/src/shared/components/LayoutRenderer.js +++ b/ui/src/shared/components/LayoutRenderer.js @@ -4,6 +4,8 @@ import Resizeable from 'react-component-resizable' import _ from 'lodash' +import Authorized, {EDITOR_ROLE} from 'src/auth/Authorized' + import Layout from 'src/shared/components/Layout' import { @@ -87,43 +89,59 @@ class LayoutRenderer extends Component { return ( - - {cells.map(cell => -
- -
- )} -
+ + {cells.map(cell => +
+ + + +
+ )} +
+
) } diff --git a/ui/src/shared/components/RoleIndicator.js b/ui/src/shared/components/RoleIndicator.js new file mode 100644 index 0000000000..2cd5b96a09 --- /dev/null +++ b/ui/src/shared/components/RoleIndicator.js @@ -0,0 +1,55 @@ +import React, {PropTypes} from 'react' +import uuid from 'node-uuid' +import {connect} from 'react-redux' + +import ReactTooltip from 'react-tooltip' + +import {getMeRole} from 'src/auth/Authorized' + +const RoleIndicator = ({me, isUsingAuth}) => { + if (!isUsingAuth) { + return null + } + + const roleName = getMeRole(me) + + const RoleTooltip = `

Role: ${roleName}

` + const uuidTooltip = uuid.v4() + + return ( +
+ + +
+ ) +} + +const {arrayOf, bool, shape, string} = PropTypes + +RoleIndicator.propTypes = { + isUsingAuth: bool.isRequired, + me: shape({ + roles: arrayOf( + shape({ + name: string.isRequired, + }) + ), + }), +} + +const mapStateToProps = ({auth: {me, isUsingAuth}}) => ({ + me, + isUsingAuth, +}) + +export default connect(mapStateToProps)(RoleIndicator) diff --git a/ui/src/shared/reducers/auth.js b/ui/src/shared/reducers/auth.js index 889f1887fb..09f406da61 100644 --- a/ui/src/shared/reducers/auth.js +++ b/ui/src/shared/reducers/auth.js @@ -30,7 +30,7 @@ const authReducer = (state = initialState, action) => { } case 'LOGOUT_LINK_RECEIVED': { const {logoutLink} = action.payload - return {...state, logoutLink} + return {...state, logoutLink, isUsingAuth: !!logoutLink} } } diff --git a/ui/src/side_nav/components/NavItems.js b/ui/src/side_nav/components/NavItems.js index 4977b4b35f..c0761582a7 100644 --- a/ui/src/side_nav/components/NavItems.js +++ b/ui/src/side_nav/components/NavItems.js @@ -66,9 +66,8 @@ const NavBlock = React.createClass({ render() { const {location, className} = this.props - const isActive = React.Children.toArray(this.props.children).find(child => { - return location.startsWith(child.props.link) + return location.startsWith(child.props.link) // if location is undefined, this will fail silently }) const children = React.Children.map(this.props.children, child => { @@ -114,19 +113,11 @@ const NavBlock = React.createClass({ const NavBar = React.createClass({ propTypes: { children: node, - location: string.isRequired, }, render() { - const children = React.Children.map(this.props.children, child => { - if (child && child.type === NavBlock) { - return React.cloneElement(child, { - location: this.props.location, - }) - } + const {children} = this.props - return child - }) return (