diff --git a/src/app/services/actions/auth-action-creators.ts b/src/app/services/actions/auth-action-creators.ts index fde0f7935..4c49e6643 100644 --- a/src/app/services/actions/auth-action-creators.ts +++ b/src/app/services/actions/auth-action-creators.ts @@ -1,7 +1,7 @@ import { authenticationWrapper } from '../../../modules/authentication'; import { IAction } from '../../../types/action'; import { Mode } from '../../../types/enums'; -import { GET_AUTH_TOKEN_SUCCESS, GET_CONSENTED_SCOPES_SUCCESS, LOGOUT_SUCCESS } from '../redux-constants'; +import { AUTHENTICATION_PENDING, GET_AUTH_TOKEN_SUCCESS, GET_CONSENTED_SCOPES_SUCCESS, LOGOUT_SUCCESS } from '../redux-constants'; export function getAuthTokenSuccess(response: boolean): any { return { @@ -9,6 +9,7 @@ export function getAuthTokenSuccess(response: boolean): any { response, }; } + export function getConsentedScopesSuccess(response: string[]): IAction { return { type: GET_CONSENTED_SCOPES_SUCCESS, @@ -23,15 +24,23 @@ export function signOutSuccess(response: boolean): any { }; } +export function setAuthenticationPending(response: boolean): any { + return { + type: AUTHENTICATION_PENDING, + response, + }; +} + export function signOut() { return (dispatch: Function, getState: Function) => { const { graphExplorerMode } = getState(); + dispatch(setAuthenticationPending(true)); if (graphExplorerMode === Mode.Complete) { authenticationWrapper.logOut(); } else { authenticationWrapper.logOutPopUp(); + dispatch(signOutSuccess(false)); } - dispatch(signOutSuccess(false)); }; } diff --git a/src/app/services/actions/query-action-creators.ts b/src/app/services/actions/query-action-creators.ts index 575838414..ffeb6d71f 100644 --- a/src/app/services/actions/query-action-creators.ts +++ b/src/app/services/actions/query-action-creators.ts @@ -14,7 +14,7 @@ import { addHistoryItem } from './request-history-action-creators'; export function runQuery(query: IQuery): Function { return (dispatch: Function, getState: Function) => { - const tokenPresent = getState().authToken; + const tokenPresent = !!getState()?.authToken?.token; const respHeaders: any = {}; const createdAt = new Date().toISOString(); diff --git a/src/app/services/reducers/auth-reducers.ts b/src/app/services/reducers/auth-reducers.ts index d5a7c9f81..d3bf7e92d 100644 --- a/src/app/services/reducers/auth-reducers.ts +++ b/src/app/services/reducers/auth-reducers.ts @@ -1,12 +1,33 @@ import { IAction } from '../../../types/action'; -import { GET_AUTH_TOKEN_SUCCESS, GET_CONSENTED_SCOPES_SUCCESS, LOGOUT_SUCCESS } from '../redux-constants'; +import { IAuthenticateResult } from '../../../types/authentication'; +import { + AUTHENTICATION_PENDING, GET_AUTH_TOKEN_SUCCESS, + GET_CONSENTED_SCOPES_SUCCESS, LOGOUT_SUCCESS +} from '../redux-constants'; -export function authToken(state = {}, action: IAction): string | object { + +const initialState: IAuthenticateResult = { + pending: false, + token: false +} + +export function authToken(state = initialState, action: IAction): IAuthenticateResult { switch (action.type) { case GET_AUTH_TOKEN_SUCCESS: - return action.response; + return { + token: true, + pending: false + }; case LOGOUT_SUCCESS: - return action.response; + return { + token: false, + pending: false + }; + case AUTHENTICATION_PENDING: + return { + token: true, + pending: true + }; default: return state; } diff --git a/src/app/services/redux-constants.ts b/src/app/services/redux-constants.ts index a5984d5a5..8d52c868f 100644 --- a/src/app/services/redux-constants.ts +++ b/src/app/services/redux-constants.ts @@ -41,3 +41,4 @@ export const AUTOCOMPLETE_FETCH_PENDING = 'AUTOCOMPLETE_FETCH_PENDING'; export const RESIZE_SUCCESS = 'RESIZE_SUCCESS'; export const RESPONSE_EXPANDED = 'RESPONSE_EXPANDED'; export const PERMISSIONS_PANEL_OPEN = 'PERMISSIONS_PANEL_OPEN'; +export const AUTHENTICATION_PENDING = 'AUTHENTICATION_PENDING'; diff --git a/src/app/views/App.tsx b/src/app/views/App.tsx index 12cd13794..f59c770ec 100644 --- a/src/app/views/App.tsx +++ b/src/app/views/App.tsx @@ -407,7 +407,7 @@ const mapStateToProps = ({ sidebarProperties, theme, termsOfUse, minimised: !mobileScreen && !showSidebar, sampleQuery, - authenticated: !!authToken + authenticated: !!authToken.token }; }; diff --git a/src/app/views/authentication/Authentication.tsx b/src/app/views/authentication/Authentication.tsx index cd39bb10b..e28bf6a94 100644 --- a/src/app/views/authentication/Authentication.tsx +++ b/src/app/views/authentication/Authentication.tsx @@ -1,54 +1,57 @@ import { SeverityLevel } from '@microsoft/applicationinsights-web'; import { Icon, Label, MessageBarType, Spinner, SpinnerSize, styled } from 'office-ui-fabric-react'; -import React, { Component } from 'react'; +import React, { useState } from 'react'; import { FormattedMessage, injectIntl } from 'react-intl'; -import { connect } from 'react-redux'; -import { bindActionCreators, Dispatch } from 'redux'; +import { useDispatch, useSelector } from 'react-redux'; import { authenticationWrapper } from '../../../modules/authentication'; import { componentNames, errorTypes, telemetry } from '../../../telemetry'; -import { IAuthenticationProps } from '../../../types/authentication'; import { Mode } from '../../../types/enums'; import { IRootState } from '../../../types/root'; -import * as authActionCreators from '../../services/actions/auth-action-creators'; -import * as queryStatusActionCreators from '../../services/actions/query-status-action-creator'; +import { getAuthTokenSuccess, getConsentedScopesSuccess } from '../../services/actions/auth-action-creators'; +import { setQueryResponseStatus } from '../../services/actions/query-status-action-creator'; import { translateMessage } from '../../utils/translate-messages'; import { classNames } from '../classnames'; import { showSignInButtonOrProfile } from './auth-util-components'; import { authenticationStyles } from './Authentication.styles'; -export class Authentication extends Component { - constructor(props: IAuthenticationProps) { - super(props); - this.state = { loginInProgress: false }; - } +const Authentication = (props: any) => { + const dispatch = useDispatch(); + const [loginInProgress, setLoginInProgress] = useState(false); + const { sidebarProperties, authToken, graphExplorerMode } = useSelector((state: IRootState) => state); + const mobileScreen = !!sidebarProperties.mobileScreen; + const showSidebar = !!sidebarProperties.showSidebar; + const tokenPresent = !!authToken.token; + const logoutInProgress = !!authToken.pending; + const minimised = !mobileScreen && !showSidebar; - public signIn = async (): Promise => { - const { - intl: { messages }, - }: any = this.props; - this.setState({ loginInProgress: true }); + const classes = classNames(props); + + const { + intl: { messages }, + }: any = props; + const signIn = async (): Promise => { + setLoginInProgress(true); try { const authResponse = await authenticationWrapper.logIn(); if (authResponse) { - this.setState({ loginInProgress: false }); - - this.props.actions!.signIn(authResponse.accessToken); - this.props.actions!.storeScopes(authResponse.scopes); + setLoginInProgress(false); + dispatch(getAuthTokenSuccess(!!authResponse.accessToken)) + dispatch(getConsentedScopesSuccess(authResponse.scopes)) } } catch (error) { const { errorCode } = error; - this.props.actions!.setQueryResponseStatus({ + dispatch(setQueryResponseStatus({ ok: false, statusText: messages['Authentication failed'], status: errorCode === 'popup_window_error' ? translateMessage('popup blocked, allow pop-up windows in your browser') : errorCode ? errorCode.replace('_', ' ') : '', messageType: MessageBarType.error - }); - this.setState({ loginInProgress: false }); + })); + setLoginInProgress(false); telemetry.trackException( new Error(errorTypes.OPERATIONAL_ERROR), SeverityLevel.Error, @@ -57,78 +60,50 @@ export class Authentication extends Component - {loginInProgress ? showLoginInProgressSpinner(classes, minimised) - : - mobileScreen ? showSignInButtonOrProfile(tokenPresent, mobileScreen, this.signIn, minimised) : - <> - {!tokenPresent && graphExplorerMode === Mode.Complete && !minimised && showUnAuthenticatedText(classes)} -
{showSignInButtonOrProfile(tokenPresent, mobileScreen, this.signIn, minimised)}
- } - - ); + const showProgressSpinner = (): React.ReactNode => { + return
+ + {!minimised && } +
; } -} - -function showUnAuthenticatedText(classes: any): React.ReactNode { - return <> - -
- - ; -} + const showUnAuthenticatedText = (): React.ReactNode => { + return <> + -function showLoginInProgressSpinner(classes: any, minimised: boolean): React.ReactNode { - return
- - {!minimised && } -
; -} +
+ + ; + } -function mapStateToProps({ sidebarProperties, theme, authToken, graphExplorerMode }: IRootState) { - const mobileScreen = !!sidebarProperties.mobileScreen; - const showSidebar = !!sidebarProperties.showSidebar; - return { - tokenPresent: !!authToken, - mobileScreen, - appTheme: theme, - minimised: !mobileScreen && !showSidebar, - graphExplorerMode - }; -} + if (logoutInProgress) { + return showProgressSpinner(); + } -function mapDispatchToProps(dispatch: Dispatch): object { - return { - actions: bindActionCreators({ - ...authActionCreators, - ...queryStatusActionCreators - }, - dispatch) - }; + return ( + <> + {loginInProgress ? showProgressSpinner() + : + mobileScreen ? showSignInButtonOrProfile(tokenPresent, mobileScreen, signIn, minimised) : + <> + {!tokenPresent && graphExplorerMode === Mode.Complete && !minimised && showUnAuthenticatedText()} +
{showSignInButtonOrProfile(tokenPresent, mobileScreen, signIn, minimised)}
+ } + + ) } // @ts-ignore const IntlAuthentication = injectIntl(Authentication); // @ts-ignore const StyledAuthentication = styled(IntlAuthentication, authenticationStyles); -export default connect( - mapStateToProps, - mapDispatchToProps -)(StyledAuthentication); +export default StyledAuthentication; diff --git a/src/app/views/query-runner/query-input/QueryInput.tsx b/src/app/views/query-runner/query-input/QueryInput.tsx index 2df70ec47..70c51ac1e 100644 --- a/src/app/views/query-runner/query-input/QueryInput.tsx +++ b/src/app/views/query-runner/query-input/QueryInput.tsx @@ -36,7 +36,7 @@ const QueryInput = (props: IQueryInputProps) => { const { sampleQuery, authToken, isLoadingData: submitting } = useSelector((state: IRootState) => state); - const authenticated = !!authToken; + const authenticated = !!authToken.token; const showError = !authenticated && sampleQuery.selectedVerb !== 'GET'; const verbSelector: any = queryRunnerStyles().verbSelector; diff --git a/src/app/views/query-runner/request/auth/Auth.tsx b/src/app/views/query-runner/request/auth/Auth.tsx index 433ef264c..d2cc39ab9 100644 --- a/src/app/views/query-runner/request/auth/Auth.tsx +++ b/src/app/views/query-runner/request/auth/Auth.tsx @@ -43,7 +43,7 @@ export function Auth(props: any) { iconName: 'code' }; - if (!authToken) { + if (!authToken.token) { return ; diff --git a/src/app/views/query-runner/request/permissions/PanelList.tsx b/src/app/views/query-runner/request/permissions/PanelList.tsx index 0d4c19918..49f2be12e 100644 --- a/src/app/views/query-runner/request/permissions/PanelList.tsx +++ b/src/app/views/query-runner/request/permissions/PanelList.tsx @@ -34,7 +34,7 @@ const PanelList = ({ messages, const { consentedScopes, scopes, authToken } = useSelector((state: IRootState) => state); const [permissions, setPermissions] = useState(scopes.data.sort(dynamicSort('value', SortOrder.ASC))); const permissionsList: any[] = []; - const tokenPresent = !!authToken; + const tokenPresent = !!authToken.token; setConsentedStatus(tokenPresent, permissions, consentedScopes); diff --git a/src/app/views/query-runner/request/permissions/Permission.tsx b/src/app/views/query-runner/request/permissions/Permission.tsx index 3fbf33e47..555e45caa 100644 --- a/src/app/views/query-runner/request/permissions/Permission.tsx +++ b/src/app/views/query-runner/request/permissions/Permission.tsx @@ -282,7 +282,7 @@ function mapStateToProps({ sampleQuery, scopes, authToken, consentedScopes, dime return { sample: sampleQuery, scopes, - tokenPresent: authToken, + tokenPresent: authToken.token, consentedScopes, dimensions }; diff --git a/src/app/views/query-runner/request/permissions/TabList.tsx b/src/app/views/query-runner/request/permissions/TabList.tsx index ec8726e9e..b5c42205e 100644 --- a/src/app/views/query-runner/request/permissions/TabList.tsx +++ b/src/app/views/query-runner/request/permissions/TabList.tsx @@ -20,7 +20,7 @@ const TabList = ({ columns, classes, renderItemColumn, renderDetailsHeader, maxH const dispatch = useDispatch(); const { consentedScopes, scopes, authToken } = useSelector((state: IRootState) => state); const permissions: IPermission[] = scopes.hasUrl ? scopes.data : []; - const tokenPresent = !!authToken; + const tokenPresent = !!authToken.token; setConsentedStatus(tokenPresent, permissions, consentedScopes); diff --git a/src/app/views/settings/Settings.tsx b/src/app/views/settings/Settings.tsx index 24c136cd8..821f45e1e 100644 --- a/src/app/views/settings/Settings.tsx +++ b/src/app/views/settings/Settings.tsx @@ -17,7 +17,7 @@ import React, { useEffect, useState } from 'react'; import { FormattedMessage, injectIntl } from 'react-intl'; import { useDispatch, useSelector } from 'react-redux'; -import '../../utils/string-operations'; +import '../../utils/string-operations'; import { geLocale } from '../../../appLocale'; import { componentNames, eventTypes, telemetry } from '../../../telemetry'; import { loadGETheme } from '../../../themes'; @@ -33,7 +33,8 @@ import { Permission } from '../query-runner/request/permissions'; function Settings(props: ISettingsProps) { const dispatch = useDispatch(); - const { permissionsPanelOpen } = useSelector((state: IRootState) => state); + const { permissionsPanelOpen, authToken, theme: appTheme } = useSelector((state: IRootState) => state); + const authenticated = authToken.token; const [themeChooserDialogHidden, hideThemeChooserDialog] = useState(true); const [items, setItems] = useState([]); const [selectedPermissions, setSelectedPermissions] = useState([]); @@ -42,9 +43,6 @@ function Settings(props: ISettingsProps) { intl: { messages } }: any = props; - const authenticated = useSelector((state: any) => (!!state.authToken)); - const appTheme = useSelector((state: any) => (state.theme)); - useEffect(() => { const menuItems: any = [ { diff --git a/src/app/views/sidebar/sample-queries/SampleQueries.tsx b/src/app/views/sidebar/sample-queries/SampleQueries.tsx index 45e05aa98..d49126f54 100644 --- a/src/app/views/sidebar/sample-queries/SampleQueries.tsx +++ b/src/app/views/sidebar/sample-queries/SampleQueries.tsx @@ -472,7 +472,7 @@ function displayTipMessage(actions: any, selectedQuery: ISampleQuery) { function mapStateToProps({ authToken, profile, samples, theme }: IRootState) { return { - tokenPresent: !!authToken, + tokenPresent: !!authToken.token, profile, samples, appTheme: theme diff --git a/src/index.tsx b/src/index.tsx index 35d7373b1..5daf9cd2d 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -53,7 +53,7 @@ const currentTheme = readTheme(); loadGETheme(currentTheme); const appState: any = store({ - authToken: '', + authToken: { token: false, pending: false }, consentedScopes: [], isLoadingData: false, profile: null, diff --git a/src/messages/GE.json b/src/messages/GE.json index 7a8ee2f4e..12fc2874f 100644 --- a/src/messages/GE.json +++ b/src/messages/GE.json @@ -351,5 +351,6 @@ "Click here to go to the next page": "Click here to go to the next page", "and experiment on": " and experiment on", "Scope consent failed": "Scope consent failed", - "Missing url": "Please enter a valid url to run the query" + "Missing url": "Please enter a valid url to run the query", + "Signing you out...": "Signing you out..." } \ No newline at end of file diff --git a/src/tests/services/reducers/auth-reducers.spec.tsx b/src/tests/services/reducers/auth-reducers.spec.tsx index 94916a7c8..90cb96c5c 100644 --- a/src/tests/services/reducers/auth-reducers.spec.tsx +++ b/src/tests/services/reducers/auth-reducers.spec.tsx @@ -3,7 +3,7 @@ import { GET_AUTH_TOKEN_SUCCESS } from '../../../app/services/redux-constants'; describe('Auth Reducer', () => { it('should return initial state', () => { - const initialState = {}; + const initialState = { token: false, pending: false }; const dummyAction = { type: 'Dummy', response: { displayName: 'Megan Bowen' } }; const newState = authToken(initialState, dummyAction); @@ -11,11 +11,11 @@ describe('Auth Reducer', () => { }); it('should handle GET_AUTH_TOKEN_SUCCESS', () => { - const initialState = ''; + const initialState = { token: false, pending: false }; - const queryAction = { type: GET_AUTH_TOKEN_SUCCESS, response: 'a token' }; + const queryAction = { type: GET_AUTH_TOKEN_SUCCESS, response: true }; const newState = authToken(initialState, queryAction); - expect(newState).toEqual('a token'); + expect(newState).toEqual({ token: true, pending: false }); }); }); diff --git a/src/types/authentication.ts b/src/types/authentication.ts index 1cbd9e554..c4710d974 100644 --- a/src/types/authentication.ts +++ b/src/types/authentication.ts @@ -13,7 +13,13 @@ export interface IAuthenticationProps { setQueryResponseStatus: Function; }; tokenPresent: boolean; + inProgress: boolean; mobileScreen: boolean; minimised: boolean; graphExplorerMode: Mode; } + +export interface IAuthenticateResult { + pending: boolean; + token: boolean; +} diff --git a/src/types/root.ts b/src/types/root.ts index 609d9310d..251f02f11 100644 --- a/src/types/root.ts +++ b/src/types/root.ts @@ -1,4 +1,5 @@ import { IAdaptiveCardResponse } from './adaptivecard'; +import { IAuthenticateResult } from './authentication'; import { IAutocompleteResponse } from './auto-complete'; import { IDimensions } from './dimensions'; import { Mode } from './enums'; @@ -20,7 +21,7 @@ export interface IRootState { sampleQuery: IQuery; termsOfUse: boolean; sidebarProperties: ISidebarProps; - authToken: string; + authToken: IAuthenticateResult; samples: ISampleQuery[]; consentedScopes: string[]; scopes: IScopes;