Merge remote-tracking branch 'origin/multitenancy' into multitenancy_reset_current_org
commit
0dcfa3f6f5
|
@ -4,8 +4,8 @@ import {
|
|||
authExpired,
|
||||
authRequested,
|
||||
authReceived,
|
||||
meRequested,
|
||||
meReceivedNotUsingAuth,
|
||||
meGetRequested,
|
||||
meGetCompletedNotUsingAuth,
|
||||
} from 'shared/actions/auth'
|
||||
|
||||
const defaultAuth = {
|
||||
|
@ -51,17 +51,17 @@ describe('Shared.Reducers.authReducer', () => {
|
|||
expect(reducedState.isAuthLoading).to.equal(false)
|
||||
})
|
||||
|
||||
it('should handle ME_REQUESTED', () => {
|
||||
const reducedState = authReducer(initialState, meRequested())
|
||||
it('should handle ME_GET_REQUESTED', () => {
|
||||
const reducedState = authReducer(initialState, meGetRequested())
|
||||
|
||||
expect(reducedState.isMeLoading).to.equal(true)
|
||||
})
|
||||
|
||||
it('should handle ME_RECEIVED__NON_AUTH', () => {
|
||||
it('should handle ME_GET_COMPLETED__NON_AUTH', () => {
|
||||
const loadingState = {...initialState, isMeLoading: true}
|
||||
const reducedState = authReducer(
|
||||
loadingState,
|
||||
meReceivedNotUsingAuth(defaultMe)
|
||||
meGetCompletedNotUsingAuth(defaultMe)
|
||||
)
|
||||
|
||||
expect(reducedState.me).to.deep.equal(defaultMe)
|
||||
|
|
|
@ -5,10 +5,9 @@ import {bindActionCreators} from 'redux'
|
|||
|
||||
import {MEMBER_ROLE, VIEWER_ROLE} from 'src/auth/Authorized'
|
||||
|
||||
import {getSources} from 'shared/apis'
|
||||
import {showDatabases} from 'shared/apis/metaQuery'
|
||||
|
||||
import {loadSources as loadSourcesAction} from 'shared/actions/sources'
|
||||
import {getSourcesAsync} from 'shared/actions/sources'
|
||||
import {errorThrown as errorThrownAction} from 'shared/actions/errors'
|
||||
|
||||
import {DEFAULT_HOME_PAGE} from 'shared/constants'
|
||||
|
@ -19,6 +18,7 @@ import {DEFAULT_HOME_PAGE} from 'shared/constants'
|
|||
const {arrayOf, bool, func, node, shape, string} = PropTypes
|
||||
const CheckSources = React.createClass({
|
||||
propTypes: {
|
||||
getSources: func.isRequired,
|
||||
sources: arrayOf(
|
||||
shape({
|
||||
links: shape({
|
||||
|
@ -42,8 +42,6 @@ const CheckSources = React.createClass({
|
|||
location: shape({
|
||||
pathname: string.isRequired,
|
||||
}).isRequired,
|
||||
loadSources: func.isRequired,
|
||||
errorThrown: func.isRequired,
|
||||
auth: shape({
|
||||
isUsingAuth: bool,
|
||||
me: shape({
|
||||
|
@ -81,16 +79,22 @@ const CheckSources = React.createClass({
|
|||
},
|
||||
|
||||
async componentWillMount() {
|
||||
const {loadSources, errorThrown} = this.props
|
||||
await this.props.getSources()
|
||||
this.setState({isFetching: false})
|
||||
},
|
||||
|
||||
try {
|
||||
const {data: {sources}} = await getSources()
|
||||
loadSources(sources)
|
||||
this.setState({isFetching: false})
|
||||
} catch (error) {
|
||||
errorThrown(error, 'Unable to connect to Chronograf server')
|
||||
this.setState({isFetching: false})
|
||||
shouldComponentUpdate(nextProps) {
|
||||
const {auth: {isUsingAuth, me}} = nextProps
|
||||
// don't update this component if currentOrganization is what has changed,
|
||||
// or else the app will try to call showDatabases in componentWillUpdate,
|
||||
// which will fail unless sources have been refreshed
|
||||
if (
|
||||
isUsingAuth &&
|
||||
me.currentOrganization.id !== this.props.auth.me.currentOrganization.id
|
||||
) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
},
|
||||
|
||||
async componentWillUpdate(nextProps, nextState) {
|
||||
|
@ -139,6 +143,8 @@ const CheckSources = React.createClass({
|
|||
if (!isFetching && !location.pathname.includes('/manage-sources')) {
|
||||
// Do simple query to proxy to see if the source is up.
|
||||
try {
|
||||
// the guard around currentOrganization prevents this showDatabases
|
||||
// invocation since sources haven't been refreshed yet
|
||||
await showDatabases(source.links.proxy)
|
||||
} catch (error) {
|
||||
errorThrown(error, 'Unable to connect to source')
|
||||
|
@ -178,7 +184,7 @@ const mapStateToProps = ({sources, auth, me}) => ({
|
|||
})
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
loadSources: bindActionCreators(loadSourcesAction, dispatch),
|
||||
getSources: bindActionCreators(getSourcesAsync, dispatch),
|
||||
errorThrown: bindActionCreators(errorThrownAction, dispatch),
|
||||
})
|
||||
|
||||
|
|
|
@ -3,30 +3,37 @@ import {connect} from 'react-redux'
|
|||
import {bindActionCreators} from 'redux'
|
||||
|
||||
import * as adminChronografActionCreators from 'src/admin/actions/chronograf'
|
||||
import {publishAutoDismissingNotification} from 'shared/dispatchers'
|
||||
import {getMeAsync} from 'shared/actions/auth'
|
||||
|
||||
import OrganizationsTable from 'src/admin/components/chronograf/OrganizationsTable'
|
||||
|
||||
class OrganizationsPage extends Component {
|
||||
componentDidMount() {
|
||||
const {links, actions: {loadOrganizationsAsync}} = this.props
|
||||
|
||||
loadOrganizationsAsync(links.organizations)
|
||||
}
|
||||
|
||||
handleCreateOrganization = organization => {
|
||||
handleCreateOrganization = async organization => {
|
||||
const {links, actions: {createOrganizationAsync}} = this.props
|
||||
createOrganizationAsync(links.organizations, organization)
|
||||
await createOrganizationAsync(links.organizations, organization)
|
||||
this.refreshMe()
|
||||
}
|
||||
|
||||
handleRenameOrganization = (organization, name) => {
|
||||
handleRenameOrganization = async (organization, name) => {
|
||||
const {actions: {updateOrganizationAsync}} = this.props
|
||||
updateOrganizationAsync(organization, {...organization, name})
|
||||
await updateOrganizationAsync(organization, {...organization, name})
|
||||
this.refreshMe()
|
||||
}
|
||||
|
||||
handleDeleteOrganization = organization => {
|
||||
const {actions: {deleteOrganizationAsync}} = this.props
|
||||
deleteOrganizationAsync(organization)
|
||||
this.refreshMe()
|
||||
}
|
||||
|
||||
refreshMe = () => {
|
||||
const {getMe} = this.props
|
||||
getMe({shouldResetMe: false})
|
||||
}
|
||||
|
||||
handleTogglePublic = organization => {
|
||||
|
@ -40,6 +47,8 @@ class OrganizationsPage extends Component {
|
|||
handleChooseDefaultRole = (organization, defaultRole) => {
|
||||
const {actions: {updateOrganizationAsync}} = this.props
|
||||
updateOrganizationAsync(organization, {...organization, defaultRole})
|
||||
// refreshMe is here to update the org's defaultRole in `me.organizations`
|
||||
this.refreshMe()
|
||||
}
|
||||
|
||||
render() {
|
||||
|
@ -77,7 +86,7 @@ OrganizationsPage.propTypes = {
|
|||
updateOrganizationAsync: func.isRequired,
|
||||
deleteOrganizationAsync: func.isRequired,
|
||||
}),
|
||||
notify: func.isRequired,
|
||||
getMe: func.isRequired,
|
||||
}
|
||||
|
||||
const mapStateToProps = ({links, adminChronograf: {organizations}}) => ({
|
||||
|
@ -87,7 +96,7 @@ const mapStateToProps = ({links, adminChronograf: {organizations}}) => ({
|
|||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
actions: bindActionCreators(adminChronografActionCreators, dispatch),
|
||||
notify: bindActionCreators(publishAutoDismissingNotification, dispatch),
|
||||
getMe: bindActionCreators(getMeAsync, dispatch),
|
||||
})
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(OrganizationsPage)
|
||||
|
|
|
@ -6,6 +6,7 @@ import {Provider} from 'react-redux'
|
|||
import {Router, Route, useRouterHistory} from 'react-router'
|
||||
import {createHistory} from 'history'
|
||||
import {syncHistoryWithStore} from 'react-router-redux'
|
||||
import {bindActionCreators} from 'redux'
|
||||
|
||||
import configureStore from 'src/store/configureStore'
|
||||
import {loadLocalStorage} from 'src/localStorage'
|
||||
|
@ -34,18 +35,9 @@ import {AdminChronografPage, AdminInfluxDBPage} from 'src/admin'
|
|||
import {SourcePage, ManageSources} from 'src/sources'
|
||||
import NotFound from 'shared/components/NotFound'
|
||||
|
||||
import {getMe} from 'shared/apis'
|
||||
import {getMeAsync} from 'shared/actions/auth'
|
||||
|
||||
import {disablePresentationMode} from 'shared/actions/app'
|
||||
import {
|
||||
authRequested,
|
||||
authReceived,
|
||||
meRequested,
|
||||
meReceivedNotUsingAuth,
|
||||
meReceivedUsingAuth,
|
||||
logoutLinkReceived,
|
||||
} from 'shared/actions/auth'
|
||||
import {linksReceived} from 'shared/actions/links'
|
||||
import {errorThrown} from 'shared/actions/errors'
|
||||
|
||||
import 'src/style/chronograf.scss'
|
||||
|
@ -87,45 +79,24 @@ const Root = React.createClass({
|
|||
},
|
||||
|
||||
async checkAuth() {
|
||||
dispatch(authRequested())
|
||||
dispatch(meRequested())
|
||||
try {
|
||||
await this.startHeartbeat({shouldDispatchResponse: true})
|
||||
await this.startHeartbeat({shouldResetMe: true})
|
||||
} catch (error) {
|
||||
dispatch(errorThrown(error))
|
||||
}
|
||||
},
|
||||
|
||||
async startHeartbeat({shouldDispatchResponse}) {
|
||||
try {
|
||||
// These non-me objects are added to every response by some AJAX trickery
|
||||
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(logoutLinkReceived(logoutLink))
|
||||
dispatch(linksReceived({external, users, organizations, me: meLink}))
|
||||
}
|
||||
getMe: bindActionCreators(getMeAsync, dispatch),
|
||||
|
||||
setTimeout(() => {
|
||||
if (store.getState().auth.me !== null) {
|
||||
this.startHeartbeat({shouldDispatchResponse: false})
|
||||
}
|
||||
}, HEARTBEAT_INTERVAL)
|
||||
} catch (error) {
|
||||
dispatch(errorThrown(error))
|
||||
}
|
||||
async startHeartbeat(config) {
|
||||
// TODO: use destructure syntax with default {} value -- couldn't figure it out
|
||||
await this.getMe({shouldResetMe: config && config.shouldResetMe})
|
||||
|
||||
setTimeout(() => {
|
||||
if (store.getState().auth.me !== null) {
|
||||
this.startHeartbeat()
|
||||
}
|
||||
}, HEARTBEAT_INTERVAL)
|
||||
},
|
||||
|
||||
flushErrorsQueue() {
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
import {updateMe as updateMeAJAX} from 'shared/apis/auth'
|
||||
import {getMe as getMeAJAX, updateMe as updateMeAJAX} from 'shared/apis/auth'
|
||||
|
||||
import {linksReceived} from 'shared/actions/links'
|
||||
|
||||
import {publishAutoDismissingNotification} from 'shared/dispatchers'
|
||||
import {errorThrown} from 'shared/actions/errors'
|
||||
|
@ -21,24 +23,28 @@ export const authReceived = auth => ({
|
|||
},
|
||||
})
|
||||
|
||||
export const meRequested = () => ({
|
||||
type: 'ME_REQUESTED',
|
||||
export const meGetRequested = () => ({
|
||||
type: 'ME_GET_REQUESTED',
|
||||
})
|
||||
|
||||
export const meReceivedNotUsingAuth = me => ({
|
||||
type: 'ME_RECEIVED__NON_AUTH',
|
||||
export const meGetCompletedNotUsingAuth = me => ({
|
||||
type: 'ME_GET_COMPLETED__NON_AUTH',
|
||||
payload: {
|
||||
me,
|
||||
},
|
||||
})
|
||||
|
||||
export const meReceivedUsingAuth = me => ({
|
||||
type: 'ME_RECEIVED__AUTH',
|
||||
export const meGetCompletedUsingAuth = me => ({
|
||||
type: 'ME_GET_COMPLETED__AUTH',
|
||||
payload: {
|
||||
me,
|
||||
},
|
||||
})
|
||||
|
||||
export const meGetFailed = () => ({
|
||||
type: 'ME_GET_FAILED',
|
||||
})
|
||||
|
||||
export const meChangeOrganizationRequested = () => ({
|
||||
type: 'ME_CHANGE_ORGANIZATION_REQUESTED',
|
||||
})
|
||||
|
@ -58,6 +64,39 @@ export const logoutLinkReceived = logoutLink => ({
|
|||
},
|
||||
})
|
||||
|
||||
// shouldResetMe protects against `me` being nullified in Redux temporarily,
|
||||
// which currently causes the app to show a loading spinner until me is
|
||||
// re-hydrated. if `getMeAsync` is only being used to refresh me after creating
|
||||
// an organization, this is undesirable behavior
|
||||
export const getMeAsync = ({shouldResetMe}) => async dispatch => {
|
||||
if (shouldResetMe) {
|
||||
dispatch(authRequested())
|
||||
dispatch(meGetRequested())
|
||||
}
|
||||
try {
|
||||
// These non-me objects are added to every response by some AJAX trickery
|
||||
const {
|
||||
data: me,
|
||||
auth,
|
||||
logoutLink,
|
||||
external,
|
||||
users,
|
||||
organizations,
|
||||
meLink,
|
||||
} = await getMeAJAX()
|
||||
const isUsingAuth = !!logoutLink
|
||||
dispatch(
|
||||
isUsingAuth ? meGetCompletedUsingAuth(me) : meGetCompletedNotUsingAuth(me)
|
||||
)
|
||||
dispatch(authReceived(auth))
|
||||
dispatch(logoutLinkReceived(logoutLink))
|
||||
dispatch(linksReceived({external, users, organizations, me: meLink}))
|
||||
} catch (error) {
|
||||
dispatch(errorThrown(error))
|
||||
dispatch(meGetFailed())
|
||||
}
|
||||
}
|
||||
|
||||
export const meChangeOrganizationAsync = (
|
||||
url,
|
||||
organization
|
||||
|
@ -72,7 +111,10 @@ export const meChangeOrganizationAsync = (
|
|||
)
|
||||
)
|
||||
dispatch(meChangeOrganizationCompleted())
|
||||
dispatch(meReceivedUsingAuth(data))
|
||||
dispatch(meGetCompletedUsingAuth(data))
|
||||
// TODO: reload sources upon me change org if non-refresh behavior preferred
|
||||
// instead of current behavior on both invocations of meChangeOrganization,
|
||||
// which is to refresh index via router.push('')
|
||||
} catch (error) {
|
||||
dispatch(errorThrown(error))
|
||||
dispatch(meChangeOrganizationFailed())
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
import {
|
||||
deleteSource,
|
||||
getSources,
|
||||
getSources as getSourcesAJAX,
|
||||
getKapacitors as getKapacitorsAJAX,
|
||||
updateKapacitor as updateKapacitorAJAX,
|
||||
deleteKapacitor as deleteKapacitorAJAX,
|
||||
} from 'shared/apis'
|
||||
import {publishNotification} from './notifications'
|
||||
import {errorThrown} from 'shared/actions/errors'
|
||||
|
||||
import {HTTP_NOT_FOUND} from 'shared/constants'
|
||||
|
||||
|
@ -67,7 +68,7 @@ export const removeAndLoadSources = source => async dispatch => {
|
|||
}
|
||||
}
|
||||
|
||||
const {data: {sources: newSources}} = await getSources()
|
||||
const {data: {sources: newSources}} = await getSourcesAJAX()
|
||||
dispatch(loadSources(newSources))
|
||||
} catch (err) {
|
||||
dispatch(
|
||||
|
@ -110,3 +111,12 @@ export const deleteKapacitorAsync = kapacitor => async dispatch => {
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
export const getSourcesAsync = () => async dispatch => {
|
||||
try {
|
||||
const {data: {sources}} = await getSourcesAJAX()
|
||||
dispatch(loadSources(sources))
|
||||
} catch (error) {
|
||||
dispatch(errorThrown(error))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,12 @@
|
|||
import AJAX from 'src/utils/ajax'
|
||||
|
||||
export function getMe() {
|
||||
return AJAX({
|
||||
resource: 'me',
|
||||
method: 'GET',
|
||||
})
|
||||
}
|
||||
|
||||
export const updateMe = async (url, updatedMe) => {
|
||||
try {
|
||||
return await AJAX({
|
||||
|
|
|
@ -8,13 +8,6 @@ export function fetchLayouts() {
|
|||
})
|
||||
}
|
||||
|
||||
export function getMe() {
|
||||
return AJAX({
|
||||
resource: 'me',
|
||||
method: 'GET',
|
||||
})
|
||||
}
|
||||
|
||||
export function getSources() {
|
||||
return AJAX({
|
||||
resource: 'sources',
|
||||
|
|
|
@ -25,10 +25,10 @@ const authReducer = (state = initialState, action) => {
|
|||
const {auth: {links}} = action.payload
|
||||
return {...state, links, isAuthLoading: false}
|
||||
}
|
||||
case 'ME_REQUESTED': {
|
||||
case 'ME_GET_REQUESTED': {
|
||||
return {...state, isMeLoading: true}
|
||||
}
|
||||
case 'ME_RECEIVED__NON_AUTH': {
|
||||
case 'ME_GET_COMPLETED__NON_AUTH': {
|
||||
const {me} = action.payload
|
||||
return {
|
||||
...state,
|
||||
|
@ -36,7 +36,7 @@ const authReducer = (state = initialState, action) => {
|
|||
isMeLoading: false,
|
||||
}
|
||||
}
|
||||
case 'ME_RECEIVED__AUTH': {
|
||||
case 'ME_GET_COMPLETED__AUTH': {
|
||||
const {me, me: {currentOrganization}} = action.payload
|
||||
return {
|
||||
...state,
|
||||
|
|
|
@ -204,15 +204,25 @@ $sidebar-menu--gutter: 18px;
|
|||
opacity: 0.6;
|
||||
}
|
||||
// Invisible triangle for easier mouse movement when navigating to sub items
|
||||
.sidebar-menu--item + .sidebar-menu--triangle {
|
||||
.sidebar-menu--item + .sidebar-menu--triangle,
|
||||
.sidebar-menu--inverse .sidebar-menu--triangle {
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
}
|
||||
.sidebar-menu--item + .sidebar-menu--triangle {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
z-index: -1;
|
||||
top: $sidebar--width;
|
||||
left: 0px;
|
||||
transform: translate(-50%,-50%) rotate(45deg);
|
||||
}
|
||||
.sidebar-menu--inverse .sidebar-menu--triangle {
|
||||
width: 50px;
|
||||
height: 60px;
|
||||
bottom: 12px;
|
||||
left: 6px;
|
||||
transform: translate(-50%,-50%) rotate(30deg);
|
||||
}
|
||||
|
||||
.sidebar-menu--section {
|
||||
white-space: nowrap;
|
||||
|
|
Loading…
Reference in New Issue