Refactor getMe & refresh me for SideNav upon change organizations

pull/2395/head
Jared Scheib 2017-11-10 19:37:35 -08:00
parent e1e81bee5e
commit 9c8cac04a9
7 changed files with 81 additions and 64 deletions

View File

@ -4,7 +4,7 @@ import {
authExpired, authExpired,
authRequested, authRequested,
authReceived, authReceived,
meRequested, meGetRequested,
meReceivedNotUsingAuth, meReceivedNotUsingAuth,
} from 'shared/actions/auth' } from 'shared/actions/auth'
@ -51,8 +51,8 @@ describe('Shared.Reducers.authReducer', () => {
expect(reducedState.isAuthLoading).to.equal(false) expect(reducedState.isAuthLoading).to.equal(false)
}) })
it('should handle ME_REQUESTED', () => { it('should handle ME_GET_REQUESTED', () => {
const reducedState = authReducer(initialState, meRequested()) const reducedState = authReducer(initialState, meGetRequested())
expect(reducedState.isMeLoading).to.equal(true) expect(reducedState.isMeLoading).to.equal(true)
}) })

View File

@ -3,30 +3,37 @@ import {connect} from 'react-redux'
import {bindActionCreators} from 'redux' import {bindActionCreators} from 'redux'
import * as adminChronografActionCreators from 'src/admin/actions/chronograf' 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' import OrganizationsTable from 'src/admin/components/chronograf/OrganizationsTable'
class OrganizationsPage extends Component { class OrganizationsPage extends Component {
componentDidMount() { componentDidMount() {
const {links, actions: {loadOrganizationsAsync}} = this.props const {links, actions: {loadOrganizationsAsync}} = this.props
loadOrganizationsAsync(links.organizations) loadOrganizationsAsync(links.organizations)
} }
handleCreateOrganization = organization => { handleCreateOrganization = async organization => {
const {links, actions: {createOrganizationAsync}} = this.props 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 const {actions: {updateOrganizationAsync}} = this.props
updateOrganizationAsync(organization, {...organization, name}) await updateOrganizationAsync(organization, {...organization, name})
this.refreshMe()
} }
handleDeleteOrganization = organization => { handleDeleteOrganization = organization => {
const {actions: {deleteOrganizationAsync}} = this.props const {actions: {deleteOrganizationAsync}} = this.props
deleteOrganizationAsync(organization) deleteOrganizationAsync(organization)
this.refreshMe()
}
refreshMe = () => {
const {getMe} = this.props
getMe({shouldResetMe: false})
} }
handleTogglePublic = organization => { handleTogglePublic = organization => {
@ -40,6 +47,8 @@ class OrganizationsPage extends Component {
handleChooseDefaultRole = (organization, defaultRole) => { handleChooseDefaultRole = (organization, defaultRole) => {
const {actions: {updateOrganizationAsync}} = this.props const {actions: {updateOrganizationAsync}} = this.props
updateOrganizationAsync(organization, {...organization, defaultRole}) updateOrganizationAsync(organization, {...organization, defaultRole})
// refreshMe is here to update the org's defaultRole in `me.organizations`
this.refreshMe()
} }
render() { render() {
@ -77,7 +86,7 @@ OrganizationsPage.propTypes = {
updateOrganizationAsync: func.isRequired, updateOrganizationAsync: func.isRequired,
deleteOrganizationAsync: func.isRequired, deleteOrganizationAsync: func.isRequired,
}), }),
notify: func.isRequired, getMe: func.isRequired,
} }
const mapStateToProps = ({links, adminChronograf: {organizations}}) => ({ const mapStateToProps = ({links, adminChronograf: {organizations}}) => ({
@ -87,7 +96,7 @@ const mapStateToProps = ({links, adminChronograf: {organizations}}) => ({
const mapDispatchToProps = dispatch => ({ const mapDispatchToProps = dispatch => ({
actions: bindActionCreators(adminChronografActionCreators, dispatch), actions: bindActionCreators(adminChronografActionCreators, dispatch),
notify: bindActionCreators(publishAutoDismissingNotification, dispatch), getMe: bindActionCreators(getMeAsync, dispatch),
}) })
export default connect(mapStateToProps, mapDispatchToProps)(OrganizationsPage) export default connect(mapStateToProps, mapDispatchToProps)(OrganizationsPage)

View File

@ -6,6 +6,7 @@ import {Provider} from 'react-redux'
import {Router, Route, useRouterHistory} from 'react-router' import {Router, Route, useRouterHistory} from 'react-router'
import {createHistory} from 'history' import {createHistory} from 'history'
import {syncHistoryWithStore} from 'react-router-redux' import {syncHistoryWithStore} from 'react-router-redux'
import {bindActionCreators} from 'redux'
import configureStore from 'src/store/configureStore' import configureStore from 'src/store/configureStore'
import {loadLocalStorage} from 'src/localStorage' import {loadLocalStorage} from 'src/localStorage'
@ -34,18 +35,9 @@ import {AdminChronografPage, AdminInfluxDBPage} from 'src/admin'
import {SourcePage, ManageSources} from 'src/sources' import {SourcePage, ManageSources} from 'src/sources'
import NotFound from 'shared/components/NotFound' import NotFound from 'shared/components/NotFound'
import {getMe} from 'shared/apis' import {getMeAsync} from 'shared/actions/auth'
import {disablePresentationMode} from 'shared/actions/app' 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 {errorThrown} from 'shared/actions/errors'
import 'src/style/chronograf.scss' import 'src/style/chronograf.scss'
@ -87,45 +79,24 @@ const Root = React.createClass({
}, },
async checkAuth() { async checkAuth() {
dispatch(authRequested())
dispatch(meRequested())
try { try {
await this.startHeartbeat({shouldDispatchResponse: true}) await this.startHeartbeat({shouldResetMe: true})
} catch (error) { } catch (error) {
dispatch(errorThrown(error)) dispatch(errorThrown(error))
} }
}, },
async startHeartbeat({shouldDispatchResponse}) { getMe: bindActionCreators(getMeAsync, dispatch),
try {
// These non-me objects are added to every response by some AJAX trickery async startHeartbeat(config) {
const { // TODO: use destructure syntax with default {} value -- couldn't figure it out
data: me, await this.getMe({shouldResetMe: config && config.shouldResetMe})
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}))
}
setTimeout(() => { setTimeout(() => {
if (store.getState().auth.me !== null) { if (store.getState().auth.me !== null) {
this.startHeartbeat({shouldDispatchResponse: false}) this.startHeartbeat()
} }
}, HEARTBEAT_INTERVAL) }, HEARTBEAT_INTERVAL)
} catch (error) {
dispatch(errorThrown(error))
}
}, },
flushErrorsQueue() { flushErrorsQueue() {

View File

@ -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 {publishAutoDismissingNotification} from 'shared/dispatchers'
import {errorThrown} from 'shared/actions/errors' import {errorThrown} from 'shared/actions/errors'
@ -21,8 +23,8 @@ export const authReceived = auth => ({
}, },
}) })
export const meRequested = () => ({ export const meGetRequested = () => ({
type: 'ME_REQUESTED', type: 'ME_GET_REQUESTED',
}) })
export const meReceivedNotUsingAuth = me => ({ export const meReceivedNotUsingAuth = me => ({
@ -39,6 +41,10 @@ export const meReceivedUsingAuth = me => ({
}, },
}) })
export const meGetFailed = () => ({
type: 'ME_GET_FAILED',
})
export const meChangeOrganizationRequested = () => ({ export const meChangeOrganizationRequested = () => ({
type: 'ME_CHANGE_ORGANIZATION_REQUESTED', type: 'ME_CHANGE_ORGANIZATION_REQUESTED',
}) })
@ -58,6 +64,37 @@ 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 ? meReceivedUsingAuth(me) : meReceivedNotUsingAuth(me))
dispatch(authReceived(auth))
dispatch(logoutLinkReceived(logoutLink))
dispatch(linksReceived({external, users, organizations, me: meLink}))
} catch (error) {
dispatch(errorThrown(error))
dispatch(meGetFailed())
}
}
export const meChangeOrganizationAsync = ( export const meChangeOrganizationAsync = (
url, url,
organization organization

View File

@ -1,5 +1,12 @@
import AJAX from 'src/utils/ajax' import AJAX from 'src/utils/ajax'
export function getMe() {
return AJAX({
resource: 'me',
method: 'GET',
})
}
export const updateMe = async (url, updatedMe) => { export const updateMe = async (url, updatedMe) => {
try { try {
return await AJAX({ return await AJAX({

View File

@ -8,13 +8,6 @@ export function fetchLayouts() {
}) })
} }
export function getMe() {
return AJAX({
resource: 'me',
method: 'GET',
})
}
export function getSources() { export function getSources() {
return AJAX({ return AJAX({
resource: 'sources', resource: 'sources',

View File

@ -25,7 +25,7 @@ const authReducer = (state = initialState, action) => {
const {auth: {links}} = action.payload const {auth: {links}} = action.payload
return {...state, links, isAuthLoading: false} return {...state, links, isAuthLoading: false}
} }
case 'ME_REQUESTED': { case 'ME_GET_REQUESTED': {
return {...state, isMeLoading: true} return {...state, isMeLoading: true}
} }
case 'ME_RECEIVED__NON_AUTH': { case 'ME_RECEIVED__NON_AUTH': {