@@ -198,7 +198,7 @@ class AdminPage extends Component {
const {arrayOf, func, shape, string} = PropTypes
-AdminPage.propTypes = {
+AdminInfluxDBPage.propTypes = {
source: shape({
id: string.isRequired,
links: shape({
@@ -231,7 +231,7 @@ AdminPage.propTypes = {
notify: func,
}
-const mapStateToProps = ({admin: {users, roles, permissions}}) => ({
+const mapStateToProps = ({adminInfluxDB: {users, roles, permissions}}) => ({
users,
roles,
permissions,
@@ -267,4 +267,4 @@ const mapDispatchToProps = dispatch => ({
notify: bindActionCreators(publishAutoDismissingNotification, dispatch),
})
-export default connect(mapStateToProps, mapDispatchToProps)(AdminPage)
+export default connect(mapStateToProps, mapDispatchToProps)(AdminInfluxDBPage)
diff --git a/ui/src/admin/containers/DatabaseManagerPage.js b/ui/src/admin/containers/DatabaseManagerPage.js
index 71d7f36584..d5fc3d746f 100644
--- a/ui/src/admin/containers/DatabaseManagerPage.js
+++ b/ui/src/admin/containers/DatabaseManagerPage.js
@@ -5,7 +5,7 @@ import _ from 'lodash'
import DatabaseManager from 'src/admin/components/DatabaseManager'
-import * as adminActionCreators from 'src/admin/actions'
+import * as adminActionCreators from 'src/admin/actions/influxdb'
import {publishAutoDismissingNotification} from 'shared/dispatchers'
class DatabaseManagerPage extends Component {
@@ -155,7 +155,7 @@ DatabaseManagerPage.propTypes = {
notify: func,
}
-const mapStateToProps = ({admin: {databases, retentionPolicies}}) => ({
+const mapStateToProps = ({adminInfluxDB: {databases, retentionPolicies}}) => ({
databases,
retentionPolicies,
})
diff --git a/ui/src/admin/containers/OrganizationsPage.js b/ui/src/admin/containers/OrganizationsPage.js
new file mode 100644
index 0000000000..9ba53944aa
--- /dev/null
+++ b/ui/src/admin/containers/OrganizationsPage.js
@@ -0,0 +1,78 @@
+import React, {Component, PropTypes} from 'react'
+import {connect} from 'react-redux'
+import {bindActionCreators} from 'redux'
+
+import * as adminChronografActionCreators from 'src/admin/actions/chronograf'
+import {publishAutoDismissingNotification} from 'shared/dispatchers'
+
+import OrganizationsTable from 'src/admin/components/chronograf/OrganizationsTable'
+
+class OrganizationsPage extends Component {
+ componentDidMount() {
+ const {links, actions: {loadOrganizationsAsync}} = this.props
+
+ loadOrganizationsAsync(links.organizations)
+ }
+
+ handleCreateOrganization = organizationName => {
+ const {links, actions: {createOrganizationAsync}} = this.props
+ createOrganizationAsync(links.organizations, {name: organizationName})
+ }
+
+ handleRenameOrganization = (organization, name) => {
+ const {actions: {updateOrganizationAsync}} = this.props
+ updateOrganizationAsync(organization, {...organization, name})
+ }
+
+ handleDeleteOrganization = organization => {
+ const {actions: {deleteOrganizationAsync}} = this.props
+ deleteOrganizationAsync(organization)
+ }
+
+ render() {
+ const {organizations} = this.props
+
+ return (
+
+ )
+ }
+}
+
+const {arrayOf, func, shape, string} = PropTypes
+
+OrganizationsPage.propTypes = {
+ links: shape({
+ organizations: string.isRequired,
+ }),
+ organizations: arrayOf(
+ shape({
+ id: string, // when optimistically created, it will not have an id
+ name: string.isRequired,
+ link: string,
+ })
+ ),
+ actions: shape({
+ loadOrganizationsAsync: func.isRequired,
+ createOrganizationAsync: func.isRequired,
+ updateOrganizationAsync: func.isRequired,
+ deleteOrganizationAsync: func.isRequired,
+ }),
+ notify: func.isRequired,
+}
+
+const mapStateToProps = ({links, adminChronograf: {organizations}}) => ({
+ links,
+ organizations,
+})
+
+const mapDispatchToProps = dispatch => ({
+ actions: bindActionCreators(adminChronografActionCreators, dispatch),
+ notify: bindActionCreators(publishAutoDismissingNotification, dispatch),
+})
+
+export default connect(mapStateToProps, mapDispatchToProps)(OrganizationsPage)
diff --git a/ui/src/admin/containers/QueriesPage.js b/ui/src/admin/containers/QueriesPage.js
index 53bf93b2d9..5cfc753407 100644
--- a/ui/src/admin/containers/QueriesPage.js
+++ b/ui/src/admin/containers/QueriesPage.js
@@ -15,7 +15,7 @@ import {
loadQueries as loadQueriesAction,
setQueryToKill as setQueryToKillAction,
killQueryAsync,
-} from 'src/admin/actions'
+} from 'src/admin/actions/influxdb'
import {publishAutoDismissingNotification} from 'shared/dispatchers'
@@ -100,7 +100,7 @@ QueriesPage.propTypes = {
notify: func,
}
-const mapStateToProps = ({admin: {queries, queryIDToKill}}) => ({
+const mapStateToProps = ({adminInfluxDB: {queries, queryIDToKill}}) => ({
queries,
queryIDToKill,
})
diff --git a/ui/src/admin/index.js b/ui/src/admin/index.js
index ef6e719b35..5663e7aeb3 100644
--- a/ui/src/admin/index.js
+++ b/ui/src/admin/index.js
@@ -1,2 +1,5 @@
-import AdminPage from './containers/AdminPage'
-export {AdminPage}
+import AdminInfluxDBPage from './containers/AdminInfluxDBPage'
+import AdminChronografPage from './containers/AdminChronografPage'
+import OrganizationsPage from './containers/OrganizationsPage'
+
+export {AdminChronografPage, AdminInfluxDBPage, OrganizationsPage}
diff --git a/ui/src/admin/reducers/chronograf.js b/ui/src/admin/reducers/chronograf.js
new file mode 100644
index 0000000000..cd48710e26
--- /dev/null
+++ b/ui/src/admin/reducers/chronograf.js
@@ -0,0 +1,92 @@
+import {isSameUser} from 'shared/reducers/helpers/auth'
+
+const initialState = {
+ users: [],
+ organizations: [],
+}
+
+const adminChronograf = (state = initialState, action) => {
+ switch (action.type) {
+ case 'CHRONOGRAF_LOAD_USERS': {
+ return {...state, ...action.payload}
+ }
+
+ case 'CHRONOGRAF_LOAD_ORGANIZATIONS': {
+ return {...state, ...action.payload}
+ }
+
+ case 'CHRONOGRAF_ADD_USER': {
+ const {user} = action.payload
+ return {...state, users: [user, ...state.users]}
+ }
+
+ case 'CHRONOGRAF_UPDATE_USER': {
+ const {user, updatedUser} = action.payload
+ return {
+ ...state,
+ users: state.users.map(
+ u => (u.links.self === user.links.self ? {...updatedUser} : u)
+ ),
+ }
+ }
+ case 'CHRONOGRAF_SYNC_USER': {
+ const {staleUser, syncedUser} = action.payload
+ return {
+ ...state,
+ users: state.users.map(
+ // stale user does not have links, so uniqueness is on name, provider, & scheme
+ u => (isSameUser(u, staleUser) ? {...syncedUser} : u)
+ ),
+ }
+ }
+
+ case 'CHRONOGRAF_REMOVE_USER': {
+ const {user} = action.payload
+ return {
+ ...state,
+ // stale user does not have links, so uniqueness is on name, provider, & scheme
+ users: state.users.filter(u => !isSameUser(u, user)),
+ }
+ }
+
+ case 'CHRONOGRAF_ADD_ORGANIZATION': {
+ const {organization} = action.payload
+ return {...state, organizations: [organization, ...state.organizations]}
+ }
+
+ case 'CHRONOGRAF_RENAME_ORGANIZATION': {
+ const {organization, newName} = action.payload
+ return {
+ ...state,
+ organizations: state.organizations.map(
+ o =>
+ o.links.self === organization.links.self ? {...o, name: newName} : o
+ ),
+ }
+ }
+
+ case 'CHRONOGRAF_SYNC_ORGANIZATION': {
+ const {staleOrganization, syncedOrganization} = action.payload
+ return {
+ ...state,
+ organizations: state.organizations.map(
+ o => (o.name === staleOrganization.name ? {...syncedOrganization} : o)
+ ),
+ }
+ }
+
+ case 'CHRONOGRAF_REMOVE_ORGANIZATION': {
+ const {organization} = action.payload
+ return {
+ ...state,
+ organizations: state.organizations.filter(
+ o => o.name !== organization.name
+ ),
+ }
+ }
+ }
+
+ return state
+}
+
+export default adminChronograf
diff --git a/ui/src/admin/reducers/index.js b/ui/src/admin/reducers/index.js
new file mode 100644
index 0000000000..7356e32f48
--- /dev/null
+++ b/ui/src/admin/reducers/index.js
@@ -0,0 +1,4 @@
+import adminChronograf from './chronograf'
+import adminInfluxDB from './influxdb'
+
+export default {adminChronograf, adminInfluxDB}
diff --git a/ui/src/admin/reducers/admin.js b/ui/src/admin/reducers/influxdb.js
similarity index 85%
rename from ui/src/admin/reducers/admin.js
rename to ui/src/admin/reducers/influxdb.js
index 7450a860fb..23b86492bd 100644
--- a/ui/src/admin/reducers/admin.js
+++ b/ui/src/admin/reducers/influxdb.js
@@ -16,25 +16,25 @@ const initialState = {
databases: [],
}
-export default function admin(state = initialState, action) {
+const adminInfluxDB = (state = initialState, action) => {
switch (action.type) {
- case 'LOAD_USERS': {
+ case 'INFLUXDB_LOAD_USERS': {
return {...state, ...action.payload}
}
- case 'LOAD_ROLES': {
+ case 'INFLUXDB_LOAD_ROLES': {
return {...state, ...action.payload}
}
- case 'LOAD_PERMISSIONS': {
+ case 'INFLUXDB_LOAD_PERMISSIONS': {
return {...state, ...action.payload}
}
- case 'LOAD_DATABASES': {
+ case 'INFLUXDB_LOAD_DATABASES': {
return {...state, ...action.payload}
}
- case 'ADD_USER': {
+ case 'INFLUXDB_ADD_USER': {
const newUser = {...NEW_DEFAULT_USER, isEditing: true}
return {
...state,
@@ -42,7 +42,7 @@ export default function admin(state = initialState, action) {
}
}
- case 'ADD_ROLE': {
+ case 'INFLUXDB_ADD_ROLE': {
const newRole = {...NEW_DEFAULT_ROLE, isEditing: true}
return {
...state,
@@ -50,7 +50,7 @@ export default function admin(state = initialState, action) {
}
}
- case 'ADD_DATABASE': {
+ case 'INFLUXDB_ADD_DATABASE': {
const newDatabase = {
...NEW_DEFAULT_DATABASE,
links: {self: `temp-ID${uuid.v4()}`},
@@ -63,7 +63,7 @@ export default function admin(state = initialState, action) {
}
}
- case 'ADD_RETENTION_POLICY': {
+ case 'INFLUXDB_ADD_RETENTION_POLICY': {
const {database} = action.payload
const databases = state.databases.map(
db =>
@@ -81,7 +81,7 @@ export default function admin(state = initialState, action) {
return {...state, databases}
}
- case 'SYNC_USER': {
+ case 'INFLUXDB_SYNC_USER': {
const {staleUser, syncedUser} = action.payload
const newState = {
users: state.users.map(
@@ -91,7 +91,7 @@ export default function admin(state = initialState, action) {
return {...state, ...newState}
}
- case 'SYNC_ROLE': {
+ case 'INFLUXDB_SYNC_ROLE': {
const {staleRole, syncedRole} = action.payload
const newState = {
roles: state.roles.map(
@@ -101,7 +101,7 @@ export default function admin(state = initialState, action) {
return {...state, ...newState}
}
- case 'SYNC_DATABASE': {
+ case 'INFLUXDB_SYNC_DATABASE': {
const {stale, synced} = action.payload
const newState = {
databases: state.databases.map(
@@ -112,7 +112,7 @@ export default function admin(state = initialState, action) {
return {...state, ...newState}
}
- case 'SYNC_RETENTION_POLICY': {
+ case 'INFLUXDB_SYNC_RETENTION_POLICY': {
const {database, stale, synced} = action.payload
const newState = {
databases: state.databases.map(
@@ -132,7 +132,7 @@ export default function admin(state = initialState, action) {
return {...state, ...newState}
}
- case 'EDIT_USER': {
+ case 'INFLUXDB_EDIT_USER': {
const {user, updates} = action.payload
const newState = {
users: state.users.map(
@@ -142,7 +142,7 @@ export default function admin(state = initialState, action) {
return {...state, ...newState}
}
- case 'EDIT_ROLE': {
+ case 'INFLUXDB_EDIT_ROLE': {
const {role, updates} = action.payload
const newState = {
roles: state.roles.map(
@@ -152,7 +152,7 @@ export default function admin(state = initialState, action) {
return {...state, ...newState}
}
- case 'EDIT_DATABASE': {
+ case 'INFLUXDB_EDIT_DATABASE': {
const {database, updates} = action.payload
const newState = {
databases: state.databases.map(
@@ -164,7 +164,7 @@ export default function admin(state = initialState, action) {
return {...state, ...newState}
}
- case 'EDIT_RETENTION_POLICY': {
+ case 'INFLUXDB_EDIT_RETENTION_POLICY': {
const {database, retentionPolicy, updates} = action.payload
const newState = {
@@ -187,7 +187,7 @@ export default function admin(state = initialState, action) {
return {...state, ...newState}
}
- case 'DELETE_USER': {
+ case 'INFLUXDB_DELETE_USER': {
const {user} = action.payload
const newState = {
users: state.users.filter(u => u.links.self !== user.links.self),
@@ -196,7 +196,7 @@ export default function admin(state = initialState, action) {
return {...state, ...newState}
}
- case 'DELETE_ROLE': {
+ case 'INFLUXDB_DELETE_ROLE': {
const {role} = action.payload
const newState = {
roles: state.roles.filter(r => r.links.self !== role.links.self),
@@ -205,7 +205,7 @@ export default function admin(state = initialState, action) {
return {...state, ...newState}
}
- case 'REMOVE_DATABASE': {
+ case 'INFLUXDB_REMOVE_DATABASE': {
const {database} = action.payload
const newState = {
databases: state.databases.filter(
@@ -216,7 +216,7 @@ export default function admin(state = initialState, action) {
return {...state, ...newState}
}
- case 'REMOVE_RETENTION_POLICY': {
+ case 'INFLUXDB_REMOVE_RETENTION_POLICY': {
const {database, retentionPolicy} = action.payload
const newState = {
databases: state.databases.map(
@@ -235,7 +235,7 @@ export default function admin(state = initialState, action) {
return {...state, ...newState}
}
- case 'ADD_DATABASE_DELETE_CODE': {
+ case 'INFLUXDB_ADD_DATABASE_DELETE_CODE': {
const {database} = action.payload
const newState = {
databases: state.databases.map(
@@ -247,7 +247,7 @@ export default function admin(state = initialState, action) {
return {...state, ...newState}
}
- case 'REMOVE_DATABASE_DELETE_CODE': {
+ case 'INFLUXDB_REMOVE_DATABASE_DELETE_CODE': {
const {database} = action.payload
delete database.deleteCode
@@ -260,11 +260,11 @@ export default function admin(state = initialState, action) {
return {...state, ...newState}
}
- case 'LOAD_QUERIES': {
+ case 'INFLUXDB_LOAD_QUERIES': {
return {...state, ...action.payload}
}
- case 'FILTER_USERS': {
+ case 'INFLUXDB_FILTER_USERS': {
const {text} = action.payload
const newState = {
users: state.users.map(u => {
@@ -275,7 +275,7 @@ export default function admin(state = initialState, action) {
return {...state, ...newState}
}
- case 'FILTER_ROLES': {
+ case 'INFLUXDB_FILTER_ROLES': {
const {text} = action.payload
const newState = {
roles: state.roles.map(r => {
@@ -286,7 +286,7 @@ export default function admin(state = initialState, action) {
return {...state, ...newState}
}
- case 'KILL_QUERY': {
+ case 'INFLUXDB_KILL_QUERY': {
const {queryID} = action.payload
const nextState = {
queries: reject(state.queries, q => +q.id === +queryID),
@@ -295,10 +295,12 @@ export default function admin(state = initialState, action) {
return {...state, ...nextState}
}
- case 'SET_QUERY_TO_KILL': {
+ case 'INFLUXDB_SET_QUERY_TO_KILL': {
return {...state, ...action.payload}
}
}
return state
}
+
+export default adminInfluxDB
diff --git a/ui/src/auth/Authorized.js b/ui/src/auth/Authorized.js
index e8eefc1bd9..ed66d783b4 100644
--- a/ui/src/auth/Authorized.js
+++ b/ui/src/auth/Authorized.js
@@ -1,7 +1,7 @@
import React, {PropTypes} from 'react'
import {connect} from 'react-redux'
-import _ from 'lodash'
+export const MEMBER_ROLE = 'member'
export const VIEWER_ROLE = 'viewer'
export const EDITOR_ROLE = 'editor'
export const ADMIN_ROLE = 'admin'
@@ -26,21 +26,21 @@ export const isUserAuthorized = (meRole, requiredRole) => {
return meRole === ADMIN_ROLE || meRole === SUPERADMIN_ROLE
case SUPERADMIN_ROLE:
return meRole === SUPERADMIN_ROLE
+ // 'member' is the default role and has no authorization for anything currently
+ case MEMBER_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,
+ meRole,
isUsingAuth,
requiredRole,
- replaceWith,
+ replaceWithIfNotAuthorized,
+ replaceWithIfNotUsingAuth,
+ replaceWithIfAuthorized,
propsOverride,
}) => {
// if me response has not been received yet, render nothing
@@ -51,38 +51,38 @@ const Authorized = ({
// React.isValidElement guards against multiple children wrapped by Authorized
const firstChild = React.isValidElement(children) ? children : children[0]
- const meRole = getMeRole(me)
+ if (!isUsingAuth) {
+ return replaceWithIfNotUsingAuth || firstChild
+ }
- if (!isUsingAuth || isUserAuthorized(meRole, requiredRole)) {
- return firstChild
+ if (isUserAuthorized(meRole, requiredRole)) {
+ return replaceWithIfAuthorized || firstChild
}
if (propsOverride) {
return React.cloneElement(firstChild, {...propsOverride})
}
- return replaceWith || null
+ return replaceWithIfNotAuthorized || null
}
-const {arrayOf, bool, node, shape, string} = PropTypes
+const {bool, node, shape, string} = PropTypes
Authorized.propTypes = {
isUsingAuth: bool,
- replaceWith: node,
+ replaceWithIfNotUsingAuth: node,
+ replaceWithIfAuthorized: node,
+ replaceWithIfNotAuthorized: node,
children: node.isRequired,
me: shape({
- roles: arrayOf(
- shape({
- name: string.isRequired,
- })
- ),
+ role: string.isRequired,
}),
requiredRole: string.isRequired,
propsOverride: shape(),
}
-const mapStateToProps = ({auth: {me, isUsingAuth}}) => ({
- me,
+const mapStateToProps = ({auth: {me: {role}, isUsingAuth}}) => ({
+ meRole: role,
isUsingAuth,
})
diff --git a/ui/src/auth/Purgatory.js b/ui/src/auth/Purgatory.js
new file mode 100644
index 0000000000..14774a9feb
--- /dev/null
+++ b/ui/src/auth/Purgatory.js
@@ -0,0 +1,70 @@
+import React, {PropTypes} from 'react'
+import {connect} from 'react-redux'
+
+import {MEMBER_ROLE} from 'src/auth/Authorized'
+
+const memberCopy = (
+
This role does not grant you sufficient permissions to view Chronograf.
+)
+const viewerCopy = (
+
+ This organization does not have any configured sources
and your role
+ does not have permission to configure a source.
+
+)
+
+const Purgatory = ({name, provider, scheme, currentOrganization, role}) =>
+
+
+
+
+
+
+ Logged in to {currentOrganization.name} as a{' '}
+ {role}.
+
+ {role === MEMBER_ROLE ? memberCopy : viewerCopy}
+
Contact your Administrator for assistance.
+
+
+
+ username: {name}
+
+ provider: {provider}
+
+ scheme: {scheme}
+
+
+
+
+
+ Made by InfluxData
+
+
+
+
+
+const {shape, string} = PropTypes
+
+Purgatory.propTypes = {
+ name: string.isRequired,
+ provider: string.isRequired,
+ scheme: string.isRequired,
+ currentOrganization: shape({
+ id: string.isRequired,
+ name: string.isRequired,
+ }).isRequired,
+ role: string.isRequired,
+}
+
+const mapStateToProps = ({
+ auth: {me: {name, provider, scheme, currentOrganization, role}},
+}) => ({
+ name,
+ provider,
+ scheme,
+ currentOrganization,
+ role,
+})
+
+export default connect(mapStateToProps)(Purgatory)
diff --git a/ui/src/auth/index.js b/ui/src/auth/index.js
index 0b9eb546a7..9239661bea 100644
--- a/ui/src/auth/index.js
+++ b/ui/src/auth/index.js
@@ -1,7 +1,15 @@
import Login from './Login'
+import Purgatory from './Purgatory'
+
import {
UserIsAuthenticated,
Authenticated,
UserIsNotAuthenticated,
} from './Authenticated'
-export {Login, UserIsAuthenticated, Authenticated, UserIsNotAuthenticated}
+export {
+ Login,
+ Purgatory,
+ UserIsAuthenticated,
+ Authenticated,
+ UserIsNotAuthenticated,
+}
diff --git a/ui/src/dashboards/components/DashboardHeader.js b/ui/src/dashboards/components/DashboardHeader.js
index ce47d1504a..bf101c0373 100644
--- a/ui/src/dashboards/components/DashboardHeader.js
+++ b/ui/src/dashboards/components/DashboardHeader.js
@@ -50,7 +50,7 @@ const DashboardHeader = ({
{dashboard
?
{activeDashboard}
diff --git a/ui/src/dashboards/components/DashboardsTable.js b/ui/src/dashboards/components/DashboardsTable.js
index 9f688ad47d..887c56d80b 100644
--- a/ui/src/dashboards/components/DashboardsTable.js
+++ b/ui/src/dashboards/components/DashboardsTable.js
@@ -38,7 +38,10 @@ const DashboardsTable = ({
)
: None}
- }>
+ }
+ >
{
@@ -125,6 +142,7 @@ const Root = React.createClass({
+
-
+
+
diff --git a/ui/src/shared/actions/auth.js b/ui/src/shared/actions/auth.js
index dc0334f4f9..43ab62ecc0 100644
--- a/ui/src/shared/actions/auth.js
+++ b/ui/src/shared/actions/auth.js
@@ -1,3 +1,8 @@
+import {updateMe as updateMeAJAX} from 'shared/apis/auth'
+
+import {publishAutoDismissingNotification} from 'shared/dispatchers'
+import {errorThrown} from 'shared/actions/errors'
+
export const authExpired = auth => ({
type: 'AUTH_EXPIRED',
payload: {
@@ -20,16 +25,56 @@ export const meRequested = () => ({
type: 'ME_REQUESTED',
})
-export const meReceived = me => ({
- type: 'ME_RECEIVED',
+export const meReceivedNotUsingAuth = me => ({
+ type: 'ME_RECEIVED__NON_AUTH',
payload: {
me,
},
})
+export const meReceivedUsingAuth = me => ({
+ type: 'ME_RECEIVED__AUTH',
+ payload: {
+ me,
+ },
+})
+
+export const meChangeOrganizationRequested = () => ({
+ type: 'ME_CHANGE_ORGANIZATION_REQUESTED',
+})
+
+export const meChangeOrganizationCompleted = () => ({
+ type: 'ME_CHANGE_ORGANIZATION_COMPLETED',
+})
+
+export const meChangeOrganizationFailed = () => ({
+ type: 'ME_CHANGE_ORGANIZATION_FAILED',
+})
+
export const logoutLinkReceived = logoutLink => ({
type: 'LOGOUT_LINK_RECEIVED',
payload: {
logoutLink,
},
})
+
+export const meChangeOrganizationAsync = (
+ url,
+ organization
+) => async dispatch => {
+ dispatch(meChangeOrganizationRequested())
+ try {
+ const {data} = await updateMeAJAX(url, organization)
+ dispatch(
+ publishAutoDismissingNotification(
+ 'success',
+ `Now signed into ${data.currentOrganization.name}`
+ )
+ )
+ dispatch(meChangeOrganizationCompleted())
+ dispatch(meReceivedUsingAuth(data))
+ } catch (error) {
+ dispatch(errorThrown(error))
+ dispatch(meChangeOrganizationFailed())
+ }
+}
diff --git a/ui/src/shared/apis/auth.js b/ui/src/shared/apis/auth.js
new file mode 100644
index 0000000000..581dfadd8e
--- /dev/null
+++ b/ui/src/shared/apis/auth.js
@@ -0,0 +1,14 @@
+import AJAX from 'src/utils/ajax'
+
+export const updateMe = async (url, updatedMe) => {
+ try {
+ return await AJAX({
+ method: 'PUT',
+ url,
+ data: updatedMe,
+ })
+ } catch (error) {
+ console.error(error)
+ throw error
+ }
+}
diff --git a/ui/src/shared/components/ConfirmButtons.js b/ui/src/shared/components/ConfirmButtons.js
index 0272bc6614..6a7f59cef4 100644
--- a/ui/src/shared/components/ConfirmButtons.js
+++ b/ui/src/shared/components/ConfirmButtons.js
@@ -1,6 +1,8 @@
import React, {PropTypes, Component} from 'react'
import classnames from 'classnames'
+import OnClickOutside from 'shared/components/OnClickOutside'
+
class ConfirmButtons extends Component {
constructor(props) {
super(props)
@@ -14,31 +16,54 @@ class ConfirmButtons extends Component {
this.props.onCancel(item)
}
- render() {
- const {item, buttonSize, isDisabled} = this.props
+ handleClickOutside = () => {
+ this.props.onClickOutside(this.props.item)
+ }
- return (
-
-
-
-
- )
+ render() {
+ const {item, buttonSize, isDisabled, confirmLeft} = this.props
+
+ return confirmLeft
+ ?
+
+
+
+ :
+
+
+
}
}
@@ -50,9 +75,12 @@ ConfirmButtons.propTypes = {
onCancel: func.isRequired,
buttonSize: string,
isDisabled: bool,
+ onClickOutside: func,
+ confirmLeft: bool,
}
ConfirmButtons.defaultProps = {
buttonSize: 'btn-sm',
+ onClickOutside: () => {},
}
-export default ConfirmButtons
+export default OnClickOutside(ConfirmButtons)
diff --git a/ui/src/shared/components/DeleteConfirmButtons.js b/ui/src/shared/components/DeleteConfirmButtons.js
index 3f503727d2..4a7aec5836 100644
--- a/ui/src/shared/components/DeleteConfirmButtons.js
+++ b/ui/src/shared/components/DeleteConfirmButtons.js
@@ -4,14 +4,14 @@ import classnames from 'classnames'
import OnClickOutside from 'shared/components/OnClickOutside'
import ConfirmButtons from 'shared/components/ConfirmButtons'
-const DeleteButton = ({onClickDelete, buttonSize}) =>
+const DeleteButton = ({onClickDelete, buttonSize, text}) =>
class DeleteConfirmButtons extends Component {
@@ -37,7 +37,7 @@ class DeleteConfirmButtons extends Component {
}
render() {
- const {onDelete, item, buttonSize} = this.props
+ const {onDelete, item, buttonSize, text} = this.props
const {isConfirming} = this.state
return isConfirming
@@ -48,6 +48,7 @@ class DeleteConfirmButtons extends Component {
buttonSize={buttonSize}
/>
:
@@ -57,11 +58,17 @@ class DeleteConfirmButtons extends Component {
const {func, oneOfType, shape, string} = PropTypes
DeleteButton.propTypes = {
+ text: string.isRequired,
onClickDelete: func.isRequired,
buttonSize: string,
}
+DeleteButton.defaultProps = {
+ text: 'Delete',
+}
+
DeleteConfirmButtons.propTypes = {
+ text: string,
item: oneOfType([(string, shape())]),
onDelete: func.isRequired,
buttonSize: string,
diff --git a/ui/src/shared/components/Dropdown.js b/ui/src/shared/components/Dropdown.js
index 33d93aafb7..19e6cd887c 100644
--- a/ui/src/shared/components/Dropdown.js
+++ b/ui/src/shared/components/Dropdown.js
@@ -22,6 +22,7 @@ class Dropdown extends Component {
buttonColor: 'btn-default',
menuWidth: '100%',
useAutoComplete: false,
+ disabled: false,
}
handleClickOutside = () => {
@@ -29,6 +30,11 @@ class Dropdown extends Component {
}
handleClick = e => {
+ const {disabled} = this.props
+
+ if (disabled) {
+ return
+ }
this.toggleMenu(e)
if (this.props.onClick) {
this.props.onClick(e)
@@ -208,10 +214,12 @@ class Dropdown extends Component {
buttonColor,
toggleStyle,
useAutoComplete,
+ disabled,
} = this.props
const {isOpen, searchTerm, filteredItems} = this.state
const menuItems = useAutoComplete ? filteredItems : items
+ const disabledClass = disabled ? 'disabled' : null
return (
{useAutoComplete && isOpen
?
:
{iconName
@@ -297,6 +305,7 @@ Dropdown.propTypes = {
menuClass: string,
useAutoComplete: bool,
toggleStyle: shape(),
+ disabled: bool,
}
export default OnClickOutside(Dropdown)
diff --git a/ui/src/shared/components/RoleIndicator.js b/ui/src/shared/components/RoleIndicator.js
index 2cd5b96a09..e955ebb9fe 100644
--- a/ui/src/shared/components/RoleIndicator.js
+++ b/ui/src/shared/components/RoleIndicator.js
@@ -4,7 +4,7 @@ import {connect} from 'react-redux'
import ReactTooltip from 'react-tooltip'
-import {getMeRole} from 'src/auth/Authorized'
+import {getMeRole} from 'shared/reducers/helpers/auth'
const RoleIndicator = ({me, isUsingAuth}) => {
if (!isUsingAuth) {
@@ -39,6 +39,10 @@ const {arrayOf, bool, shape, string} = PropTypes
RoleIndicator.propTypes = {
isUsingAuth: bool.isRequired,
me: shape({
+ currentOrganization: shape({
+ name: string.isRequired,
+ id: string.isRequired,
+ }),
roles: arrayOf(
shape({
name: string.isRequired,
diff --git a/ui/src/shared/components/SlideToggle.js b/ui/src/shared/components/SlideToggle.js
new file mode 100644
index 0000000000..f917ec7f88
--- /dev/null
+++ b/ui/src/shared/components/SlideToggle.js
@@ -0,0 +1,47 @@
+import React, {Component, PropTypes} from 'react'
+
+class SlideToggle extends Component {
+ constructor(props) {
+ super(props)
+
+ this.state = {
+ active: this.props.active,
+ }
+ }
+
+ handleClick = () => {
+ const {onToggle} = this.props
+
+ this.setState({active: !this.state.active}, () => {
+ onToggle(this.state.active)
+ })
+ }
+
+ render() {
+ const {size} = this.props
+ const {active} = this.state
+
+ const classNames = active
+ ? `slide-toggle slide-toggle__${size} active`
+ : `slide-toggle slide-toggle__${size}`
+
+ return (
+
+ )
+ }
+}
+
+const {bool, func, string} = PropTypes
+
+SlideToggle.defaultProps = {
+ size: 'sm',
+}
+SlideToggle.propTypes = {
+ active: bool,
+ size: string,
+ onToggle: func.isRequired,
+}
+
+export default SlideToggle
diff --git a/ui/src/shared/reducers/auth.js b/ui/src/shared/reducers/auth.js
index 09f406da61..cea6c1c340 100644
--- a/ui/src/shared/reducers/auth.js
+++ b/ui/src/shared/reducers/auth.js
@@ -6,6 +6,10 @@ const getInitialState = () => ({
logoutLink: null,
})
+import {getMeRole} from 'shared/reducers/helpers/auth'
+
+import {DEFAULT_ORG_NAME} from 'src/admin/constants/dummyUsers'
+
export const initialState = getInitialState()
const authReducer = (state = initialState, action) => {
@@ -24,13 +28,30 @@ const authReducer = (state = initialState, action) => {
case 'ME_REQUESTED': {
return {...state, isMeLoading: true}
}
- case 'ME_RECEIVED': {
+ case 'ME_RECEIVED__NON_AUTH': {
const {me} = action.payload
- return {...state, me, isMeLoading: false}
+ return {
+ ...state,
+ me: {...me},
+ isMeLoading: false,
+ }
+ }
+ case 'ME_RECEIVED__AUTH': {
+ const {me, me: {currentOrganization}} = action.payload
+ return {
+ ...state,
+ me: {
+ ...me,
+ role: getMeRole(me),
+ currentOrganization: currentOrganization || DEFAULT_ORG_NAME, // TODO: make sure currentOrganization is received as non-superadmin
+ },
+ isMeLoading: false,
+ }
}
case 'LOGOUT_LINK_RECEIVED': {
const {logoutLink} = action.payload
- return {...state, logoutLink, isUsingAuth: !!logoutLink}
+ const isUsingAuth = !!logoutLink
+ return {...state, logoutLink, isUsingAuth}
}
}
diff --git a/ui/src/shared/reducers/helpers/auth.js b/ui/src/shared/reducers/helpers/auth.js
new file mode 100644
index 0000000000..e56ddf8308
--- /dev/null
+++ b/ui/src/shared/reducers/helpers/auth.js
@@ -0,0 +1,20 @@
+import _ from 'lodash'
+
+import {SUPERADMIN_ROLE, MEMBER_ROLE} from 'src/auth/Authorized'
+
+export const getMeRole = me => {
+ const currentRoleOrg = me.roles.find(
+ role => me.currentOrganization.id === role.organization
+ )
+ const currentRole = _.get(currentRoleOrg, 'name', MEMBER_ROLE)
+
+ return me.superAdmin ? SUPERADMIN_ROLE : currentRole
+}
+
+export const isSameUser = (userA, userB) => {
+ return (
+ userA.name === userB.name &&
+ userA.provider === userB.provider &&
+ userA.scheme === userB.scheme
+ )
+}
diff --git a/ui/src/side_nav/components/UserNavBlock.js b/ui/src/side_nav/components/UserNavBlock.js
new file mode 100644
index 0000000000..15c2472e41
--- /dev/null
+++ b/ui/src/side_nav/components/UserNavBlock.js
@@ -0,0 +1,126 @@
+import React, {PropTypes, Component} from 'react'
+import {connect} from 'react-redux'
+import {bindActionCreators} from 'redux'
+
+import classnames from 'classnames'
+
+import {meChangeOrganizationAsync} from 'shared/actions/auth'
+
+class UserNavBlock extends Component {
+ handleChangeCurrentOrganization = organizationID => () => {
+ const {links, meChangeOrganization} = this.props
+ meChangeOrganization(links.me, {organization: organizationID})
+ }
+
+ render() {
+ const {
+ logoutLink,
+ links: {external: {custom: customLinks}},
+ me,
+ me: {currentOrganization, organizations, roles},
+ } = this.props
+
+ // TODO: find a better way to glean this information.
+ // Need this method for when a user is a superadmin,
+ // which doesn't reflect their role in the current org
+ const currentRole = roles.find(cr => {
+ return cr.organization === currentOrganization.id
+ }).name
+
+ return (
+
+
+
+
+ {currentOrganization.name} ({currentRole})
+
+
+ {me.name}
+
+
+ Logout
+
+
Switch Organizations
+ {roles.map((r, i) => {
+ const isLinkCurrentOrg = currentOrganization.id === r.organization
+ return (
+
+ {organizations.find(o => o.id === r.organization).name}{' '}
+ ({r.name})
+
+ )
+ })}
+ {customLinks
+ ?
Custom Links
+ : null}
+ {customLinks
+ ? customLinks.map((link, i) =>
+
+ {link.name}
+
+ )
+ : null}
+
+
+
+ )
+ }
+}
+
+const {arrayOf, func, shape, string} = PropTypes
+
+UserNavBlock.propTypes = {
+ links: shape({
+ me: string,
+ external: shape({
+ custom: arrayOf(
+ shape({
+ name: string.isRequired,
+ url: string.isRequired,
+ })
+ ),
+ }),
+ }),
+ logoutLink: string.isRequired,
+ me: shape({
+ currentOrganization: shape({
+ id: string.isRequired,
+ name: string.isRequired,
+ }),
+ name: string,
+ organizations: arrayOf(
+ shape({
+ id: string.isRequired,
+ name: string.isRequired,
+ })
+ ),
+ roles: arrayOf(
+ shape({
+ id: string,
+ name: string,
+ })
+ ),
+ role: string,
+ }).isRequired,
+ meChangeOrganization: func.isRequired,
+}
+
+const mapDispatchToProps = dispatch => ({
+ meChangeOrganization: bindActionCreators(meChangeOrganizationAsync, dispatch),
+})
+
+export default connect(null, mapDispatchToProps)(UserNavBlock)
diff --git a/ui/src/side_nav/containers/SideNav.js b/ui/src/side_nav/containers/SideNav.js
index 141b7707ce..6a5bfa083f 100644
--- a/ui/src/side_nav/containers/SideNav.js
+++ b/ui/src/side_nav/containers/SideNav.js
@@ -4,6 +4,7 @@ import {connect} from 'react-redux'
import Authorized, {ADMIN_ROLE} from 'src/auth/Authorized'
+import UserNavBlock from 'src/side_nav/components/UserNavBlock'
import {
NavBar,
NavBlock,
@@ -26,37 +27,36 @@ const SideNav = React.createClass({
isHidden: bool.isRequired,
isUsingAuth: bool,
logoutLink: string,
- customLinks: arrayOf(
- shape({
- name: string.isRequired,
- url: string.isRequired,
- })
- ),
- },
-
- renderUserMenuBlockWithCustomLinks(customLinks, logoutLink) {
- return [
-
,
- ...customLinks
- .sort((a, b) => a.name.toLowerCase() > b.name.toLowerCase())
- .map(({name, url}, i) =>
-
- {name}
-
+ links: shape({
+ me: string,
+ external: shape({
+ custom: arrayOf(
+ shape({
+ name: string.isRequired,
+ url: string.isRequired,
+ })
),
-
- Logout
- ,
- ]
+ }),
+ }),
+ me: shape({
+ currentOrganization: shape({
+ id: string.isRequired,
+ name: string.isRequired,
+ }),
+ name: string,
+ organizations: arrayOf(
+ shape({
+ id: string.isRequired,
+ name: string.isRequired,
+ })
+ ),
+ roles: arrayOf(
+ shape({
+ id: string,
+ name: string,
+ })
+ ),
+ }).isRequired,
},
render() {
@@ -66,7 +66,8 @@ const SideNav = React.createClass({
isHidden,
isUsingAuth,
logoutLink,
- customLinks,
+ links,
+ me,
} = this.props
const sourcePrefix = `/sources/${sourceID}`
@@ -123,13 +124,37 @@ const SideNav = React.createClass({
Create
-
+
+
+
+
+ }
+ >
-
+
+
+ Chronograf
+
+
+ InfluxDB
+
{isUsingAuth
- ?
- {customLinks
- ? this.renderUserMenuBlockWithCustomLinks(
- customLinks,
- logoutLink
- )
- : }
-
+ ?
: null}
},
})
-
const mapStateToProps = ({
- auth: {isUsingAuth, logoutLink},
+ auth: {isUsingAuth, logoutLink, me},
app: {ephemeral: {inPresentationMode}},
- links: {external: {custom: customLinks}},
+ links,
}) => ({
isHidden: inPresentationMode,
isUsingAuth,
logoutLink,
- customLinks,
+ links,
+ me,
})
export default connect(mapStateToProps)(withRouter(SideNav))
diff --git a/ui/src/sources/components/InfluxTable.js b/ui/src/sources/components/InfluxTable.js
index 91898debd2..7a0a1acdd7 100644
--- a/ui/src/sources/components/InfluxTable.js
+++ b/ui/src/sources/components/InfluxTable.js
@@ -144,7 +144,7 @@ const InfluxTable = ({
{s.name}
diff --git a/ui/src/store/configureStore.js b/ui/src/store/configureStore.js
index 3fad50db3f..9f7a3e6950 100644
--- a/ui/src/store/configureStore.js
+++ b/ui/src/store/configureStore.js
@@ -9,7 +9,7 @@ import {queryStringConfig} from 'shared/middleware/queryStringConfig'
import statusReducers from 'src/status/reducers'
import sharedReducers from 'shared/reducers'
import dataExplorerReducers from 'src/data_explorer/reducers'
-import adminReducer from 'src/admin/reducers/admin'
+import adminReducers from 'src/admin/reducers'
import kapacitorReducers from 'src/kapacitor/reducers'
import dashboardUI from 'src/dashboards/reducers/ui'
import dashTimeV1 from 'src/dashboards/reducers/dashTimeV1'
@@ -20,7 +20,7 @@ const rootReducer = combineReducers({
...sharedReducers,
...dataExplorerReducers,
...kapacitorReducers,
- admin: adminReducer,
+ ...adminReducers,
dashboardUI,
dashTimeV1,
routing: routerReducer,
diff --git a/ui/src/style/chronograf.scss b/ui/src/style/chronograf.scss
index 79fbcc3e9d..90a73201ee 100644
--- a/ui/src/style/chronograf.scss
+++ b/ui/src/style/chronograf.scss
@@ -41,6 +41,7 @@
@import 'components/input-tag-list';
@import 'components/newsfeed';
@import 'components/opt-in';
+@import 'components/organizations-table';
@import 'components/page-header-dropdown';
@import 'components/page-header-editable';
@import 'components/page-spinner';
@@ -49,6 +50,7 @@
@import 'components/redacted-input';
@import 'components/resizer';
@import 'components/search-widget';
+@import 'components/slide-toggle';
@import 'components/info-indicators';
@import 'components/source-selector';
@import 'components/tables';
@@ -60,6 +62,7 @@
@import 'pages/kapacitor';
@import 'pages/dashboards';
@import 'pages/admin';
+@import 'pages/users';
// TODO
@import 'unsorted';
diff --git a/ui/src/style/components/organizations-table.scss b/ui/src/style/components/organizations-table.scss
new file mode 100644
index 0000000000..78942d9398
--- /dev/null
+++ b/ui/src/style/components/organizations-table.scss
@@ -0,0 +1,134 @@
+/*
+ Styles for the Manage Organizations Page
+ ------------------------------------------------------------------------------
+ Is not actually a table
+*/
+
+.orgs-table--org {
+ width: 100%;
+ display: flex;
+ align-items: center;
+ margin-bottom: 8px;
+ position: relative;
+
+ &:last-of-type {
+ margin-bottom: 0;
+ }
+}
+.orgs-table--id {
+ padding: 0 11px;
+ width: 60px;
+ height: 30px;
+ line-height: 30px;
+ font-size: 13px;
+ color: $g13-mist;
+ font-weight: 500;
+}
+.orgs-table--name,
+.orgs-table--name-disabled,
+input[type="text"].form-control.orgs-table--input {
+ flex: 1 0 0;
+ font-weight: 600;
+ font-size: 13px;
+ margin-right: 4px;
+}
+.orgs-table--name,
+.orgs-table--name-disabled {
+ @include no-user-select();
+ padding: 0 11px;
+ border-radius: 4px;
+ height: 30px;
+ line-height: 28px;
+ border-style: solid;
+ border-width: 2px;
+}
+.orgs-table--name {
+ border-color: $g2-kevlar;
+ background-color: $g2-kevlar;
+ color: $g13-mist;
+ position: relative;
+ transition:
+ color 0.4s ease,
+ background-color 0.4s ease,
+ border-color 0.4s ease;
+
+ > span.icon {
+ position: absolute;
+ top: 50%;
+ right: 11px;
+ transform: translateY(-50%);
+ color: $g8-storm;
+ opacity: 0;
+ transition: opacity 0.25s ease;
+ }
+
+ &:hover {
+ color: $g20-white;
+ background-color: $g5-pepper;
+ border-color: $g5-pepper;
+ cursor: text;
+
+ > span.icon {opacity: 1;}
+ }
+}
+.orgs-table--name-disabled {
+ border-color: $g4-onyx;
+ background-color: $g4-onyx;
+ font-style: italic;
+ color: $g9-mountain;
+}
+
+.orgs-table--default-role,
+.orgs-table--default-role-disabled {
+ width: 130px;
+ height: 30px;
+ margin-right: 4px;
+}
+.orgs-table--default-role.editing {
+ width: 96px;
+}
+.orgs-table--default-role-disabled {
+ background-color: $g4-onyx;
+ font-style: italic;
+ color: $g9-mountain;
+ padding: 0 11px;
+ line-height: 30px;
+ font-size: 13px;
+ font-weight: 600;
+ @include no-user-select();
+}
+.orgs-table--delete {
+ height: 30px;
+ width: 30px;
+}
+
+
+/* Table Headers */
+.orgs-table--org-labels {
+ display: flex;
+ align-items: center;
+ border-bottom: 2px solid $g5-pepper;
+ margin-bottom: 10px;
+ width: 100%;
+ @include no-user-select();
+
+ > .orgs-table--name,
+ > .orgs-table--name:hover {
+ transition: none;
+ background-color: transparent;
+ border-color: transparent;
+ }
+
+ > .orgs-table--id,
+ > .orgs-table--name,
+ > .orgs-table--name:hover,
+ > .orgs-table--default-role {
+ color: $g15-platinum;
+ font-weight: 700;
+ }
+ > .orgs-table--default-role {
+ line-height: 30px;
+ font-size: 13px;
+ padding: 0 11px;
+ }
+}
diff --git a/ui/src/style/components/slide-toggle.scss b/ui/src/style/components/slide-toggle.scss
new file mode 100644
index 0000000000..09f9b9c533
--- /dev/null
+++ b/ui/src/style/components/slide-toggle.scss
@@ -0,0 +1,63 @@
+/*
+ Slide Toggle Component
+ ------------------------------------------------------------------------------
+*/
+.slide-toggle {
+ background-color: $g1-raven;
+ position: relative;
+ padding: 0 4px;
+ display: inline-block;
+ transition: background-color 0.25s ease;
+
+ &:hover {
+ cursor: pointer;
+ background-color: $g2-kevlar;
+ }
+}
+.slide-toggle--knob {
+ position: absolute;
+ top: 50%;
+ transition:
+ background-color 0.25s ease,
+ transform 0.25s cubic-bezier(0.175, 0.885, 0.32, 1.275);
+ background-color: $g4-onyx;
+ transform: translate(0,-50%);
+ border-radius: 50%;
+
+ .slide-toggle:hover & {
+ background-color: $g6-smoke;
+ }
+}
+
+.slide-toggle.active .slide-toggle--knob {
+ background-color: $c-rainforest;
+ transform: translate(100%,-50%);
+}
+.slide-toggle.active:hover .slide-toggle--knob {
+ background-color: $c-honeydew;
+}
+
+/* Size Modifiers */
+
+.slide-toggle {
+ /* Extra Small */
+ &.slide-toggle__xs {
+ .slide-toggle--knob {
+ width: 16px;
+ height: 16px;
+ }
+ height: 22px;
+ border-radius: 11px;
+ width: 40px;
+ }
+ /* Extra Small */
+ &.slide-toggle__sm {
+ .slide-toggle--knob {
+ width: 22px;
+ height: 22px;
+ }
+ height: 30px;
+ border-radius: 15px;
+ width: 52px;
+ }
+}
diff --git a/ui/src/style/components/tables.scss b/ui/src/style/components/tables.scss
index 9425cce4f8..8f6e91161c 100644
--- a/ui/src/style/components/tables.scss
+++ b/ui/src/style/components/tables.scss
@@ -97,10 +97,14 @@ table.table thead th.sortable-header,
Empty State for Tables
----------------------------------------------
*/
-.table-empty-state {
+tr.table-empty-state,
+.table-highlight tr.table-empty-state:hover {
+ background-color: transparent;
+
> th {
text-align: center;
-
+ @include no-user-select();
+
> p {
font-weight: 400;
font-size: 18px;
diff --git a/ui/src/style/layout/sidebar.scss b/ui/src/style/layout/sidebar.scss
index 5ff2503fa5..c7c4fd2fe4 100644
--- a/ui/src/style/layout/sidebar.scss
+++ b/ui/src/style/layout/sidebar.scss
@@ -151,6 +151,7 @@ $sidebar-menu--gutter: 18px;
cursor: pointer;
}
}
+.sidebar-menu--item,
.sidebar-menu--item:link,
.sidebar-menu--item:active,
.sidebar-menu--item:visited {
@@ -163,13 +164,15 @@ $sidebar-menu--gutter: 18px;
transition: none;
// Rounding bottom outside corner of match container
- &:last-child {border-bottom-right-radius: $radius;}
+ &:nth-last-child(2) {border-bottom-right-radius: $radius;}
}
+.sidebar-menu--item.active,
.sidebar-menu--item.active:link,
.sidebar-menu--item.active:active,
.sidebar-menu--item.active:visited {
@include gradient-h($sidebar-menu--item-bg,$sidebar-menu--item-bg-accent);
color: $sidebar-menu--item-text-active;
+ font-weight: 700;
}
.sidebar-menu--item:hover,
.sidebar-menu--item.active:hover {
@@ -188,6 +191,9 @@ $sidebar-menu--gutter: 18px;
font-weight: 400;
padding: 0px $sidebar-menu--gutter;
}
+.sidebar-menu--item > strong {
+ opacity: 0.6;
+}
// Invisible triangle for easier mouse movement when navigating to sub items
.sidebar-menu--item + .sidebar-menu--triangle {
position: absolute;
@@ -198,3 +204,25 @@ $sidebar-menu--gutter: 18px;
left: 0px;
transform: translate(-50%,-50%) rotate(45deg);
}
+
+.sidebar-menu--section {
+ white-space: nowrap;
+ font-size: 13px;
+ line-height: 22px;
+ font-weight: 600;
+ padding: 4px $sidebar-menu--gutter;
+ text-transform: uppercase;
+ color: $c-hydrogen;
+ @include no-user-select();
+ position: relative;
+
+ &:after {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 2px;
+ @include gradient-h($c-laser,$c-potassium);
+ }
+}
diff --git a/ui/src/style/pages/admin.scss b/ui/src/style/pages/admin.scss
index 715dba70eb..a46587fbe9 100644
--- a/ui/src/style/pages/admin.scss
+++ b/ui/src/style/pages/admin.scss
@@ -1,5 +1,5 @@
/*
- Styles for Admin Pages
+ Styles for InfluxDB Admin Page
----------------------------------------------------------------------------
*/
diff --git a/ui/src/style/pages/auth-page.scss b/ui/src/style/pages/auth-page.scss
index 75df36ae6c..e34d97f537 100644
--- a/ui/src/style/pages/auth-page.scss
+++ b/ui/src/style/pages/auth-page.scss
@@ -38,12 +38,14 @@
h1 {
color: $g20-white;
+ @include no-user-select();
font-weight: 200;
font-size: 52px;
letter-spacing: -2px;
}
p {
color: $g11-sidewalk;
+ @include no-user-select();
}
.btn {
@@ -65,12 +67,14 @@
height: 100px;
}
.auth-credits {
+ @include no-user-select();
+ font-weight: 600;
z-index: 90;
position: absolute;
bottom: ($sidebar--width / 4);
left: 50%;
transform: translateX(-50%);
- font-size: 12px;
+ font-size: 13px;
color: $g11-sidewalk;
.icon {
@@ -78,6 +82,31 @@
vertical-align: middle;
position: relative;
top: -1px;
- margin-right: 1px;
+ margin-right: 2px;
+ }
+}
+
+/* Purgatory */
+.auth--purgatory {
+ margin-top: 30px;
+ min-width: 400px;
+ background-color: $g3-castle;
+ border-radius: 4px;
+ min-height: 200px;
+ padding: 30px;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+
+ > h3 {
+ white-space: nowrap;
+ }
+ > p {
+ text-align: center;
+ }
+
+ hr {
+ width: 100%;
}
}
diff --git a/ui/src/style/pages/users.scss b/ui/src/style/pages/users.scss
new file mode 100644
index 0000000000..e5b5855933
--- /dev/null
+++ b/ui/src/style/pages/users.scss
@@ -0,0 +1,112 @@
+/*
+ Styles for Chronograf Users Admin Page
+ ----------------------------------------------------------------------------
+*/
+
+.chronograf-user--role,
+.chronograf-user--org {
+ display: inline-block;
+ width: 100%;
+ height: 22px;
+ line-height: 22px;
+}
+.chronograf-user--role,
+.chronograf-user--org,
+table.table.chronograf-admin-table .dropdown {
+ margin-bottom: 2px;
+
+ &:last-child {
+ margin: 0;
+ }
+}
+.chronograf-user--org {
+ padding-left: 7px;
+}
+table.table.chronograf-admin-table thead tr th.align-with-col-text {
+ padding-left: 15px;
+}
+.dropdown-label {
+ margin: 0 8px 0 0;
+ font-weight: 700;
+ color: $g13-mist;
+ font-size: 14px;
+}
+.panel-body.chronograf-admin-table--panel {
+ border-top-left-radius: 0;
+ border-top-right-radius: 0;
+ padding-top: 11px;
+}
+.chronograf-admin-table--batch {
+ border-radius: 5px 5px 0 0;
+ background-color: $g4-onyx;
+ padding: 11px 38px;
+ display: flex;
+ align-items: center;
+}
+.chronograf-admin-table--batch-actions {
+ display: flex;
+ align-items: center;
+
+ > .dropdown,
+ > .btn {
+ margin-left: 4px;
+ }
+}
+.chronograf-admin-table--num-selected {
+ @include no-user-select();
+ margin: 0px 11px 0 0;
+ display: inline-block;
+ height: 30px;
+ line-height: 30px;
+ font-size: 14px;
+ font-weight: 500;
+ color: $g13-mist;
+}
+.super-admin-toggle .dropdown-toggle {
+ width: 70px;
+}
+
+/* Make dropdowns in admin table appear as plaintext until hover */
+table.table.chronograf-admin-table tbody tr.chronograf-admin-table--user td div.dropdown div.btn.dropdown-toggle {
+ transition: none;
+ background-color: $g3-castle;
+ color: $g13-mist;
+
+ > .caret {
+ opacity: 0;
+ }
+}
+table.table.chronograf-admin-table tbody tr.chronograf-admin-table--user.selected td div.dropdown div.btn.dropdown-toggle {
+ background-color: $g5-pepper;
+}
+table.table.chronograf-admin-table tbody tr.chronograf-admin-table--user:hover td div.dropdown div.btn.dropdown-toggle {
+ background-color: $c-pool;
+ color: $g20-white;
+
+ > .caret {
+ opacity: 1;
+ }
+
+ &:hover {
+ background-color: $c-laser;
+ }
+}
+table.table.chronograf-admin-table tbody tr.chronograf-admin-table--user td div.dropdown.open div.btn.dropdown-toggle,
+table.table.chronograf-admin-table tbody tr.chronograf-admin-table--user td div.dropdown.open div.btn.dropdown-toggle:hover {
+ background-color: $c-hydrogen;
+ color: $g20-white;
+
+ > .caret {
+ opacity: 1;
+ }
+}
+
+/* Styles for new user row */
+table.table.chronograf-admin-table tbody tr.chronograf-admin-table--new-user {
+ background-color: $g4-onyx;
+
+ > td {
+ padding-top: 8px;
+ padding-bottom: 8px;
+ }
+}
diff --git a/ui/src/style/unsorted.scss b/ui/src/style/unsorted.scss
index 53917ca210..3d4084420b 100644
--- a/ui/src/style/unsorted.scss
+++ b/ui/src/style/unsorted.scss
@@ -418,5 +418,15 @@ $dash-editable-header-padding: 7px;
}
}
}
-
+}
+
+/*
+ Stretch to fit Dropdowns
+ -----------------------------------------------------------------------------
+*/
+
+div.dropdown.dropdown-stretch,
+div.dropdown.dropdown-stretch > div.dropdown-toggle,
+div.dropdown.dropdown-stretch > button.dropdown-toggle {
+ width: 100%;
}
diff --git a/ui/src/utils/ajax.js b/ui/src/utils/ajax.js
index 99c44ab761..69ee8bf04d 100644
--- a/ui/src/utils/ajax.js
+++ b/ui/src/utils/ajax.js
@@ -9,12 +9,18 @@ const addBasepath = (url, excludeBasepath) => {
return excludeBasepath ? url : `${basepath}${url}`
}
-const generateResponseWithLinks = (response, {auth, logout, external}) => ({
- ...response,
- auth: {links: auth},
- logoutLink: logout,
- external,
-})
+const generateResponseWithLinks = (response, newLinks) => {
+ const {auth, logout, external, users, organizations, me: meLink} = newLinks
+ return {
+ ...response,
+ auth: {links: auth},
+ logoutLink: logout,
+ external,
+ users,
+ organizations,
+ meLink,
+ }
+}
const AJAX = async (
{url, resource, id, method = 'GET', data = {}, params = {}, headers = {}},