Merge pull request #2180 from influxdata/multitenancy_ui_superadmin_admin_panel
Implement Admin & Superadmin UI with Org switchingpull/10616/head
commit
e26e80ca87
|
@ -0,0 +1,141 @@
|
|||
import reducer from 'src/admin/reducers/chronograf'
|
||||
|
||||
import {loadUsers} from 'src/admin/actions/chronograf'
|
||||
|
||||
import {
|
||||
MEMBER_ROLE,
|
||||
VIEWER_ROLE,
|
||||
EDITOR_ROLE,
|
||||
ADMIN_ROLE,
|
||||
} from 'src/auth/Authorized'
|
||||
|
||||
let state
|
||||
|
||||
const users = [
|
||||
{
|
||||
id: '666',
|
||||
name: 'bob@billietta.com',
|
||||
roles: [
|
||||
{
|
||||
name: 'admin',
|
||||
organization: '0',
|
||||
},
|
||||
{
|
||||
name: 'member',
|
||||
organization: '667',
|
||||
},
|
||||
],
|
||||
provider: 'github',
|
||||
scheme: 'oauth2',
|
||||
superAdmin: true,
|
||||
links: {
|
||||
self: '/chronograf/v1/users/666',
|
||||
},
|
||||
organizations: [
|
||||
{
|
||||
id: '0',
|
||||
name: 'Default',
|
||||
},
|
||||
{
|
||||
id: '667',
|
||||
name: 'Engineering',
|
||||
defaultRole: 'member',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: '831',
|
||||
name: 'billybob@gmail.com',
|
||||
roles: [
|
||||
{
|
||||
name: 'member',
|
||||
organization: '0',
|
||||
},
|
||||
{
|
||||
name: 'viewer',
|
||||
organization: '667',
|
||||
},
|
||||
{
|
||||
name: 'editor',
|
||||
organization: '1236',
|
||||
},
|
||||
],
|
||||
provider: 'github',
|
||||
scheme: 'oauth2',
|
||||
superAdmin: false,
|
||||
links: {
|
||||
self: '/chronograf/v1/users/831',
|
||||
},
|
||||
organizations: [
|
||||
{
|
||||
id: '0',
|
||||
name: 'Default',
|
||||
},
|
||||
{
|
||||
id: '667',
|
||||
name: 'Engineering',
|
||||
defaultRole: 'member',
|
||||
},
|
||||
{
|
||||
id: '1236',
|
||||
name: 'PsyOps',
|
||||
defaultRole: 'editor',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: '720',
|
||||
name: 'shorty@gmail.com',
|
||||
roles: [
|
||||
{
|
||||
name: 'admin',
|
||||
organization: '667',
|
||||
},
|
||||
{
|
||||
name: 'viewer',
|
||||
organization: '1236',
|
||||
},
|
||||
],
|
||||
provider: 'github',
|
||||
scheme: 'oauth2',
|
||||
superAdmin: false,
|
||||
links: {
|
||||
self: '/chronograf/v1/users/720',
|
||||
},
|
||||
organizations: [
|
||||
{
|
||||
id: '667',
|
||||
name: 'Engineering',
|
||||
defaultRole: 'member',
|
||||
},
|
||||
{
|
||||
id: '1236',
|
||||
name: 'PsyOps',
|
||||
defaultRole: 'editor',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: '271',
|
||||
name: 'shawn.ofthe.dead@altavista.yop',
|
||||
roles: [],
|
||||
provider: 'github',
|
||||
scheme: 'oauth2',
|
||||
superAdmin: false,
|
||||
links: {
|
||||
self: '/chronograf/v1/users/271',
|
||||
},
|
||||
organizations: [],
|
||||
},
|
||||
]
|
||||
|
||||
describe('Admin.Chronograf.Reducers', () => {
|
||||
it('it can load all users', () => {
|
||||
const actual = reducer(state, loadUsers({users}))
|
||||
const expected = {
|
||||
users,
|
||||
}
|
||||
|
||||
expect(actual.users).to.deep.equal(expected.users)
|
||||
})
|
||||
})
|
|
@ -1,4 +1,4 @@
|
|||
import reducer from 'src/admin/reducers/admin'
|
||||
import reducer from 'src/admin/reducers/influxdb'
|
||||
|
||||
import {
|
||||
addUser,
|
||||
|
@ -21,7 +21,7 @@ import {
|
|||
filterUsers,
|
||||
addDatabaseDeleteCode,
|
||||
removeDatabaseDeleteCode,
|
||||
} from 'src/admin/actions'
|
||||
} from 'src/admin/actions/influxdb'
|
||||
|
||||
import {
|
||||
NEW_DEFAULT_USER,
|
||||
|
@ -138,7 +138,7 @@ const db2 = {
|
|||
deleteCode: 'DELETE',
|
||||
}
|
||||
|
||||
describe('Admin.Reducers', () => {
|
||||
describe('Admin.InfluxDB.Reducers', () => {
|
||||
describe('Databases', () => {
|
||||
const state = {databases: [db1, db2]}
|
||||
|
|
@ -5,7 +5,7 @@ import {
|
|||
authRequested,
|
||||
authReceived,
|
||||
meRequested,
|
||||
meReceived,
|
||||
meReceivedNotUsingAuth,
|
||||
} from 'shared/actions/auth'
|
||||
|
||||
const defaultAuth = {
|
||||
|
@ -22,7 +22,6 @@ const defaultAuth = {
|
|||
|
||||
const defaultMe = {
|
||||
name: 'wishful_modal@overlay.technology',
|
||||
password: '',
|
||||
links: {
|
||||
self: '/chronograf/v1/users/wishful_modal@overlay.technology',
|
||||
},
|
||||
|
@ -58,9 +57,12 @@ describe('Shared.Reducers.authReducer', () => {
|
|||
expect(reducedState.isMeLoading).to.equal(true)
|
||||
})
|
||||
|
||||
it('should handle ME_RECEIVED', () => {
|
||||
const loadingState = Object.assign({}, initialState, {isMeLoading: true})
|
||||
const reducedState = authReducer(loadingState, meReceived(defaultMe))
|
||||
it('should handle ME_RECEIVED__NON_AUTH', () => {
|
||||
const loadingState = {...initialState, isMeLoading: true}
|
||||
const reducedState = authReducer(
|
||||
loadingState,
|
||||
meReceivedNotUsingAuth(defaultMe)
|
||||
)
|
||||
|
||||
expect(reducedState.me).to.deep.equal(defaultMe)
|
||||
expect(reducedState.isAuthLoading).to.equal(false)
|
||||
|
|
|
@ -3,6 +3,8 @@ import {withRouter} from 'react-router'
|
|||
import {connect} from 'react-redux'
|
||||
import {bindActionCreators} from 'redux'
|
||||
|
||||
import {MEMBER_ROLE, VIEWER_ROLE} from 'src/auth/Authorized'
|
||||
|
||||
import {getSources} from 'shared/apis'
|
||||
import {showDatabases} from 'shared/apis/metaQuery'
|
||||
|
||||
|
@ -14,7 +16,7 @@ import {DEFAULT_HOME_PAGE} from 'shared/constants'
|
|||
// Acts as a 'router middleware'. The main `App` component is responsible for
|
||||
// getting the list of data nodes, but not every page requires them to function.
|
||||
// Routes that do require data nodes can be nested under this component.
|
||||
const {arrayOf, func, node, shape, string} = PropTypes
|
||||
const {arrayOf, bool, func, node, shape, string} = PropTypes
|
||||
const CheckSources = React.createClass({
|
||||
propTypes: {
|
||||
sources: arrayOf(
|
||||
|
@ -42,6 +44,15 @@ const CheckSources = React.createClass({
|
|||
}).isRequired,
|
||||
loadSources: func.isRequired,
|
||||
errorThrown: func.isRequired,
|
||||
auth: shape({
|
||||
isUsingAuth: bool,
|
||||
me: shape({
|
||||
currentOrganization: shape({
|
||||
name: string.isRequired,
|
||||
id: string.isRequired,
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
},
|
||||
|
||||
childContextTypes: {
|
||||
|
@ -83,7 +94,14 @@ const CheckSources = React.createClass({
|
|||
},
|
||||
|
||||
async componentWillUpdate(nextProps, nextState) {
|
||||
const {router, location, params, errorThrown, sources} = nextProps
|
||||
const {
|
||||
router,
|
||||
location,
|
||||
params,
|
||||
errorThrown,
|
||||
sources,
|
||||
auth: {isUsingAuth, me},
|
||||
} = nextProps
|
||||
const {isFetching} = nextState
|
||||
const source = sources.find(s => s.id === params.sourceID)
|
||||
const defaultSource = sources.find(s => s.default === true)
|
||||
|
@ -92,6 +110,23 @@ const CheckSources = React.createClass({
|
|||
const rest = location.pathname.match(/\/sources\/\d+?\/(.+)/)
|
||||
const restString = rest === null ? DEFAULT_HOME_PAGE : rest[1]
|
||||
|
||||
if (isUsingAuth && me.role === MEMBER_ROLE) {
|
||||
// if you're a member, go to purgatory.
|
||||
return router.push('/purgatory')
|
||||
}
|
||||
|
||||
if (isUsingAuth && me.role === VIEWER_ROLE) {
|
||||
if (defaultSource) {
|
||||
return router.push(`/sources/${defaultSource.id}/${restString}`)
|
||||
} else if (sources[0]) {
|
||||
return router.push(`/sources/${sources[0].id}/${restString}`)
|
||||
}
|
||||
// if you're a viewer and there are no sources, go to purgatory.
|
||||
return router.push('/purgatory')
|
||||
}
|
||||
|
||||
// if you're an editor or not using auth, try for sources or otherwise
|
||||
// create one
|
||||
if (defaultSource) {
|
||||
return router.push(`/sources/${defaultSource.id}/${restString}`)
|
||||
} else if (sources[0]) {
|
||||
|
@ -112,11 +147,21 @@ const CheckSources = React.createClass({
|
|||
},
|
||||
|
||||
render() {
|
||||
const {params, sources} = this.props
|
||||
const {
|
||||
params,
|
||||
sources,
|
||||
auth: {isUsingAuth, me, me: {currentOrganization}},
|
||||
} = this.props
|
||||
const {isFetching} = this.state
|
||||
const source = sources.find(s => s.id === params.sourceID)
|
||||
|
||||
if (isFetching || !source) {
|
||||
if (
|
||||
isFetching ||
|
||||
!source ||
|
||||
typeof isUsingAuth !== 'boolean' ||
|
||||
(me && me.role === undefined) || // TODO: not sure this happens
|
||||
!currentOrganization
|
||||
) {
|
||||
return <div className="page-spinner" />
|
||||
}
|
||||
|
||||
|
@ -132,8 +177,10 @@ const CheckSources = React.createClass({
|
|||
},
|
||||
})
|
||||
|
||||
const mapStateToProps = ({sources}) => ({
|
||||
const mapStateToProps = ({sources, auth, me}) => ({
|
||||
sources,
|
||||
auth,
|
||||
me,
|
||||
})
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
|
|
|
@ -0,0 +1,211 @@
|
|||
import {
|
||||
getUsers as getUsersAJAX,
|
||||
getOrganizations as getOrganizationsAJAX,
|
||||
createUser as createUserAJAX,
|
||||
updateUser as updateUserAJAX,
|
||||
deleteUser as deleteUserAJAX,
|
||||
createOrganization as createOrganizationAJAX,
|
||||
updateOrganization as updateOrganizationAJAX,
|
||||
deleteOrganization as deleteOrganizationAJAX,
|
||||
} from 'src/admin/apis/chronograf'
|
||||
|
||||
import {publishAutoDismissingNotification} from 'shared/dispatchers'
|
||||
import {errorThrown} from 'shared/actions/errors'
|
||||
|
||||
// action creators
|
||||
|
||||
// response contains `users` and `links`
|
||||
export const loadUsers = ({users}) => ({
|
||||
type: 'CHRONOGRAF_LOAD_USERS',
|
||||
payload: {
|
||||
users,
|
||||
},
|
||||
})
|
||||
|
||||
export const loadOrganizations = ({organizations}) => ({
|
||||
type: 'CHRONOGRAF_LOAD_ORGANIZATIONS',
|
||||
payload: {
|
||||
organizations,
|
||||
},
|
||||
})
|
||||
|
||||
export const addUser = user => ({
|
||||
type: 'CHRONOGRAF_ADD_USER',
|
||||
payload: {
|
||||
user,
|
||||
},
|
||||
})
|
||||
|
||||
export const updateUser = (user, updatedUser) => ({
|
||||
type: 'CHRONOGRAF_UPDATE_USER',
|
||||
payload: {
|
||||
user,
|
||||
updatedUser,
|
||||
},
|
||||
})
|
||||
|
||||
export const syncUser = (staleUser, syncedUser) => ({
|
||||
type: 'CHRONOGRAF_SYNC_USER',
|
||||
payload: {
|
||||
staleUser,
|
||||
syncedUser,
|
||||
},
|
||||
})
|
||||
|
||||
export const removeUser = user => ({
|
||||
type: 'CHRONOGRAF_REMOVE_USER',
|
||||
payload: {
|
||||
user,
|
||||
},
|
||||
})
|
||||
|
||||
export const addOrganization = organization => ({
|
||||
type: 'CHRONOGRAF_ADD_ORGANIZATION',
|
||||
payload: {
|
||||
organization,
|
||||
},
|
||||
})
|
||||
|
||||
export const renameOrganization = (organization, newName) => ({
|
||||
type: 'CHRONOGRAF_RENAME_ORGANIZATION',
|
||||
payload: {
|
||||
organization,
|
||||
newName,
|
||||
},
|
||||
})
|
||||
|
||||
export const syncOrganization = (staleOrganization, syncedOrganization) => ({
|
||||
type: 'CHRONOGRAF_SYNC_ORGANIZATION',
|
||||
payload: {
|
||||
staleOrganization,
|
||||
syncedOrganization,
|
||||
},
|
||||
})
|
||||
|
||||
export const removeOrganization = organization => ({
|
||||
type: 'CHRONOGRAF_REMOVE_ORGANIZATION',
|
||||
payload: {
|
||||
organization,
|
||||
},
|
||||
})
|
||||
|
||||
// async actions (thunks)
|
||||
export const loadUsersAsync = url => async dispatch => {
|
||||
try {
|
||||
const {data} = await getUsersAJAX(url)
|
||||
dispatch(loadUsers(data))
|
||||
} catch (error) {
|
||||
dispatch(errorThrown(error))
|
||||
}
|
||||
}
|
||||
|
||||
export const loadOrganizationsAsync = url => async dispatch => {
|
||||
try {
|
||||
const {data} = await getOrganizationsAJAX(url)
|
||||
dispatch(loadOrganizations(data))
|
||||
} catch (error) {
|
||||
dispatch(errorThrown(error))
|
||||
}
|
||||
}
|
||||
|
||||
export const createUserAsync = (url, user) => async dispatch => {
|
||||
dispatch(addUser(user))
|
||||
try {
|
||||
const {data} = await createUserAJAX(url, user)
|
||||
dispatch(syncUser(user, data))
|
||||
} catch (error) {
|
||||
dispatch(errorThrown(error))
|
||||
dispatch(removeUser(user))
|
||||
}
|
||||
}
|
||||
|
||||
export const updateUserAsync = (user, updatedUser) => async dispatch => {
|
||||
dispatch(updateUser(user, updatedUser))
|
||||
try {
|
||||
// currently the request will be rejected if name, provider, or scheme, or
|
||||
// no roles are sent with the request.
|
||||
// TODO: remove the null assignments below so that the user request can have
|
||||
// the original name, provider, and scheme once the change to allow this is
|
||||
// implemented server-side
|
||||
const {data} = await updateUserAJAX({
|
||||
...updatedUser,
|
||||
name: null,
|
||||
provider: null,
|
||||
scheme: null,
|
||||
})
|
||||
dispatch(
|
||||
publishAutoDismissingNotification(
|
||||
'success',
|
||||
`User updated: ${user.scheme}::${user.provider}::${user.name}`
|
||||
)
|
||||
)
|
||||
// it's not necessary to syncUser again but it's useful for good
|
||||
// measure and for the clarity of insight in the redux story
|
||||
dispatch(syncUser(user, data))
|
||||
} catch (error) {
|
||||
dispatch(errorThrown(error))
|
||||
dispatch(syncUser(user, user))
|
||||
}
|
||||
}
|
||||
|
||||
export const deleteUserAsync = user => async dispatch => {
|
||||
dispatch(removeUser(user))
|
||||
try {
|
||||
await deleteUserAJAX(user)
|
||||
dispatch(
|
||||
publishAutoDismissingNotification(
|
||||
'success',
|
||||
`User removed from organization: ${user.scheme}::${user.provider}::${user.name}`
|
||||
)
|
||||
)
|
||||
} catch (error) {
|
||||
dispatch(errorThrown(error))
|
||||
dispatch(addUser(user))
|
||||
}
|
||||
}
|
||||
|
||||
export const createOrganizationAsync = (
|
||||
url,
|
||||
organization
|
||||
) => async dispatch => {
|
||||
dispatch(addOrganization(organization))
|
||||
try {
|
||||
const {data} = await createOrganizationAJAX(url, organization)
|
||||
dispatch(syncOrganization(organization, data))
|
||||
} catch (error) {
|
||||
dispatch(errorThrown(error))
|
||||
dispatch(removeOrganization(organization))
|
||||
}
|
||||
}
|
||||
|
||||
export const updateOrganizationAsync = (
|
||||
organization,
|
||||
updatedOrganization
|
||||
) => async dispatch => {
|
||||
dispatch(renameOrganization(organization, updatedOrganization.name))
|
||||
try {
|
||||
const {data} = await updateOrganizationAJAX(updatedOrganization)
|
||||
// it's not necessary to syncOrganization again but it's useful for good
|
||||
// measure and for the clarity of insight in the redux story
|
||||
dispatch(syncOrganization(updatedOrganization, data))
|
||||
} catch (error) {
|
||||
dispatch(errorThrown(error))
|
||||
dispatch(syncOrganization(organization, organization)) // restore if fail
|
||||
}
|
||||
}
|
||||
|
||||
export const deleteOrganizationAsync = organization => async dispatch => {
|
||||
dispatch(removeOrganization(organization))
|
||||
try {
|
||||
await deleteOrganizationAJAX(organization)
|
||||
dispatch(
|
||||
publishAutoDismissingNotification(
|
||||
'success',
|
||||
`Organization deleted: ${organization.name}`
|
||||
)
|
||||
)
|
||||
} catch (error) {
|
||||
dispatch(errorThrown(error))
|
||||
dispatch(addOrganization(organization))
|
||||
}
|
||||
}
|
|
@ -14,7 +14,7 @@ import {
|
|||
updateRole as updateRoleAJAX,
|
||||
updateUser as updateUserAJAX,
|
||||
updateRetentionPolicy as updateRetentionPolicyAJAX,
|
||||
} from 'src/admin/apis'
|
||||
} from 'src/admin/apis/influxdb'
|
||||
|
||||
import {killQuery as killQueryProxy} from 'shared/apis/metaQuery'
|
||||
|
||||
|
@ -25,54 +25,54 @@ import {REVERT_STATE_DELAY} from 'shared/constants'
|
|||
import _ from 'lodash'
|
||||
|
||||
export const loadUsers = ({users}) => ({
|
||||
type: 'LOAD_USERS',
|
||||
type: 'INFLUXDB_LOAD_USERS',
|
||||
payload: {
|
||||
users,
|
||||
},
|
||||
})
|
||||
|
||||
export const loadRoles = ({roles}) => ({
|
||||
type: 'LOAD_ROLES',
|
||||
type: 'INFLUXDB_LOAD_ROLES',
|
||||
payload: {
|
||||
roles,
|
||||
},
|
||||
})
|
||||
|
||||
export const loadPermissions = ({permissions}) => ({
|
||||
type: 'LOAD_PERMISSIONS',
|
||||
type: 'INFLUXDB_LOAD_PERMISSIONS',
|
||||
payload: {
|
||||
permissions,
|
||||
},
|
||||
})
|
||||
|
||||
export const loadDatabases = databases => ({
|
||||
type: 'LOAD_DATABASES',
|
||||
type: 'INFLUXDB_LOAD_DATABASES',
|
||||
payload: {
|
||||
databases,
|
||||
},
|
||||
})
|
||||
|
||||
export const addUser = () => ({
|
||||
type: 'ADD_USER',
|
||||
type: 'INFLUXDB_ADD_USER',
|
||||
})
|
||||
|
||||
export const addRole = () => ({
|
||||
type: 'ADD_ROLE',
|
||||
type: 'INFLUXDB_ADD_ROLE',
|
||||
})
|
||||
|
||||
export const addDatabase = () => ({
|
||||
type: 'ADD_DATABASE',
|
||||
type: 'INFLUXDB_ADD_DATABASE',
|
||||
})
|
||||
|
||||
export const addRetentionPolicy = database => ({
|
||||
type: 'ADD_RETENTION_POLICY',
|
||||
type: 'INFLUXDB_ADD_RETENTION_POLICY',
|
||||
payload: {
|
||||
database,
|
||||
},
|
||||
})
|
||||
|
||||
export const syncUser = (staleUser, syncedUser) => ({
|
||||
type: 'SYNC_USER',
|
||||
type: 'INFLUXDB_SYNC_USER',
|
||||
payload: {
|
||||
staleUser,
|
||||
syncedUser,
|
||||
|
@ -80,7 +80,7 @@ export const syncUser = (staleUser, syncedUser) => ({
|
|||
})
|
||||
|
||||
export const syncRole = (staleRole, syncedRole) => ({
|
||||
type: 'SYNC_ROLE',
|
||||
type: 'INFLUXDB_SYNC_ROLE',
|
||||
payload: {
|
||||
staleRole,
|
||||
syncedRole,
|
||||
|
@ -88,7 +88,7 @@ export const syncRole = (staleRole, syncedRole) => ({
|
|||
})
|
||||
|
||||
export const syncDatabase = (stale, synced) => ({
|
||||
type: 'SYNC_DATABASE',
|
||||
type: 'INFLUXDB_SYNC_DATABASE',
|
||||
payload: {
|
||||
stale,
|
||||
synced,
|
||||
|
@ -96,7 +96,7 @@ export const syncDatabase = (stale, synced) => ({
|
|||
})
|
||||
|
||||
export const syncRetentionPolicy = (database, stale, synced) => ({
|
||||
type: 'SYNC_RETENTION_POLICY',
|
||||
type: 'INFLUXDB_SYNC_RETENTION_POLICY',
|
||||
payload: {
|
||||
database,
|
||||
stale,
|
||||
|
@ -105,7 +105,7 @@ export const syncRetentionPolicy = (database, stale, synced) => ({
|
|||
})
|
||||
|
||||
export const editUser = (user, updates) => ({
|
||||
type: 'EDIT_USER',
|
||||
type: 'INFLUXDB_EDIT_USER',
|
||||
payload: {
|
||||
user,
|
||||
updates,
|
||||
|
@ -113,7 +113,7 @@ export const editUser = (user, updates) => ({
|
|||
})
|
||||
|
||||
export const editRole = (role, updates) => ({
|
||||
type: 'EDIT_ROLE',
|
||||
type: 'INFLUXDB_EDIT_ROLE',
|
||||
payload: {
|
||||
role,
|
||||
updates,
|
||||
|
@ -121,7 +121,7 @@ export const editRole = (role, updates) => ({
|
|||
})
|
||||
|
||||
export const editDatabase = (database, updates) => ({
|
||||
type: 'EDIT_DATABASE',
|
||||
type: 'INFLUXDB_EDIT_DATABASE',
|
||||
payload: {
|
||||
database,
|
||||
updates,
|
||||
|
@ -129,21 +129,21 @@ export const editDatabase = (database, updates) => ({
|
|||
})
|
||||
|
||||
export const killQuery = queryID => ({
|
||||
type: 'KILL_QUERY',
|
||||
type: 'INFLUXDB_KILL_QUERY',
|
||||
payload: {
|
||||
queryID,
|
||||
},
|
||||
})
|
||||
|
||||
export const setQueryToKill = queryIDToKill => ({
|
||||
type: 'SET_QUERY_TO_KILL',
|
||||
type: 'INFLUXDB_SET_QUERY_TO_KILL',
|
||||
payload: {
|
||||
queryIDToKill,
|
||||
},
|
||||
})
|
||||
|
||||
export const loadQueries = queries => ({
|
||||
type: 'LOAD_QUERIES',
|
||||
type: 'INFLUXDB_LOAD_QUERIES',
|
||||
payload: {
|
||||
queries,
|
||||
},
|
||||
|
@ -151,7 +151,7 @@ export const loadQueries = queries => ({
|
|||
|
||||
// TODO: change to 'removeUser'
|
||||
export const deleteUser = user => ({
|
||||
type: 'DELETE_USER',
|
||||
type: 'INFLUXDB_DELETE_USER',
|
||||
payload: {
|
||||
user,
|
||||
},
|
||||
|
@ -159,21 +159,21 @@ export const deleteUser = user => ({
|
|||
|
||||
// TODO: change to 'removeRole'
|
||||
export const deleteRole = role => ({
|
||||
type: 'DELETE_ROLE',
|
||||
type: 'INFLUXDB_DELETE_ROLE',
|
||||
payload: {
|
||||
role,
|
||||
},
|
||||
})
|
||||
|
||||
export const removeDatabase = database => ({
|
||||
type: 'REMOVE_DATABASE',
|
||||
type: 'INFLUXDB_REMOVE_DATABASE',
|
||||
payload: {
|
||||
database,
|
||||
},
|
||||
})
|
||||
|
||||
export const removeRetentionPolicy = (database, retentionPolicy) => ({
|
||||
type: 'REMOVE_RETENTION_POLICY',
|
||||
type: 'INFLUXDB_REMOVE_RETENTION_POLICY',
|
||||
payload: {
|
||||
database,
|
||||
retentionPolicy,
|
||||
|
@ -181,35 +181,35 @@ export const removeRetentionPolicy = (database, retentionPolicy) => ({
|
|||
})
|
||||
|
||||
export const filterUsers = text => ({
|
||||
type: 'FILTER_USERS',
|
||||
type: 'INFLUXDB_FILTER_USERS',
|
||||
payload: {
|
||||
text,
|
||||
},
|
||||
})
|
||||
|
||||
export const filterRoles = text => ({
|
||||
type: 'FILTER_ROLES',
|
||||
type: 'INFLUXDB_FILTER_ROLES',
|
||||
payload: {
|
||||
text,
|
||||
},
|
||||
})
|
||||
|
||||
export const addDatabaseDeleteCode = database => ({
|
||||
type: 'ADD_DATABASE_DELETE_CODE',
|
||||
type: 'INFLUXDB_ADD_DATABASE_DELETE_CODE',
|
||||
payload: {
|
||||
database,
|
||||
},
|
||||
})
|
||||
|
||||
export const removeDatabaseDeleteCode = database => ({
|
||||
type: 'REMOVE_DATABASE_DELETE_CODE',
|
||||
type: 'INFLUXDB_REMOVE_DATABASE_DELETE_CODE',
|
||||
payload: {
|
||||
database,
|
||||
},
|
||||
})
|
||||
|
||||
export const editRetentionPolicy = (database, retentionPolicy, updates) => ({
|
||||
type: 'EDIT_RETENTION_POLICY',
|
||||
type: 'INFLUXDB_EDIT_RETENTION_POLICY',
|
||||
payload: {
|
||||
database,
|
||||
retentionPolicy,
|
|
@ -0,0 +1,104 @@
|
|||
import AJAX from 'src/utils/ajax'
|
||||
|
||||
export const getUsers = async url => {
|
||||
try {
|
||||
return await AJAX({
|
||||
method: 'GET',
|
||||
url,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export const getOrganizations = async url => {
|
||||
try {
|
||||
return await AJAX({
|
||||
method: 'GET',
|
||||
url,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export const createUser = async (url, user) => {
|
||||
try {
|
||||
return await AJAX({
|
||||
method: 'POST',
|
||||
url,
|
||||
data: user,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: change updatedUserWithRolesOnly to a whole user that can have the
|
||||
// original name, provider, and scheme once the change to allow this is
|
||||
// implemented server-side
|
||||
export const updateUser = async updatedUserWithRolesOnly => {
|
||||
try {
|
||||
return await AJAX({
|
||||
method: 'PATCH',
|
||||
url: updatedUserWithRolesOnly.links.self,
|
||||
data: updatedUserWithRolesOnly,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export const deleteUser = async user => {
|
||||
try {
|
||||
return await AJAX({
|
||||
method: 'DELETE',
|
||||
url: user.links.self,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export const createOrganization = async (url, organization) => {
|
||||
try {
|
||||
return await AJAX({
|
||||
method: 'POST',
|
||||
url,
|
||||
data: organization,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export const updateOrganization = async organization => {
|
||||
try {
|
||||
return await AJAX({
|
||||
method: 'PATCH',
|
||||
url: organization.links.self,
|
||||
data: organization,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export const deleteOrganization = async organization => {
|
||||
try {
|
||||
return await AJAX({
|
||||
method: 'DELETE',
|
||||
url: organization.links.self,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
throw error
|
||||
}
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
|
||||
import {
|
||||
isUserAuthorized,
|
||||
ADMIN_ROLE,
|
||||
SUPERADMIN_ROLE,
|
||||
} from 'src/auth/Authorized'
|
||||
|
||||
import {Tab, Tabs, TabPanel, TabPanels, TabList} from 'shared/components/Tabs'
|
||||
import OrganizationsPage from 'src/admin/containers/OrganizationsPage'
|
||||
import UsersTable from 'src/admin/components/chronograf/UsersTable'
|
||||
|
||||
const ORGANIZATIONS_TAB_NAME = 'Organizations'
|
||||
const USERS_TAB_NAME = 'Users'
|
||||
|
||||
const AdminTabs = ({
|
||||
meRole,
|
||||
// UsersTable
|
||||
users,
|
||||
organization,
|
||||
onCreateUser,
|
||||
onUpdateUserRole,
|
||||
onUpdateUserSuperAdmin,
|
||||
onDeleteUser,
|
||||
}) => {
|
||||
const tabs = [
|
||||
{
|
||||
requiredRole: SUPERADMIN_ROLE,
|
||||
type: ORGANIZATIONS_TAB_NAME,
|
||||
component: <OrganizationsPage />,
|
||||
},
|
||||
{
|
||||
requiredRole: ADMIN_ROLE,
|
||||
type: USERS_TAB_NAME,
|
||||
component: (
|
||||
<UsersTable
|
||||
users={users}
|
||||
organization={organization}
|
||||
onCreateUser={onCreateUser}
|
||||
onUpdateUserRole={onUpdateUserRole}
|
||||
onUpdateUserSuperAdmin={onUpdateUserSuperAdmin}
|
||||
onDeleteUser={onDeleteUser}
|
||||
/>
|
||||
),
|
||||
},
|
||||
].filter(t => isUserAuthorized(meRole, t.requiredRole))
|
||||
|
||||
return (
|
||||
<Tabs className="row">
|
||||
<TabList customClass="col-md-2 admin-tabs">
|
||||
{tabs.map((t, i) =>
|
||||
<Tab key={tabs[i].type}>
|
||||
{tabs[i].type}
|
||||
</Tab>
|
||||
)}
|
||||
</TabList>
|
||||
<TabPanels customClass="col-md-10 admin-tabs--content">
|
||||
{tabs.map((t, i) =>
|
||||
<TabPanel key={tabs[i].type}>
|
||||
{t.component}
|
||||
</TabPanel>
|
||||
)}
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
)
|
||||
}
|
||||
|
||||
const {arrayOf, func, shape, string} = PropTypes
|
||||
|
||||
AdminTabs.propTypes = {
|
||||
meRole: string.isRequired,
|
||||
// UsersTable
|
||||
users: arrayOf(shape()),
|
||||
organization: shape({
|
||||
name: string.isRequired,
|
||||
id: string.isRequired,
|
||||
}),
|
||||
onCreateUser: func.isRequired,
|
||||
onUpdateUserRole: func.isRequired,
|
||||
onUpdateUserSuperAdmin: func.isRequired,
|
||||
onDeleteUser: func.isRequired,
|
||||
}
|
||||
|
||||
export default AdminTabs
|
|
@ -0,0 +1,102 @@
|
|||
import React, {Component, PropTypes} from 'react'
|
||||
|
||||
import uuid from 'node-uuid'
|
||||
|
||||
import OrganizationsTableRow from 'src/admin/components/chronograf/OrganizationsTableRow'
|
||||
import OrganizationsTableRowDefault from 'src/admin/components/chronograf/OrganizationsTableRowDefault'
|
||||
import OrganizationsTableRowNew from 'src/admin/components/chronograf/OrganizationsTableRowNew'
|
||||
|
||||
import {DEFAULT_ORG_ID} from 'src/admin/constants/dummyUsers'
|
||||
|
||||
class OrganizationsTable extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
isCreatingOrganization: false,
|
||||
}
|
||||
}
|
||||
handleClickCreateOrganization = () => {
|
||||
this.setState({isCreatingOrganization: true})
|
||||
}
|
||||
|
||||
handleCancelCreateOrganization = () => {
|
||||
this.setState({isCreatingOrganization: false})
|
||||
}
|
||||
|
||||
handleCreateOrganization = newOrganization => {
|
||||
const {onCreateOrg} = this.props
|
||||
onCreateOrg(newOrganization)
|
||||
this.setState({isCreatingOrganization: false})
|
||||
}
|
||||
|
||||
render() {
|
||||
const {organizations, onDeleteOrg, onRenameOrg} = this.props
|
||||
const {isCreatingOrganization} = this.state
|
||||
|
||||
const tableTitle = `${organizations.length} Organization${organizations.length ===
|
||||
1
|
||||
? ''
|
||||
: 's'}`
|
||||
|
||||
return (
|
||||
<div className="panel panel-default">
|
||||
<div className="panel-heading u-flex u-ai-center u-jc-space-between">
|
||||
<h2 className="panel-title">
|
||||
{tableTitle}
|
||||
</h2>
|
||||
<button
|
||||
className="btn btn-sm btn-primary"
|
||||
onClick={this.handleClickCreateOrganization}
|
||||
disabled={isCreatingOrganization}
|
||||
>
|
||||
<span className="icon plus" /> Create Organization
|
||||
</button>
|
||||
</div>
|
||||
<div className="panel-body">
|
||||
<div className="orgs-table--org-labels">
|
||||
<div className="orgs-table--id">ID</div>
|
||||
<div className="orgs-table--name">Name</div>
|
||||
<div className="orgs-table--default-role">Default Role</div>
|
||||
<div className="orgs-table--delete" />
|
||||
</div>
|
||||
{isCreatingOrganization
|
||||
? <OrganizationsTableRowNew
|
||||
onCreateOrganization={this.handleCreateOrganization}
|
||||
onCancelCreateOrganization={this.handleCancelCreateOrganization}
|
||||
/>
|
||||
: null}
|
||||
{organizations.map(
|
||||
org =>
|
||||
org.id === DEFAULT_ORG_ID
|
||||
? <OrganizationsTableRowDefault
|
||||
key={uuid.v4()}
|
||||
organization={org}
|
||||
/>
|
||||
: <OrganizationsTableRow
|
||||
key={uuid.v4()}
|
||||
organization={org}
|
||||
onDelete={onDeleteOrg}
|
||||
onRename={onRenameOrg}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const {arrayOf, func, shape, string} = PropTypes
|
||||
|
||||
OrganizationsTable.propTypes = {
|
||||
organizations: arrayOf(
|
||||
shape({
|
||||
id: string, // when optimistically created, organization will not have an id
|
||||
name: string.isRequired,
|
||||
})
|
||||
).isRequired,
|
||||
onCreateOrg: func.isRequired,
|
||||
onDeleteOrg: func.isRequired,
|
||||
onRenameOrg: func.isRequired,
|
||||
}
|
||||
export default OrganizationsTable
|
|
@ -0,0 +1,159 @@
|
|||
import React, {Component, PropTypes} from 'react'
|
||||
|
||||
import ConfirmButtons from 'shared/components/ConfirmButtons'
|
||||
import Dropdown from 'shared/components/Dropdown'
|
||||
|
||||
import {USER_ROLES} from 'src/admin/constants/dummyUsers'
|
||||
import {MEMBER_ROLE} from 'src/auth/Authorized'
|
||||
|
||||
class OrganizationsTableRow extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
isEditing: false,
|
||||
isDeleting: false,
|
||||
workingName: this.props.organization.name,
|
||||
defaultRole: MEMBER_ROLE,
|
||||
}
|
||||
}
|
||||
|
||||
handleNameClick = () => {
|
||||
this.setState({isEditing: true})
|
||||
}
|
||||
|
||||
handleConfirmRename = () => {
|
||||
const {onRename, organization} = this.props
|
||||
const {workingName} = this.state
|
||||
|
||||
onRename(organization, workingName)
|
||||
this.setState({workingName, isEditing: false})
|
||||
}
|
||||
|
||||
handleCancelRename = () => {
|
||||
const {organization} = this.props
|
||||
|
||||
this.setState({
|
||||
workingName: organization.name,
|
||||
isEditing: false,
|
||||
})
|
||||
}
|
||||
|
||||
handleInputChange = e => {
|
||||
this.setState({workingName: e.target.value})
|
||||
}
|
||||
|
||||
handleInputBlur = () => {
|
||||
const {organization} = this.props
|
||||
const {workingName} = this.state
|
||||
|
||||
if (organization.name === workingName) {
|
||||
this.handleCancelRename()
|
||||
} else {
|
||||
this.handleConfirmRename()
|
||||
}
|
||||
}
|
||||
|
||||
handleKeyDown = e => {
|
||||
if (e.key === 'Enter') {
|
||||
this.handleInputBlur()
|
||||
} else if (e.key === 'Escape') {
|
||||
this.handleCancelRename()
|
||||
}
|
||||
}
|
||||
|
||||
handleFocus = e => {
|
||||
e.target.select()
|
||||
}
|
||||
|
||||
handleDeleteClick = () => {
|
||||
this.setState({isDeleting: true})
|
||||
}
|
||||
|
||||
handleDismissDeleteConfirmation = () => {
|
||||
this.setState({isDeleting: false})
|
||||
}
|
||||
|
||||
handleDeleteOrg = organization => {
|
||||
const {onDelete} = this.props
|
||||
onDelete(organization)
|
||||
}
|
||||
|
||||
handleChooseDefaultRole = role => {
|
||||
this.setState({defaultRole: role.name})
|
||||
}
|
||||
|
||||
render() {
|
||||
const {workingName, isEditing, isDeleting, defaultRole} = this.state
|
||||
const {organization} = this.props
|
||||
|
||||
const dropdownRolesItems = USER_ROLES.map(role => ({
|
||||
...role,
|
||||
text: role.name,
|
||||
}))
|
||||
|
||||
const defaultRoleClassName = isDeleting
|
||||
? 'orgs-table--default-role editing'
|
||||
: 'orgs-table--default-role'
|
||||
|
||||
return (
|
||||
<div className="orgs-table--org">
|
||||
<div className="orgs-table--id">
|
||||
{organization.id}
|
||||
</div>
|
||||
{isEditing
|
||||
? <input
|
||||
type="text"
|
||||
className="form-control input-sm orgs-table--input"
|
||||
defaultValue={workingName}
|
||||
onChange={this.handleInputChange}
|
||||
onBlur={this.handleInputBlur}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
placeholder="Name this Organization..."
|
||||
autoFocus={true}
|
||||
onFocus={this.handleFocus}
|
||||
ref={r => (this.inputRef = r)}
|
||||
/>
|
||||
: <div className="orgs-table--name" onClick={this.handleNameClick}>
|
||||
{workingName}
|
||||
<span className="icon pencil" />
|
||||
</div>}
|
||||
<div className={defaultRoleClassName}>
|
||||
<Dropdown
|
||||
items={dropdownRolesItems}
|
||||
onChoose={this.handleChooseDefaultRole}
|
||||
selected={defaultRole}
|
||||
className="dropdown-stretch"
|
||||
/>
|
||||
</div>
|
||||
{isDeleting
|
||||
? <ConfirmButtons
|
||||
item={organization}
|
||||
onCancel={this.handleDismissDeleteConfirmation}
|
||||
onConfirm={this.handleDeleteOrg}
|
||||
onClickOutside={this.handleDismissDeleteConfirmation}
|
||||
confirmLeft={true}
|
||||
/>
|
||||
: <button
|
||||
className="btn btn-sm btn-default btn-square"
|
||||
onClick={this.handleDeleteClick}
|
||||
>
|
||||
<span className="icon trash" />
|
||||
</button>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const {func, shape, string} = PropTypes
|
||||
|
||||
OrganizationsTableRow.propTypes = {
|
||||
organization: shape({
|
||||
id: string, // when optimistically created, organization will not have an id
|
||||
name: string.isRequired,
|
||||
}).isRequired,
|
||||
onDelete: func.isRequired,
|
||||
onRename: func.isRequired,
|
||||
}
|
||||
|
||||
export default OrganizationsTableRow
|
|
@ -0,0 +1,34 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
|
||||
import {MEMBER_ROLE} from 'src/auth/Authorized'
|
||||
|
||||
// This is a non-editable organization row, used currently for DEFAULT_ORG
|
||||
const OrganizationsTableRowDefault = ({organization}) =>
|
||||
<div className="orgs-table--org">
|
||||
<div className="orgs-table--id">
|
||||
{organization.id}
|
||||
</div>
|
||||
<div className="orgs-table--name-disabled">
|
||||
{organization.name}
|
||||
</div>
|
||||
<div className="orgs-table--default-role-disabled">
|
||||
{MEMBER_ROLE}
|
||||
</div>
|
||||
<button
|
||||
className="btn btn-sm btn-default btn-square orgs-table--delete"
|
||||
disabled={true}
|
||||
>
|
||||
<span className="icon trash" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
const {shape, string} = PropTypes
|
||||
|
||||
OrganizationsTableRowDefault.propTypes = {
|
||||
organization: shape({
|
||||
id: string,
|
||||
name: string.isRequired,
|
||||
}).isRequired,
|
||||
}
|
||||
|
||||
export default OrganizationsTableRowDefault
|
|
@ -0,0 +1,99 @@
|
|||
import React, {Component, PropTypes} from 'react'
|
||||
|
||||
import ConfirmButtons from 'shared/components/ConfirmButtons'
|
||||
import Dropdown from 'shared/components/Dropdown'
|
||||
|
||||
import {USER_ROLES} from 'src/admin/constants/dummyUsers'
|
||||
import {MEMBER_ROLE} from 'src/auth/Authorized'
|
||||
|
||||
class OrganizationsTableRowNew extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
name: 'Untitled Organization',
|
||||
defaultRole: MEMBER_ROLE,
|
||||
}
|
||||
}
|
||||
|
||||
handleKeyDown = e => {
|
||||
const {onCancelCreateOrganization} = this.props
|
||||
|
||||
if (e.key === 'Escape') {
|
||||
onCancelCreateOrganization()
|
||||
}
|
||||
if (e.key === 'Enter') {
|
||||
this.handleClickSave()
|
||||
}
|
||||
}
|
||||
|
||||
handleInputChange = e => {
|
||||
this.setState({name: e.target.value.trim()})
|
||||
}
|
||||
|
||||
handleInputFocus = e => {
|
||||
e.target.select()
|
||||
}
|
||||
|
||||
handleClickSave = () => {
|
||||
const {onCreateOrganization} = this.props
|
||||
const {name, defaultRole} = this.state
|
||||
|
||||
onCreateOrganization(name, defaultRole)
|
||||
}
|
||||
|
||||
handleChooseDefaultRole = role => {
|
||||
this.setState({defaultRole: role.name})
|
||||
}
|
||||
|
||||
render() {
|
||||
const {name, defaultRole} = this.state
|
||||
const {onCancelCreateOrganization} = this.props
|
||||
|
||||
const isSaveDisabled = name === null || name === ''
|
||||
|
||||
const dropdownRolesItems = USER_ROLES.map(role => ({
|
||||
...role,
|
||||
text: role.name,
|
||||
}))
|
||||
|
||||
return (
|
||||
<div className="orgs-table--org orgs-table--new-org">
|
||||
<div className="orgs-table--id">—</div>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control input-sm orgs-table--input"
|
||||
value={name}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
onChange={this.handleInputChange}
|
||||
onFocus={this.handleInputFocus}
|
||||
placeholder="Name this Organization..."
|
||||
autoFocus={true}
|
||||
ref={r => (this.inputRef = r)}
|
||||
/>
|
||||
<div className="orgs-table--default-role editing">
|
||||
<Dropdown
|
||||
items={dropdownRolesItems}
|
||||
onChoose={this.handleChooseDefaultRole}
|
||||
selected={defaultRole}
|
||||
className="dropdown-stretch"
|
||||
/>
|
||||
</div>
|
||||
<ConfirmButtons
|
||||
disabled={isSaveDisabled}
|
||||
onCancel={onCancelCreateOrganization}
|
||||
onConfirm={this.handleClickSave}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const {func} = PropTypes
|
||||
|
||||
OrganizationsTableRowNew.propTypes = {
|
||||
onCreateOrganization: func.isRequired,
|
||||
onCancelCreateOrganization: func.isRequired,
|
||||
}
|
||||
|
||||
export default OrganizationsTableRowNew
|
|
@ -0,0 +1,23 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
|
||||
const PageHeader = ({currentOrganization}) =>
|
||||
<div className="page-header">
|
||||
<div className="page-header__container">
|
||||
<div className="page-header__left">
|
||||
<h1 className="page-header__title">
|
||||
{currentOrganization.name}
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
const {shape, string} = PropTypes
|
||||
|
||||
PageHeader.propTypes = {
|
||||
currentOrganization: shape({
|
||||
id: string.isRequired,
|
||||
name: string.isRequired,
|
||||
}).isRequired,
|
||||
}
|
||||
|
||||
export default PageHeader
|
|
@ -0,0 +1,134 @@
|
|||
import React, {Component, PropTypes} from 'react'
|
||||
|
||||
import uuid from 'node-uuid'
|
||||
|
||||
import Authorized, {SUPERADMIN_ROLE} from 'src/auth/Authorized'
|
||||
|
||||
import UsersTableHeader from 'src/admin/components/chronograf/UsersTableHeader'
|
||||
import UsersTableRowNew from 'src/admin/components/chronograf/UsersTableRowNew'
|
||||
import UsersTableRow from 'src/admin/components/chronograf/UsersTableRow'
|
||||
|
||||
import {USERS_TABLE} from 'src/admin/constants/chronografTableSizing'
|
||||
|
||||
class UsersTable extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
isCreatingUser: false,
|
||||
}
|
||||
}
|
||||
|
||||
handleChangeUserRole = (user, currentRole) => newRole => {
|
||||
this.props.onUpdateUserRole(user, currentRole, newRole)
|
||||
}
|
||||
|
||||
handleChangeSuperAdmin = (user, currentStatus) => newStatus => {
|
||||
this.props.onUpdateUserSuperAdmin(user, currentStatus, newStatus)
|
||||
}
|
||||
|
||||
handleDeleteUser = user => {
|
||||
this.props.onDeleteUser(user)
|
||||
}
|
||||
|
||||
handleClickCreateUser = () => {
|
||||
this.setState({isCreatingUser: true})
|
||||
}
|
||||
|
||||
handleBlurCreateUserRow = () => {
|
||||
this.setState({isCreatingUser: false})
|
||||
}
|
||||
|
||||
render() {
|
||||
const {organization, users, onCreateUser} = this.props
|
||||
|
||||
const {isCreatingUser} = this.state
|
||||
const {
|
||||
colRole,
|
||||
colSuperAdmin,
|
||||
colProvider,
|
||||
colScheme,
|
||||
colActions,
|
||||
} = USERS_TABLE
|
||||
|
||||
return (
|
||||
<div className="panel panel-default">
|
||||
<UsersTableHeader
|
||||
numUsers={users.length}
|
||||
onClickCreateUser={this.handleClickCreateUser}
|
||||
isCreatingUser={isCreatingUser}
|
||||
/>
|
||||
<div className="panel-body">
|
||||
<table className="table table-highlight v-center chronograf-admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Username</th>
|
||||
<th style={{width: colRole}} className="align-with-col-text">
|
||||
Role
|
||||
</th>
|
||||
<Authorized requiredRole={SUPERADMIN_ROLE}>
|
||||
<th style={{width: colSuperAdmin}} className="text-center">
|
||||
SuperAdmin
|
||||
</th>
|
||||
</Authorized>
|
||||
<th style={{width: colProvider}}>Provider</th>
|
||||
<th style={{width: colScheme}}>Scheme</th>
|
||||
<th className="text-right" style={{width: colActions}} />
|
||||
<th /* for DeleteConfirmTableCell */ />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{isCreatingUser
|
||||
? <UsersTableRowNew
|
||||
organization={organization}
|
||||
onBlur={this.handleBlurCreateUserRow}
|
||||
onCreateUser={onCreateUser}
|
||||
/>
|
||||
: null}
|
||||
{users.length || !isCreatingUser
|
||||
? users.map(user =>
|
||||
<UsersTableRow
|
||||
user={user}
|
||||
key={uuid.v4()}
|
||||
organization={organization}
|
||||
onChangeUserRole={this.handleChangeUserRole}
|
||||
onChangeSuperAdmin={this.handleChangeSuperAdmin}
|
||||
onDelete={this.handleDeleteUser}
|
||||
/>
|
||||
)
|
||||
: <tr className="table-empty-state">
|
||||
<Authorized
|
||||
requiredRole={SUPERADMIN_ROLE}
|
||||
replaceWithIfNotAuthorized={
|
||||
<th colSpan="5">
|
||||
<p>No Users to display</p>
|
||||
</th>
|
||||
}
|
||||
>
|
||||
<th colSpan="6">
|
||||
<p>No Users to display</p>
|
||||
</th>
|
||||
</Authorized>
|
||||
</tr>}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const {arrayOf, func, shape, string} = PropTypes
|
||||
|
||||
UsersTable.propTypes = {
|
||||
users: arrayOf(shape()),
|
||||
organization: shape({
|
||||
name: string.isRequired,
|
||||
id: string.isRequired,
|
||||
}),
|
||||
onCreateUser: func.isRequired,
|
||||
onUpdateUserRole: func.isRequired,
|
||||
onUpdateUserSuperAdmin: func.isRequired,
|
||||
onDeleteUser: func.isRequired,
|
||||
}
|
||||
export default UsersTable
|
|
@ -0,0 +1,42 @@
|
|||
import React, {Component, PropTypes} from 'react'
|
||||
import Authorized, {ADMIN_ROLE} from 'src/auth/Authorized'
|
||||
|
||||
class UsersTableHeader extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
}
|
||||
|
||||
render() {
|
||||
const {onClickCreateUser, numUsers, isCreatingUser} = this.props
|
||||
|
||||
const panelTitle = numUsers === 1 ? `${numUsers} User` : `${numUsers} Users`
|
||||
|
||||
return (
|
||||
<div className="panel-heading u-flex u-ai-center u-jc-space-between">
|
||||
<h2 className="panel-title">
|
||||
{panelTitle}
|
||||
</h2>
|
||||
<Authorized requiredRole={ADMIN_ROLE}>
|
||||
<button
|
||||
className="btn btn-primary btn-sm"
|
||||
onClick={onClickCreateUser}
|
||||
disabled={isCreatingUser}
|
||||
>
|
||||
<span className="icon plus" />
|
||||
Create User
|
||||
</button>
|
||||
</Authorized>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const {bool, func, number} = PropTypes
|
||||
|
||||
UsersTableHeader.propTypes = {
|
||||
numUsers: number.isRequired,
|
||||
onClickCreateUser: func.isRequired,
|
||||
isCreatingUser: bool.isRequired,
|
||||
}
|
||||
|
||||
export default UsersTableHeader
|
|
@ -0,0 +1,93 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
|
||||
import Authorized, {SUPERADMIN_ROLE} from 'src/auth/Authorized'
|
||||
|
||||
import Dropdown from 'shared/components/Dropdown'
|
||||
import SlideToggle from 'shared/components/SlideToggle'
|
||||
import DeleteConfirmTableCell from 'shared/components/DeleteConfirmTableCell'
|
||||
|
||||
import {USER_ROLES} from 'src/admin/constants/dummyUsers'
|
||||
import {USERS_TABLE} from 'src/admin/constants/chronografTableSizing'
|
||||
|
||||
const UsersTableRow = ({
|
||||
user,
|
||||
organization,
|
||||
onChangeUserRole,
|
||||
onChangeSuperAdmin,
|
||||
onDelete,
|
||||
}) => {
|
||||
const {
|
||||
colRole,
|
||||
colSuperAdmin,
|
||||
colProvider,
|
||||
colScheme,
|
||||
colActions,
|
||||
} = USERS_TABLE
|
||||
|
||||
const dropdownRolesItems = USER_ROLES.map(r => ({
|
||||
...r,
|
||||
text: r.name,
|
||||
}))
|
||||
const currentRole = user.roles.find(
|
||||
role => role.organization === organization.id
|
||||
)
|
||||
|
||||
return (
|
||||
<tr className={'chronograf-admin-table--user'}>
|
||||
<td>
|
||||
<strong>
|
||||
{user.name}
|
||||
</strong>
|
||||
</td>
|
||||
<td style={{width: colRole}}>
|
||||
<span className="chronograf-user--role">
|
||||
<Dropdown
|
||||
items={dropdownRolesItems}
|
||||
selected={currentRole.name}
|
||||
onChoose={onChangeUserRole(user, currentRole)}
|
||||
buttonColor="btn-primary"
|
||||
buttonSize="btn-xs"
|
||||
className="dropdown-stretch"
|
||||
/>
|
||||
</span>
|
||||
</td>
|
||||
<Authorized requiredRole={SUPERADMIN_ROLE}>
|
||||
<td style={{width: colSuperAdmin}} className="text-center">
|
||||
<SlideToggle
|
||||
active={user.superAdmin}
|
||||
onToggle={onChangeSuperAdmin(user, user.superAdmin)}
|
||||
size="xs"
|
||||
/>
|
||||
</td>
|
||||
</Authorized>
|
||||
<td style={{width: colProvider}}>
|
||||
{user.provider}
|
||||
</td>
|
||||
<td style={{width: colScheme}}>
|
||||
{user.scheme}
|
||||
</td>
|
||||
<td className="text-right" style={{width: colActions}} />
|
||||
<DeleteConfirmTableCell
|
||||
text="Remove"
|
||||
onDelete={onDelete}
|
||||
item={user}
|
||||
buttonSize="btn-xs"
|
||||
/>
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
|
||||
const {func, shape, string} = PropTypes
|
||||
|
||||
UsersTableRow.propTypes = {
|
||||
user: shape(),
|
||||
organization: shape({
|
||||
name: string.isRequired,
|
||||
id: string.isRequired,
|
||||
}),
|
||||
onChangeUserRole: func.isRequired,
|
||||
onChangeSuperAdmin: func.isRequired,
|
||||
onDelete: func.isRequired,
|
||||
}
|
||||
|
||||
export default UsersTableRow
|
|
@ -0,0 +1,153 @@
|
|||
import React, {Component, PropTypes} from 'react'
|
||||
|
||||
import Authorized, {SUPERADMIN_ROLE} from 'src/auth/Authorized'
|
||||
|
||||
import Dropdown from 'shared/components/Dropdown'
|
||||
import SlideToggle from 'shared/components/SlideToggle'
|
||||
|
||||
import {USERS_TABLE} from 'src/admin/constants/chronografTableSizing'
|
||||
import {USER_ROLES} from 'src/admin/constants/dummyUsers'
|
||||
import {MEMBER_ROLE} from 'src/auth/Authorized'
|
||||
|
||||
class UsersTableRowNew extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
name: '',
|
||||
provider: '',
|
||||
scheme: 'oauth2',
|
||||
role: MEMBER_ROLE,
|
||||
superAdmin: false,
|
||||
}
|
||||
}
|
||||
|
||||
handleInputChange = fieldName => e => {
|
||||
this.setState({[fieldName]: e.target.value.trim()})
|
||||
}
|
||||
|
||||
handleConfirmCreateUser = () => {
|
||||
const {onBlur, onCreateUser, organization} = this.props
|
||||
const {name, provider, scheme, role, superAdmin} = this.state
|
||||
|
||||
const newUser = {
|
||||
name,
|
||||
provider,
|
||||
scheme,
|
||||
superAdmin: superAdmin.value,
|
||||
roles: [
|
||||
{
|
||||
name: role,
|
||||
organization: organization.id,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
onCreateUser(newUser)
|
||||
onBlur()
|
||||
}
|
||||
|
||||
handleInputFocus = e => {
|
||||
e.target.select()
|
||||
}
|
||||
|
||||
handleSelectRole = newRole => {
|
||||
this.setState({role: newRole.text})
|
||||
}
|
||||
|
||||
handleSelectSuperAdmin = superAdmin => {
|
||||
this.setState({superAdmin})
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
colRole,
|
||||
colProvider,
|
||||
colScheme,
|
||||
colSuperAdmin,
|
||||
colActions,
|
||||
} = USERS_TABLE
|
||||
const {onBlur} = this.props
|
||||
const {name, provider, scheme, role, superAdmin} = this.state
|
||||
|
||||
const dropdownRolesItems = USER_ROLES.map(r => ({...r, text: r.name}))
|
||||
const preventCreate = !name || !provider
|
||||
|
||||
return (
|
||||
<tr className="chronograf-admin-table--new-user">
|
||||
<td>
|
||||
<input
|
||||
className="form-control input-xs"
|
||||
type="text"
|
||||
placeholder="OAuth Username..."
|
||||
autoFocus={true}
|
||||
value={name}
|
||||
onChange={this.handleInputChange('name')}
|
||||
/>
|
||||
</td>
|
||||
<td style={{width: colRole}}>
|
||||
<Dropdown
|
||||
items={dropdownRolesItems}
|
||||
selected={role}
|
||||
onChoose={this.handleSelectRole}
|
||||
buttonColor="btn-primary"
|
||||
buttonSize="btn-xs"
|
||||
className="dropdown-stretch"
|
||||
/>
|
||||
</td>
|
||||
<Authorized requiredRole={SUPERADMIN_ROLE}>
|
||||
<td style={{width: colSuperAdmin}} className="text-center">
|
||||
<SlideToggle
|
||||
active={superAdmin}
|
||||
size="xs"
|
||||
onToggle={this.handleSelectSuperAdmin}
|
||||
/>
|
||||
</td>
|
||||
</Authorized>
|
||||
<td style={{width: colProvider}}>
|
||||
<input
|
||||
className="form-control input-xs"
|
||||
type="text"
|
||||
placeholder="OAuth Provider..."
|
||||
value={provider}
|
||||
onChange={this.handleInputChange('provider')}
|
||||
/>
|
||||
</td>
|
||||
<td style={{width: colScheme}}>
|
||||
<input
|
||||
className="form-control input-xs disabled"
|
||||
type="text"
|
||||
disabled={true}
|
||||
placeholder="OAuth Scheme..."
|
||||
value={scheme}
|
||||
/>
|
||||
</td>
|
||||
<td className="text-right" style={{width: colActions}}>
|
||||
<button className="btn btn-xs btn-square btn-info" onClick={onBlur}>
|
||||
<span className="icon remove" />
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-xs btn-square btn-success"
|
||||
disabled={preventCreate}
|
||||
onClick={this.handleConfirmCreateUser}
|
||||
>
|
||||
<span className="icon checkmark" />
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const {func, shape, string} = PropTypes
|
||||
|
||||
UsersTableRowNew.propTypes = {
|
||||
organization: shape({
|
||||
id: string.isRequired,
|
||||
name: string.isRequired,
|
||||
}),
|
||||
onBlur: func.isRequired,
|
||||
onCreateUser: func.isRequired,
|
||||
}
|
||||
|
||||
export default UsersTableRowNew
|
|
@ -0,0 +1,7 @@
|
|||
export const USERS_TABLE = {
|
||||
colRole: 120,
|
||||
colSuperAdmin: 90,
|
||||
colProvider: 170,
|
||||
colScheme: 90,
|
||||
colActions: 68,
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
import {
|
||||
MEMBER_ROLE,
|
||||
VIEWER_ROLE,
|
||||
EDITOR_ROLE,
|
||||
ADMIN_ROLE,
|
||||
} from 'src/auth/Authorized'
|
||||
|
||||
export const USER_ROLES = [
|
||||
{name: MEMBER_ROLE},
|
||||
{name: VIEWER_ROLE},
|
||||
{name: EDITOR_ROLE},
|
||||
{name: ADMIN_ROLE},
|
||||
]
|
||||
export const DEFAULT_ORG_ID = '0'
|
||||
export const DEFAULT_ORG_NAME = '__default'
|
||||
export const DEFAULT_ORG = {
|
||||
id: DEFAULT_ORG_ID,
|
||||
name: DEFAULT_ORG_NAME,
|
||||
}
|
||||
export const NO_ORG = 'No Org'
|
||||
|
||||
export const DUMMY_ORGS = [
|
||||
{id: DEFAULT_ORG_ID, name: DEFAULT_ORG_NAME},
|
||||
{name: NO_ORG},
|
||||
{id: '1', name: 'Red Team'},
|
||||
{id: '2', name: 'Blue Team'},
|
||||
{id: '3', name: 'Green Team'},
|
||||
]
|
||||
|
||||
export const SUPERADMIN_OPTION_ITEMS = [
|
||||
{value: true, text: 'yes'},
|
||||
{value: false, text: 'no'},
|
||||
]
|
||||
|
||||
export const ADD_TO_ORGANIZATION = 'Add to organization'
|
|
@ -0,0 +1,133 @@
|
|||
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 PageHeader from 'src/admin/components/chronograf/PageHeader'
|
||||
import AdminTabs from 'src/admin/components/chronograf/AdminTabs'
|
||||
import FancyScrollbar from 'shared/components/FancyScrollbar'
|
||||
|
||||
class AdminChronografPage extends Component {
|
||||
// TODO: revisit this, possibly don't call setState if both are deep equal
|
||||
componentWillReceiveProps(nextProps) {
|
||||
const {currentOrganization} = nextProps
|
||||
|
||||
const hasChangedCurrentOrganization =
|
||||
currentOrganization.id !== this.props.currentOrganization.id
|
||||
|
||||
if (hasChangedCurrentOrganization) {
|
||||
this.loadUsers()
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.loadUsers()
|
||||
}
|
||||
|
||||
loadUsers = () => {
|
||||
const {links, actions: {loadUsersAsync}} = this.props
|
||||
|
||||
loadUsersAsync(links.users)
|
||||
}
|
||||
|
||||
// SINGLE USER ACTIONS
|
||||
handleCreateUser = user => {
|
||||
const {links, actions: {createUserAsync}} = this.props
|
||||
|
||||
createUserAsync(links.users, user)
|
||||
}
|
||||
|
||||
handleUpdateUserRole = () => (user, currentRole, {name}) => {
|
||||
const {actions: {updateUserAsync}} = this.props
|
||||
|
||||
const updatedRole = {...currentRole, name}
|
||||
const newRoles = user.roles.map(
|
||||
r => (r.organization === currentRole.organization ? updatedRole : r)
|
||||
)
|
||||
|
||||
updateUserAsync(user, {...user, roles: newRoles})
|
||||
}
|
||||
handleUpdateUserSuperAdmin = () => (user, currentStatus, {value}) => {
|
||||
const {actions: {updateUserAsync}} = this.props
|
||||
|
||||
const updatedUser = {...user, superAdmin: value}
|
||||
|
||||
updateUserAsync(user, updatedUser)
|
||||
}
|
||||
handleDeleteUser = () => user => {
|
||||
const {actions: {deleteUserAsync}} = this.props
|
||||
|
||||
deleteUserAsync(user)
|
||||
}
|
||||
|
||||
render() {
|
||||
const {users, currentOrganization, meRole} = this.props
|
||||
|
||||
return (
|
||||
<div className="page">
|
||||
<PageHeader currentOrganization={currentOrganization} />
|
||||
<FancyScrollbar className="page-contents">
|
||||
{users
|
||||
? <div className="container-fluid">
|
||||
<div className="row">
|
||||
<div className="col-xs-12">
|
||||
<AdminTabs
|
||||
meRole={meRole}
|
||||
// UsersTable
|
||||
users={users}
|
||||
organization={currentOrganization}
|
||||
onCreateUser={this.handleCreateUser}
|
||||
onUpdateUserRole={this.handleUpdateUserRole()}
|
||||
onUpdateUserSuperAdmin={this.handleUpdateUserSuperAdmin()}
|
||||
onDeleteUser={this.handleDeleteUser()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
: <div className="page-spinner" />}
|
||||
</FancyScrollbar>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const {arrayOf, func, shape, string} = PropTypes
|
||||
|
||||
AdminChronografPage.propTypes = {
|
||||
links: shape({
|
||||
users: string.isRequired,
|
||||
}),
|
||||
users: arrayOf(shape),
|
||||
currentOrganization: shape({
|
||||
id: string.isRequired,
|
||||
name: string.isRequired,
|
||||
}).isRequired,
|
||||
meRole: string.isRequired,
|
||||
actions: shape({
|
||||
loadUsersAsync: func.isRequired,
|
||||
createUserAsync: func.isRequired,
|
||||
updateUserAsync: func.isRequired,
|
||||
deleteUserAsync: func.isRequired,
|
||||
}),
|
||||
notify: func.isRequired,
|
||||
}
|
||||
|
||||
const mapStateToProps = ({
|
||||
links,
|
||||
adminChronograf: {users},
|
||||
auth: {me: {currentOrganization, role: meRole}},
|
||||
}) => ({
|
||||
links,
|
||||
users,
|
||||
currentOrganization,
|
||||
meRole,
|
||||
})
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
actions: bindActionCreators(adminChronografActionCreators, dispatch),
|
||||
notify: bindActionCreators(publishAutoDismissingNotification, dispatch),
|
||||
})
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(AdminChronografPage)
|
|
@ -22,7 +22,7 @@ import {
|
|||
updateUserPermissionsAsync,
|
||||
filterUsers as filterUsersAction,
|
||||
filterRoles as filterRolesAction,
|
||||
} from 'src/admin/actions'
|
||||
} from 'src/admin/actions/influxdb'
|
||||
|
||||
import AdminTabs from 'src/admin/components/AdminTabs'
|
||||
import SourceIndicator from 'shared/components/SourceIndicator'
|
||||
|
@ -40,7 +40,7 @@ const isValidRole = role => {
|
|||
return role.name.length >= minLen
|
||||
}
|
||||
|
||||
class AdminPage extends Component {
|
||||
class AdminInfluxDBPage extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
}
|
||||
|
@ -151,7 +151,7 @@ class AdminPage extends Component {
|
|||
<div className="page-header">
|
||||
<div className="page-header__container">
|
||||
<div className="page-header__left">
|
||||
<h1 className="page-header__title">Admin</h1>
|
||||
<h1 className="page-header__title">InfluxDB Admin</h1>
|
||||
</div>
|
||||
<div className="page-header__right">
|
||||
<SourceIndicator />
|
||||
|
@ -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)
|
|
@ -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,
|
||||
})
|
||||
|
|
|
@ -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 (
|
||||
<OrganizationsTable
|
||||
organizations={organizations}
|
||||
onCreateOrg={this.handleCreateOrganization}
|
||||
onDeleteOrg={this.handleDeleteOrganization}
|
||||
onRenameOrg={this.handleRenameOrganization}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
|
@ -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,
|
||||
})
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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
|
|
@ -0,0 +1,4 @@
|
|||
import adminChronograf from './chronograf'
|
||||
import adminInfluxDB from './influxdb'
|
||||
|
||||
export default {adminChronograf, adminInfluxDB}
|
|
@ -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
|
|
@ -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,
|
||||
})
|
||||
|
||||
|
|
|
@ -0,0 +1,70 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
import {connect} from 'react-redux'
|
||||
|
||||
import {MEMBER_ROLE} from 'src/auth/Authorized'
|
||||
|
||||
const memberCopy = (
|
||||
<p>This role does not grant you sufficient permissions to view Chronograf.</p>
|
||||
)
|
||||
const viewerCopy = (
|
||||
<p>
|
||||
This organization does not have any configured sources<br />and your role
|
||||
does not have permission to configure a source.
|
||||
</p>
|
||||
)
|
||||
|
||||
const Purgatory = ({name, provider, scheme, currentOrganization, role}) =>
|
||||
<div>
|
||||
<div className="auth-page">
|
||||
<div className="auth-box">
|
||||
<div className="auth-logo" />
|
||||
<div className="auth--purgatory">
|
||||
<h3>
|
||||
Logged in to <strong>{currentOrganization.name}</strong> as a{' '}
|
||||
<em>{role}</em>.
|
||||
</h3>
|
||||
{role === MEMBER_ROLE ? memberCopy : viewerCopy}
|
||||
<p>Contact your Administrator for assistance.</p>
|
||||
<hr />
|
||||
<pre>
|
||||
<code>
|
||||
username: {name}
|
||||
<br />
|
||||
provider: {provider}
|
||||
<br />
|
||||
scheme: {scheme}
|
||||
</code>
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
<p className="auth-credits">
|
||||
Made by <span className="icon cubo-uniform" />InfluxData
|
||||
</p>
|
||||
<div className="auth-image" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
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)
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -50,7 +50,7 @@ const DashboardHeader = ({
|
|||
{dashboard
|
||||
? <Authorized
|
||||
requiredRole={EDITOR_ROLE}
|
||||
replaceWith={
|
||||
replaceWithIfNotAuthorized={
|
||||
<h1 className="page-header__title">
|
||||
{activeDashboard}
|
||||
</h1>
|
||||
|
|
|
@ -38,7 +38,10 @@ const DashboardsTable = ({
|
|||
)
|
||||
: <span className="empty-string">None</span>}
|
||||
</td>
|
||||
<Authorized requiredRole={EDITOR_ROLE} replaceWith={<td />}>
|
||||
<Authorized
|
||||
requiredRole={EDITOR_ROLE}
|
||||
replaceWithIfNotAuthorized={<td />}
|
||||
>
|
||||
<DeleteConfirmTableCell
|
||||
onDelete={onDeleteDashboard}
|
||||
item={dashboard}
|
||||
|
|
|
@ -11,7 +11,12 @@ import configureStore from 'src/store/configureStore'
|
|||
import {loadLocalStorage} from 'src/localStorage'
|
||||
|
||||
import App from 'src/App'
|
||||
import {Login, UserIsAuthenticated, UserIsNotAuthenticated} from 'src/auth'
|
||||
import {
|
||||
Login,
|
||||
UserIsAuthenticated,
|
||||
UserIsNotAuthenticated,
|
||||
Purgatory,
|
||||
} from 'src/auth'
|
||||
import CheckSources from 'src/CheckSources'
|
||||
import {StatusPage} from 'src/status'
|
||||
import {HostsPage, HostPage} from 'src/hosts'
|
||||
|
@ -25,7 +30,7 @@ import {
|
|||
KapacitorTasksPage,
|
||||
TickscriptPage,
|
||||
} from 'src/kapacitor'
|
||||
import {AdminPage} from 'src/admin'
|
||||
import {AdminChronografPage, AdminInfluxDBPage} from 'src/admin'
|
||||
import {SourcePage, ManageSources} from 'src/sources'
|
||||
import NotFound from 'shared/components/NotFound'
|
||||
|
||||
|
@ -36,7 +41,8 @@ import {
|
|||
authRequested,
|
||||
authReceived,
|
||||
meRequested,
|
||||
meReceived,
|
||||
meReceivedNotUsingAuth,
|
||||
meReceivedUsingAuth,
|
||||
logoutLinkReceived,
|
||||
} from 'shared/actions/auth'
|
||||
import {linksReceived} from 'shared/actions/links'
|
||||
|
@ -93,12 +99,23 @@ const Root = React.createClass({
|
|||
async startHeartbeat({shouldDispatchResponse}) {
|
||||
try {
|
||||
// These non-me objects are added to every response by some AJAX trickery
|
||||
const {data: me, auth, logoutLink, external} = await getMe()
|
||||
const {
|
||||
data: me,
|
||||
auth,
|
||||
logoutLink,
|
||||
external,
|
||||
users,
|
||||
organizations,
|
||||
meLink,
|
||||
} = await getMe()
|
||||
if (shouldDispatchResponse) {
|
||||
const isUsingAuth = !!logoutLink
|
||||
dispatch(
|
||||
isUsingAuth ? meReceivedUsingAuth(me) : meReceivedNotUsingAuth(me)
|
||||
)
|
||||
dispatch(authReceived(auth))
|
||||
dispatch(meReceived(me))
|
||||
dispatch(logoutLinkReceived(logoutLink))
|
||||
dispatch(linksReceived({external}))
|
||||
dispatch(linksReceived({external, users, organizations, me: meLink}))
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
|
@ -125,6 +142,7 @@ const Root = React.createClass({
|
|||
<Router history={history}>
|
||||
<Route path="/" component={UserIsAuthenticated(CheckSources)} />
|
||||
<Route path="/login" component={UserIsNotAuthenticated(Login)} />
|
||||
<Route path="/purgatory" component={UserIsAuthenticated(Purgatory)} />
|
||||
<Route
|
||||
path="/sources/new"
|
||||
component={UserIsAuthenticated(SourcePage)}
|
||||
|
@ -146,7 +164,8 @@ const Root = React.createClass({
|
|||
<Route path="kapacitors/new" component={KapacitorPage} />
|
||||
<Route path="kapacitors/:id/edit" component={KapacitorPage} />
|
||||
<Route path="kapacitor-tasks" component={KapacitorTasksPage} />
|
||||
<Route path="admin" component={AdminPage} />
|
||||
<Route path="admin-chronograf" component={AdminChronografPage} />
|
||||
<Route path="admin-influxdb" component={AdminInfluxDBPage} />
|
||||
<Route path="manage-sources" component={ManageSources} />
|
||||
<Route path="manage-sources/new" component={SourcePage} />
|
||||
<Route path="manage-sources/:id/edit" component={SourcePage} />
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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 (
|
||||
<div className="confirm-buttons">
|
||||
<button
|
||||
className={classnames('btn btn-info btn-square', {
|
||||
[buttonSize]: buttonSize,
|
||||
})}
|
||||
onClick={this.handleCancel(item)}
|
||||
>
|
||||
<span className="icon remove" />
|
||||
</button>
|
||||
<button
|
||||
className={classnames('btn btn-success btn-square', {
|
||||
[buttonSize]: buttonSize,
|
||||
})}
|
||||
disabled={isDisabled}
|
||||
title={isDisabled ? 'Cannot Save' : 'Save'}
|
||||
onClick={this.handleConfirm(item)}
|
||||
>
|
||||
<span className="icon checkmark" />
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
render() {
|
||||
const {item, buttonSize, isDisabled, confirmLeft} = this.props
|
||||
|
||||
return confirmLeft
|
||||
? <div className="confirm-buttons">
|
||||
<button
|
||||
className={classnames('btn btn-success btn-square', {
|
||||
[buttonSize]: buttonSize,
|
||||
})}
|
||||
disabled={isDisabled}
|
||||
title={isDisabled ? 'Cannot Save' : 'Save'}
|
||||
onClick={this.handleConfirm(item)}
|
||||
>
|
||||
<span className="icon checkmark" />
|
||||
</button>
|
||||
<button
|
||||
className={classnames('btn btn-info btn-square', {
|
||||
[buttonSize]: buttonSize,
|
||||
})}
|
||||
onClick={this.handleCancel(item)}
|
||||
>
|
||||
<span className="icon remove" />
|
||||
</button>
|
||||
</div>
|
||||
: <div className="confirm-buttons">
|
||||
<button
|
||||
className={classnames('btn btn-info btn-square', {
|
||||
[buttonSize]: buttonSize,
|
||||
})}
|
||||
onClick={this.handleCancel(item)}
|
||||
>
|
||||
<span className="icon remove" />
|
||||
</button>
|
||||
<button
|
||||
className={classnames('btn btn-success btn-square', {
|
||||
[buttonSize]: buttonSize,
|
||||
})}
|
||||
disabled={isDisabled}
|
||||
title={isDisabled ? 'Cannot Save' : 'Save'}
|
||||
onClick={this.handleConfirm(item)}
|
||||
>
|
||||
<span className="icon checkmark" />
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
|
|
@ -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}) =>
|
||||
<button
|
||||
className={classnames('btn btn-danger table--show-on-row-hover', {
|
||||
[buttonSize]: buttonSize,
|
||||
})}
|
||||
onClick={onClickDelete}
|
||||
>
|
||||
Delete
|
||||
{text}
|
||||
</button>
|
||||
|
||||
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}
|
||||
/>
|
||||
: <DeleteButton
|
||||
text={text}
|
||||
onClickDelete={this.handleClickDelete}
|
||||
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,
|
||||
|
|
|
@ -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 (
|
||||
<div
|
||||
onClick={this.handleClick}
|
||||
|
@ -222,7 +230,7 @@ class Dropdown extends Component {
|
|||
>
|
||||
{useAutoComplete && isOpen
|
||||
? <div
|
||||
className={`dropdown-autocomplete dropdown-toggle ${buttonSize} ${buttonColor}`}
|
||||
className={`dropdown-autocomplete dropdown-toggle ${buttonSize} ${buttonColor} ${disabledClass}`}
|
||||
style={toggleStyle}
|
||||
>
|
||||
<input
|
||||
|
@ -239,7 +247,7 @@ class Dropdown extends Component {
|
|||
<span className="caret" />
|
||||
</div>
|
||||
: <div
|
||||
className={`btn dropdown-toggle ${buttonSize} ${buttonColor}`}
|
||||
className={`btn dropdown-toggle ${buttonSize} ${buttonColor} ${disabledClass}`}
|
||||
style={toggleStyle}
|
||||
>
|
||||
{iconName
|
||||
|
@ -297,6 +305,7 @@ Dropdown.propTypes = {
|
|||
menuClass: string,
|
||||
useAutoComplete: bool,
|
||||
toggleStyle: shape(),
|
||||
disabled: bool,
|
||||
}
|
||||
|
||||
export default OnClickOutside(Dropdown)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 (
|
||||
<div className={classNames} onClick={this.handleClick}>
|
||||
<div className="slide-toggle--knob" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const {bool, func, string} = PropTypes
|
||||
|
||||
SlideToggle.defaultProps = {
|
||||
size: 'sm',
|
||||
}
|
||||
SlideToggle.propTypes = {
|
||||
active: bool,
|
||||
size: string,
|
||||
onToggle: func.isRequired,
|
||||
}
|
||||
|
||||
export default SlideToggle
|
|
@ -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}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
|
@ -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 (
|
||||
<div className="sidebar--item">
|
||||
<div className="sidebar--square">
|
||||
<div className="sidebar--icon icon user" />
|
||||
</div>
|
||||
<div className="sidebar-menu">
|
||||
<div className="sidebar-menu--heading">
|
||||
{currentOrganization.name} ({currentRole})
|
||||
</div>
|
||||
<div className="sidebar-menu--section">
|
||||
{me.name}
|
||||
</div>
|
||||
<a className="sidebar-menu--item" href={logoutLink}>
|
||||
Logout
|
||||
</a>
|
||||
<div className="sidebar-menu--section">Switch Organizations</div>
|
||||
{roles.map((r, i) => {
|
||||
const isLinkCurrentOrg = currentOrganization.id === r.organization
|
||||
return (
|
||||
<span
|
||||
key={i}
|
||||
className={classnames({
|
||||
'sidebar-menu--item': true,
|
||||
active: isLinkCurrentOrg,
|
||||
})}
|
||||
onClick={this.handleChangeCurrentOrganization(r.organization)}
|
||||
>
|
||||
{organizations.find(o => o.id === r.organization).name}{' '}
|
||||
<strong>({r.name})</strong>
|
||||
</span>
|
||||
)
|
||||
})}
|
||||
{customLinks
|
||||
? <div className="sidebar-menu--section">Custom Links</div>
|
||||
: null}
|
||||
{customLinks
|
||||
? customLinks.map((link, i) =>
|
||||
<a
|
||||
key={i}
|
||||
className="sidebar-menu--item"
|
||||
href={link.url}
|
||||
target="_blank"
|
||||
>
|
||||
{link.name}
|
||||
</a>
|
||||
)
|
||||
: null}
|
||||
<div className="sidebar-menu--triangle" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
|
@ -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 [
|
||||
<NavHeader key={0} title="User" />,
|
||||
...customLinks
|
||||
.sort((a, b) => a.name.toLowerCase() > b.name.toLowerCase())
|
||||
.map(({name, url}, i) =>
|
||||
<NavListItem
|
||||
key={i + 1}
|
||||
useAnchor={true}
|
||||
isExternal={true}
|
||||
link={url}
|
||||
>
|
||||
{name}
|
||||
</NavListItem>
|
||||
links: shape({
|
||||
me: string,
|
||||
external: shape({
|
||||
custom: arrayOf(
|
||||
shape({
|
||||
name: string.isRequired,
|
||||
url: string.isRequired,
|
||||
})
|
||||
),
|
||||
<NavListItem
|
||||
key={customLinks.length + 1}
|
||||
useAnchor={true}
|
||||
link={logoutLink}
|
||||
>
|
||||
Logout
|
||||
</NavListItem>,
|
||||
]
|
||||
}),
|
||||
}),
|
||||
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
|
||||
</NavListItem>
|
||||
</NavBlock>
|
||||
<Authorized requiredRole={ADMIN_ROLE}>
|
||||
|
||||
<Authorized
|
||||
requiredRole={ADMIN_ROLE}
|
||||
replaceWithIfNotUsingAuth={
|
||||
<NavBlock
|
||||
icon="crown2"
|
||||
link={`${sourcePrefix}/admin-influxdb`}
|
||||
location={location}
|
||||
>
|
||||
<NavHeader
|
||||
link={`${sourcePrefix}/admin-influxdb`}
|
||||
title="InfluxDB Admin"
|
||||
/>
|
||||
</NavBlock>
|
||||
}
|
||||
>
|
||||
<NavBlock
|
||||
icon="crown2"
|
||||
link={`${sourcePrefix}/admin`}
|
||||
link={`${sourcePrefix}/admin-chronograf`}
|
||||
location={location}
|
||||
>
|
||||
<NavHeader link={`${sourcePrefix}/admin`} title="Admin" />
|
||||
<NavHeader
|
||||
link={`${sourcePrefix}/admin-chronograf`}
|
||||
title="Admin"
|
||||
/>
|
||||
<NavListItem link={`${sourcePrefix}/admin-chronograf`}>
|
||||
Chronograf
|
||||
</NavListItem>
|
||||
<NavListItem link={`${sourcePrefix}/admin-influxdb`}>
|
||||
InfluxDB
|
||||
</NavListItem>
|
||||
</NavBlock>
|
||||
</Authorized>
|
||||
<NavBlock
|
||||
|
@ -143,32 +168,26 @@ const SideNav = React.createClass({
|
|||
/>
|
||||
</NavBlock>
|
||||
{isUsingAuth
|
||||
? <NavBlock icon="user" location={location}>
|
||||
{customLinks
|
||||
? this.renderUserMenuBlockWithCustomLinks(
|
||||
customLinks,
|
||||
logoutLink
|
||||
)
|
||||
: <NavHeader
|
||||
useAnchor={true}
|
||||
link={logoutLink}
|
||||
title="Logout"
|
||||
/>}
|
||||
</NavBlock>
|
||||
? <UserNavBlock
|
||||
logoutLink={logoutLink}
|
||||
links={links}
|
||||
me={me}
|
||||
sourcePrefix={sourcePrefix}
|
||||
/>
|
||||
: null}
|
||||
</NavBar>
|
||||
},
|
||||
})
|
||||
|
||||
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))
|
||||
|
|
|
@ -144,7 +144,7 @@ const InfluxTable = ({
|
|||
<h5 className="margin-zero">
|
||||
<Authorized
|
||||
requiredRole={EDITOR_ROLE}
|
||||
replaceWith={
|
||||
replaceWithIfNotAuthorized={
|
||||
<strong>
|
||||
{s.name}
|
||||
</strong>
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Styles for Admin Pages
|
||||
Styles for InfluxDB Admin Page
|
||||
----------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
|
|
|
@ -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%;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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%;
|
||||
}
|
||||
|
|
|
@ -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 = {}},
|
||||
|
|
Loading…
Reference in New Issue