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 (