diff --git a/package-lock.json b/package-lock.json index 66da9c031..970428491 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1455,6 +1455,28 @@ "tslib": "^2.1.0" } }, + "@fluentui/react-icon-provider": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/@fluentui/react-icon-provider/-/react-icon-provider-1.3.6.tgz", + "integrity": "sha512-3wb0ajdkmGucN37uz9RAZMyeZucRcqTVGm6ilPaYufcX9YKBi+MezQAEJ7mGYlBmsz+VOF7WeIbyxwLB5y6oYA==", + "requires": { + "@fluentui/set-version": "^8.2.0", + "@fluentui/style-utilities": "^8.6.6", + "tslib": "^2.1.0" + } + }, + "@fluentui/react-icons-mdl2": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/@fluentui/react-icons-mdl2/-/react-icons-mdl2-1.3.8.tgz", + "integrity": "sha512-bD0MepBNNGMrHAzkH2+zrlsgZSsmkyj+WTSiUE32PNb1cX5nGWpBLBD8byhhKZusW8w6g0ePzqEBbzdyCKuTqw==", + "requires": { + "@fluentui/react-icon-provider": "^1.3.6", + "@fluentui/set-version": "^8.2.0", + "@fluentui/utilities": "^8.8.2", + "@microsoft/load-themed-styles": "^1.10.26", + "tslib": "^2.1.0" + } + }, "@fluentui/react-portal-compat-context": { "version": "9.0.0-rc.2", "resolved": "https://registry.npmjs.org/@fluentui/react-portal-compat-context/-/react-portal-compat-context-9.0.0-rc.2.tgz", diff --git a/package.json b/package.json index 20bb2b08a..af98c7dae 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "@babel/core": "7.17.9", "@babel/runtime": "7.18.3", "@fluentui/react": "8.72.1", + "@fluentui/react-icons-mdl2": "1.3.8", "@microsoft/applicationinsights-react-js": "3.3.4", "@microsoft/applicationinsights-web": "2.7.6", "@microsoft/microsoft-graph-client": "3.0.2", diff --git a/src/app/services/actions/profile-action-creators.ts b/src/app/services/actions/profile-action-creators.ts index 921f7798c..47ac3f07c 100644 --- a/src/app/services/actions/profile-action-creators.ts +++ b/src/app/services/actions/profile-action-creators.ts @@ -7,6 +7,7 @@ import { BETA_USER_INFO_URL, DEFAULT_USER_SCOPES, USER_INFO_URL, + USER_ORGANIZATION_URL, USER_PICTURE_URL } from '../graph-constants'; import { @@ -14,7 +15,6 @@ import { PROFILE_REQUEST_SUCCESS } from '../redux-constants'; import { makeGraphRequest, parseResponse } from './query-action-creator-util'; -import { queryRunningStatus } from './query-loading-action-creators'; interface IBetaProfile { ageGroup: number; @@ -54,13 +54,13 @@ const query: IQuery = { export function getProfileInfo(): Function { return async (dispatch: Function) => { - dispatch(queryRunningStatus(true)); try { const profile: IUser = await getProfileInformation(); const { profileType, ageGroup } = await getBetaProfile(); profile.profileType = profileType; profile.ageGroup = ageGroup; profile.profileImageUrl = await getProfileImage(); + profile.tenant= await getTenantInfo(profileType); dispatch(profileRequestSuccess(profile)); } catch (error) { dispatch(profileRequestError({ error })); @@ -75,7 +75,8 @@ export async function getProfileInformation(): Promise { emailAddress: '', profileImageUrl: '', profileType: ACCOUNT_TYPE.UNDEFINED, - ageGroup: 0 + ageGroup: 0, + tenant: '' }; try { query.sampleUrl = USER_INFO_URL; @@ -148,3 +149,19 @@ export async function getProfileResponse(): Promise { response }; } + +export async function getTenantInfo(profileType: ACCOUNT_TYPE) { + if(profileType===ACCOUNT_TYPE.AAD) { + try{ + query.sampleUrl = USER_ORGANIZATION_URL; + const { userInfo: tenant } = await getProfileResponse(); + return tenant.value[0]?.displayName; + } catch (error: any) { + return ''; + } + } + if(profileType===ACCOUNT_TYPE.MSA) { + return 'Personal'; + } + return ''; +} diff --git a/src/app/services/graph-constants.ts b/src/app/services/graph-constants.ts index 0079db865..613268bc4 100644 --- a/src/app/services/graph-constants.ts +++ b/src/app/services/graph-constants.ts @@ -26,3 +26,4 @@ export const ADAPTIVE_CARD_URL = export const GRAPH_TOOOLKIT_EXAMPLE_URL = 'https://mgt.dev/?path=/story'; export const MOZILLA_CORS_DOCUMENTATION_LINK = 'https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS'; +export const USER_ORGANIZATION_URL = `${GRAPH_URL}/v1.0/organization`; diff --git a/src/app/services/reducers/dimensions-reducers.ts b/src/app/services/reducers/dimensions-reducers.ts index 6427fb94e..8a46bba49 100644 --- a/src/app/services/reducers/dimensions-reducers.ts +++ b/src/app/services/reducers/dimensions-reducers.ts @@ -5,11 +5,11 @@ import { RESIZE_SUCCESS } from '../redux-constants'; const initialState: IDimensions = { request: { width: '100%', - height: '40%' + height: '38%' }, response: { width: '100%', - height: '60%' + height: '50%' }, sidebar: { width: '26%', diff --git a/src/app/views/App.styles.ts b/src/app/views/App.styles.ts index 6efc31064..2f58ee038 100644 --- a/src/app/views/App.styles.ts +++ b/src/app/views/App.styles.ts @@ -6,10 +6,10 @@ export const appStyles = (theme: ITheme) => { background: theme.semanticColors.bodyBackground, color: theme.semanticColors.bodyText, paddingTop: theme.spacing.s1, - width: '100%', height: '100%', paddingRight: '15px', paddingLeft: '4px', + paddingBottom: '5px', marginLeft: 'auto', marginRight: 'auto' }, @@ -24,13 +24,16 @@ export const appStyles = (theme: ITheme) => { sidebar: { background: theme.palette.neutralLighter, paddingRight: 10, - marginRight: 10 + paddingLeft: 10, + marginRight: 10, + marginBottom: 9 }, sidebarMini: { background: theme.palette.neutralLighter, maxWidth: '65px', minWidth: '55px', - padding: 10 + padding: 10, + marginBottom: 9 }, layoutExtra: { minWidth: '95%', @@ -69,7 +72,9 @@ export const appStyles = (theme: ITheme) => { marginTop: 5 }, statusAreaFullScreen: { - marginTop: 10 + marginTop: 6, + position: 'relative', + bottom: 0 }, vResizeHandle: { zIndex: 1, diff --git a/src/app/views/App.tsx b/src/app/views/App.tsx index 8f7e59983..17b75abb3 100644 --- a/src/app/views/App.tsx +++ b/src/app/views/App.tsx @@ -1,4 +1,4 @@ -import { Announced, getTheme, IStackTokens, ITheme, styled } from '@fluentui/react'; +import { Announced, getTheme, ITheme, styled } from '@fluentui/react'; import { Resizable } from 're-resizable'; import React, { Component } from 'react'; import { injectIntl } from 'react-intl'; @@ -24,24 +24,18 @@ import { GRAPH_URL } from '../services/graph-constants'; import { parseSampleUrl } from '../utils/sample-url-generation'; import { substituteTokens } from '../utils/token-helpers'; import { translateMessage } from '../utils/translate-messages'; -import { - appTitleDisplayOnFullScreen, - appTitleDisplayOnMobileScreen -} from './app-sections/AppTitle'; import { headerMessaging } from './app-sections/HeaderMessaging'; import { StatusMessages, TermsOfUseMessage } from './app-sections'; import { appStyles } from './App.styles'; -import { Authentication } from './authentication'; import { classNames } from './classnames'; import { createShareLink } from './common/share'; import { QueryResponse } from './query-response'; import { QueryRunner } from './query-runner'; import { parse } from './query-runner/util/iframe-message-parser'; -import { Settings } from './settings'; import { Sidebar } from './sidebar/Sidebar'; -import { FeedbackButton } from './app-sections/FeedbackButton'; +import { MainHeader } from './main-header/MainHeader'; -interface IAppProps { +export interface IAppProps { theme?: ITheme; styles?: object; intl: any; @@ -295,30 +289,6 @@ class App extends Component { }; - public displayAuthenticationSection = (minimised: boolean) => { - return ( -
-
- -
- {minimised && -
- -
- } -
- -
-
- ); - }; - private setSidebarProperties() { const { sidebarProperties } = this.props; const properties = { ...sidebarProperties }; @@ -409,11 +379,6 @@ class App extends Component { mobileScreen, showSidebar }); - const stackTokens: IStackTokens = { - childrenGap: 10, - padding: 10 - }; - if (mobileScreen) { layout = sidebarWidth = 'ms-Grid-col ms-sm12'; sideWidth = '100%'; @@ -431,6 +396,11 @@ class App extends Component { // @ts-ignore
+ { marginRight: showSidebar || (graphExplorerMode === Mode.TryIt) && '-20px', flexDirection: (graphExplorerMode === Mode.TryIt) ? 'column' : 'row' }}> - {graphExplorerMode === Mode.Complete && ( { @@ -452,7 +421,7 @@ class App extends Component { } }} className={`ms-Grid-col ms-sm12 ms-md4 ms-lg4 ${sidebarWidth} resizable-sidebar`} - minWidth={'4'} + minWidth={'71'} maxWidth={maxWidth} enable={{ right: true @@ -460,32 +429,16 @@ class App extends Component { handleClasses={{ right: classes.vResizeHandle }} - bounds={'window'} + bounds={'parent'} size={{ width: sideWidth, height: '' }} > - - {mobileScreen && appTitleDisplayOnMobileScreen( - stackTokens, - classes, - this.toggleSidebar - )} - - {!mobileScreen && appTitleDisplayOnFullScreen( - classes, - minimised, - this.toggleSidebar - )} - -
- - {this.displayAuthenticationSection(minimised)} -
- - {showSidebar && ( ) } +
)} {graphExplorerMode === Mode.TryIt && @@ -502,13 +455,10 @@ class App extends Component { width: graphExplorerMode === Mode.TryIt ? '100%' : contentWidth, height: contentHeight }} - style={!sidebarProperties.showSidebar && !mobileScreen ? { marginLeft: '8px' } : {}} + style={!sidebarProperties.showSidebar && !mobileScreen ? { marginLeft: '8px', flex: 1 } : {flex: 1}} > -
+
-
- -
@@ -520,6 +470,9 @@ class App extends Component { )}
+
+ +
); diff --git a/src/app/views/app-sections/AppTitle.spec.tsx b/src/app/views/app-sections/AppTitle.spec.tsx deleted file mode 100644 index bbbd19b35..000000000 --- a/src/app/views/app-sections/AppTitle.spec.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import React from 'react'; -import { cleanup, render, screen } from '@testing-library/react'; - -import { appTitleDisplayOnMobileScreen, appTitleDisplayOnFullScreen } from './AppTitle'; - -afterEach(cleanup) -const renderTitle = () => { - const stackTokens = { - childrenGap: 10, - padding: 10 - } - const classes_ = jest.fn(); - return render( -
- {appTitleDisplayOnMobileScreen(stackTokens, classes_, jest.fn())} -
- ) -} - -const renderTitleOnFullScreen = (args?: any) => { - const classes_ = jest.fn(); - const mimimised = args?.minimised ? args?.minimised : false; - - return render( -
- {appTitleDisplayOnFullScreen(classes_, mimimised, jest.fn())} -
- ) -} - -jest.mock('@microsoft/applicationinsights-react-js', () => ({ - // eslint-disable-next-line react/display-name - withAITracking: () => React.Component, - ReactPlugin: Object -})) - -jest.mock('@ms-ofb/officebrowserfeedbacknpm/scripts/app/Window/Window', () => ({ - OfficeBrowserFeedback: Object -})) - -jest.mock('@ms-ofb/officebrowserfeedbacknpm/Floodgate', () => ({ - makeFloodgate: Object -})) - -jest.mock('@ms-ofb/officebrowserfeedbacknpm/scripts/app/Configuration/IInitOptions', () => ({ - AuthenticationType: 0 -})) - -// eslint-disable-next-line react/display-name -jest.mock('../query-runner/request/feedback/FeedbackForm.tsx', () => () => { - return
Feedback
-}); - -jest.mock('react-redux', () => ({ - useSelector: jest.fn(() => { - return { - profile: { - profileType: 'MSA' - } - } - }), - useDispatch: jest.fn() -})); - -// eslint-disable-next-line no-console -console.warn = jest.fn() - -describe('It should render the app title section in mobile screen size', () => { - it('Renders app title section', () => { - const { getByText } = renderTitle(); - getByText('Graph Explorer'); - }); - - it('Renders app title section in full screen without crashing', () => { - const { getByText } = renderTitleOnFullScreen({ minimised: false }); - getByText('Graph Explorer'); - }); - - it('Renders app title section in minimised mode without crashing', () => { - renderTitleOnFullScreen({ minimised: true }); - expect(screen.getByRole('heading')).toBeDefined(); - }) -}) - diff --git a/src/app/views/app-sections/AppTitle.tsx b/src/app/views/app-sections/AppTitle.tsx deleted file mode 100644 index 2a96ed2f2..000000000 --- a/src/app/views/app-sections/AppTitle.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import { getId, IconButton, IStackTokens, Label, Stack, TooltipHost } from '@fluentui/react'; -import React from 'react'; -import { FormattedMessage } from 'react-intl'; -import { FeedbackButton } from './FeedbackButton'; - -export function appTitleDisplayOnFullScreen( - classes: any, - minimised: any, - toggleSidebar: Function, -): React.ReactNode{ - - return
- -
- } - }}> - toggleSidebar()} /> - -
- {!minimised && - <> - {displayGraphLabel(classes)} - } -
-
- {!minimised && -
- -
- } -
-
; -} - -export function appTitleDisplayOnMobileScreen( - stackTokens: IStackTokens, - classes: any, - toggleSidebar: Function -): React.ReactNode { - return - <> - toggleSidebar()} - /> -
- {displayGraphLabel(classes)} -
-
- -
- -
; -} - -function displayGraphLabel(classes: any): React.ReactNode { - return ( - - ) -} diff --git a/src/app/views/app-sections/HeaderMessaging.tsx b/src/app/views/app-sections/HeaderMessaging.tsx index c082b94f0..469d085dc 100644 --- a/src/app/views/app-sections/HeaderMessaging.tsx +++ b/src/app/views/app-sections/HeaderMessaging.tsx @@ -4,13 +4,12 @@ import { FormattedMessage } from 'react-intl'; import { getLoginType } from '../../../modules/authentication/authUtils'; import { LoginType } from '../../../types/enums'; -import { Authentication } from '../authentication'; export function headerMessaging(classes: any, query: string): React.ReactNode { const loginType = getLoginType(); return ( -
+
{loginType === LoginType.Popup && <>

@@ -24,7 +23,6 @@ export function headerMessaging(classes: any, query: string): React.ReactNode {

- } {loginType === LoginType.Redirect &&

diff --git a/src/app/views/authentication/Authentication.styles.ts b/src/app/views/authentication/Authentication.styles.ts index 0d87e4503..c73c98c3a 100644 --- a/src/app/views/authentication/Authentication.styles.ts +++ b/src/app/views/authentication/Authentication.styles.ts @@ -14,11 +14,19 @@ export const authenticationStyles = (theme: ITheme) => { alignItems: 'flex-start', justifyContent: 'start', marginRight: theme.spacing.s1, - padding: theme.spacing.s1 + padding: theme.spacing.s1, + position: 'relative', + top: '1px' }, spinnerContainer: { display: 'flex', flexDirection: 'row' + }, + loginProgressLabelStyles: { + root: { + position: 'relative', + top: '6px' + } } }; }; diff --git a/src/app/views/authentication/Authentication.tsx b/src/app/views/authentication/Authentication.tsx index fd5137a4e..3b3824db9 100644 --- a/src/app/views/authentication/Authentication.tsx +++ b/src/app/views/authentication/Authentication.tsx @@ -1,11 +1,10 @@ import { SeverityLevel } from '@microsoft/applicationinsights-web'; -import { Icon, Label, MessageBar, MessageBarType, Spinner, SpinnerSize, styled } from '@fluentui/react'; +import { MessageBarType, Spinner, SpinnerSize, styled } from '@fluentui/react'; import React, { useState } from 'react'; -import { FormattedMessage, injectIntl } from 'react-intl'; +import { injectIntl } from 'react-intl'; import { useDispatch, useSelector } from 'react-redux'; import { authenticationWrapper } from '../../../modules/authentication'; import { componentNames, errorTypes, telemetry } from '../../../telemetry'; -import { Mode } from '../../../types/enums'; import { IRootState } from '../../../types/root'; import { getAuthTokenSuccess, getConsentedScopesSuccess } from '../../services/actions/auth-action-creators'; import { setQueryResponseStatus } from '../../services/actions/query-status-action-creator'; @@ -17,14 +16,11 @@ import { getSignInAuthErrorHint, signInAuthError } from '../../../modules/authen const Authentication = (props: any) => { const dispatch = useDispatch(); const [loginInProgress, setLoginInProgress] = useState(false); - const { sidebarProperties, authToken, graphExplorerMode } = useSelector( + const { authToken } = useSelector( (state: IRootState) => state ); - const mobileScreen = !!sidebarProperties.mobileScreen; - const showSidebar = !!sidebarProperties.showSidebar; const tokenPresent = !!authToken.token; const logoutInProgress = !!authToken.pending; - const minimised = !mobileScreen && !showSidebar; const classes = classNames(props); @@ -67,6 +63,21 @@ const Authentication = (props: any) => { } }; + const signInWithOther = async (): Promise => { + setLoginInProgress(true); + try{ + const authResponse = await authenticationWrapper.logInWithOther(); + if (authResponse) { + setLoginInProgress(false); + dispatch(getAuthTokenSuccess(!!authResponse.accessToken)); + dispatch(getConsentedScopesSuccess(authResponse.scopes)); + } + } catch(error: any) { + setLoginInProgress(false); + } + } + + const removeUnderScore = (statusString: string): string => { return statusString ? statusString.replace(/_/g, ' ') : statusString; } @@ -75,34 +86,10 @@ const Authentication = (props: any) => { return (

- {!minimised && ( - - )}
); }; - const showUnAuthenticatedText = (): React.ReactNode => { - return ( - <> - - -
- - {' '} - - - - ); - }; - if (logoutInProgress) { return showProgressSpinner(); } @@ -111,22 +98,13 @@ const Authentication = (props: any) => { <> {loginInProgress ? ( showProgressSpinner() - ) : mobileScreen ? ( - showSignInButtonOrProfile(tokenPresent, mobileScreen, signIn, minimised) ) : ( <> - {!tokenPresent && - graphExplorerMode === Mode.Complete && - !minimised && - showUnAuthenticatedText()} -
{showSignInButtonOrProfile( tokenPresent, - mobileScreen, signIn, - minimised + signInWithOther )} -
)} diff --git a/src/app/views/authentication/auth-util-components/ProfileButton.styles.ts b/src/app/views/authentication/auth-util-components/ProfileButton.styles.ts new file mode 100644 index 000000000..129a4c9d8 --- /dev/null +++ b/src/app/views/authentication/auth-util-components/ProfileButton.styles.ts @@ -0,0 +1,18 @@ +import { ITheme } from '@fluentui/react' + +export const profileButtonStyles = (theme: ITheme) => { + return { + actionButtonStyles: { + root: { + height: 50, + width: 50, + ':hover': { + background: `${theme.palette.neutralLight} !important` + } + }, + icon: { + flex: 1 + } + } + } +} \ No newline at end of file diff --git a/src/app/views/authentication/auth-util-components/ProfileButton.tsx b/src/app/views/authentication/auth-util-components/ProfileButton.tsx new file mode 100644 index 000000000..65f7d7b8e --- /dev/null +++ b/src/app/views/authentication/auth-util-components/ProfileButton.tsx @@ -0,0 +1,39 @@ +import { ActionButton, getId, getTheme, TooltipHost } from '@fluentui/react'; +import React from 'react'; +import { translateMessage } from '../../../utils/translate-messages'; +import Profile from '../profile/Profile'; +import { profileButtonStyles } from './ProfileButton.styles'; + +export function showSignInButtonOrProfile( + tokenPresent: boolean, + signIn: Function, + signInWithOther: Function +) { + + const currentTheme = getTheme(); + const { actionButtonStyles } = profileButtonStyles(currentTheme); + const signInButton = + + {translateMessage('sign in')} +
} + id={getId()} + calloutProps={{ gapSpace: 0 }} + > + signIn()} + styles={actionButtonStyles} + /> + + + return ( + <> + {!tokenPresent && signInButton} + {tokenPresent && } + + ); +} diff --git a/src/app/views/authentication/auth-util-components/UtilComponents.tsx b/src/app/views/authentication/auth-util-components/UtilComponents.tsx deleted file mode 100644 index 59e382fc5..000000000 --- a/src/app/views/authentication/auth-util-components/UtilComponents.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { IconButton, PrimaryButton } from '@fluentui/react'; -import React from 'react'; -import { FormattedMessage } from 'react-intl'; -import { translateMessage } from '../../../utils/translate-messages'; -import Profile from '../profile/Profile'; - -export function showSignInButtonOrProfile( - tokenPresent: boolean, - mobileScreen: boolean, - signIn: Function, - minimised: boolean -) { - - const signInButton = minimised ? signIn()} /> : signIn()} - > - {!mobileScreen && } - ; - - return ( -
- {!tokenPresent && signInButton} - {tokenPresent && } -
- ); -} diff --git a/src/app/views/authentication/auth-util-components/index.tsx b/src/app/views/authentication/auth-util-components/index.tsx index 992b5fcad..39f4d1bd4 100644 --- a/src/app/views/authentication/auth-util-components/index.tsx +++ b/src/app/views/authentication/auth-util-components/index.tsx @@ -1 +1 @@ -export { showSignInButtonOrProfile } from './UtilComponents'; +export { showSignInButtonOrProfile } from './ProfileButton'; diff --git a/src/app/views/authentication/profile/Profile.styles.ts b/src/app/views/authentication/profile/Profile.styles.ts new file mode 100644 index 000000000..129e72a40 --- /dev/null +++ b/src/app/views/authentication/profile/Profile.styles.ts @@ -0,0 +1,76 @@ +import { ITheme } from '@fluentui/react' + +export const profileStyles = (theme: ITheme) => { + return { + linkStyles: { + root: { + marginRight: '50px', + height: '24px', + color: `${theme.palette.black} !important`, + '&:hover': { textDecoration: 'none', color: `${theme.palette.themeDarkAlt} !important` } + } + }, + personaStyleToken: { + primaryText: { + paddingBottom: 5 + }, + secondaryText: + { + paddingBottom: 10, + textTransform: 'lowercase' + }, + root: { + height: '100%', + paddingLeft: '3px' + } + }, + profileSpinnerStyles: { + root: { + position: 'relative' as 'relative', + top: '2px' + } + }, + permissionsLabelStyles: { + root: { + position: 'relative' as 'relative', + bottom: '25px', + left: '92px', + textDecoration: 'underline', + color: `${theme.palette.themePrimary} !important` + } + }, + personaButtonStyles: { + root: { + ':hover': { + background: `${theme.palette.neutralLight} !important` + }, + height: '100%', + flex: 1, + display: 'flex', + alignItems: 'stretch' + } + }, + profileContainerStyles: { + display: 'flex', + alignItems: 'stretch', + flex: 1, + height: '100%' + }, + permissionPanelStyles: { + footer: { + backgroundColor: theme.palette.white + }, + commands: { + backgroundColor: theme.palette.white + } + }, + inactiveConsentStyles: { + marginRight: 10, + backgroundColor: theme.palette.themeSecondary + }, + activeConsentStyles: { + marginRight: 10, + backgroundColor: theme.palette.themeDarker + } + } +} \ No newline at end of file diff --git a/src/app/views/authentication/profile/Profile.tsx b/src/app/views/authentication/profile/Profile.tsx index bfe3548cc..3d2750702 100644 --- a/src/app/views/authentication/profile/Profile.tsx +++ b/src/app/views/authentication/profile/Profile.tsx @@ -1,54 +1,95 @@ -import { ActionButton, IPersonaSharedProps, Persona, PersonaSize, Spinner, SpinnerSize, styled } from '@fluentui/react'; -import React, { useEffect } from 'react'; +import { + ActionButton, + Callout, + DefaultButton, + FontSizes, + getTheme, + IOverlayProps, + IPersonaProps, + IPersonaSharedProps, + Label, + mergeStyleSets, + Panel, + PanelType, + Persona, + PersonaSize, + PrimaryButton, + Spinner, + SpinnerSize, + Stack, + styled +} from '@fluentui/react'; +import React, { useEffect, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; +import { useId } from '@fluentui/react-hooks'; -import { geLocale } from '../../../../appLocale'; -import { Mode } from '../../../../types/enums'; +import { componentNames, eventTypes, telemetry } from '../../../../telemetry'; import { IRootState } from '../../../../types/root'; import { signOut } from '../../../services/actions/auth-action-creators'; +import { consentToScopes } from '../../../services/actions/permissions-action-creator'; +import { togglePermissionsPanel } from '../../../services/actions/permissions-panel-action-creator'; import { getProfileInfo } from '../../../services/actions/profile-action-creators'; import { translateMessage } from '../../../utils/translate-messages'; import { classNames } from '../../classnames'; +import { Permission } from '../../query-runner/request/permissions'; import { authenticationStyles } from '../Authentication.styles'; +import { profileStyles } from './Profile.styles'; +import { Mode } from '../../../../types/enums'; + +const getInitials = (name: string) => { + let initials = ''; + if (name && name !== '') { + const n = name.indexOf('('); + name = name.substring(0, n !== -1 ? n : name.length); + const parts = name.split(' '); + for (const part of parts) { + if (part.length > 0 && part !== '') { + initials += part[0]; + } + } + initials = initials.substring(0, 2); + } + return initials; +}; const Profile = (props: any) => { const dispatch = useDispatch(); - const { sidebarProperties, profile, authToken, graphExplorerMode } = useSelector((state: IRootState) => state); - const mobileScreen = !!sidebarProperties.mobileScreen; - const showSidebar = !!sidebarProperties.showSidebar; - const minimised = !mobileScreen && !showSidebar; + const { + profile, + authToken, + permissionsPanelOpen, + graphExplorerMode + } = useSelector((state: IRootState) => state); const authenticated = authToken.token; + const [selectedPermissions, setSelectedPermissions] = useState([]); + const [isCalloutVisible, setIsCalloutVisible] = useState(false); + const toggleIsCalloutVisible = () => { setIsCalloutVisible(!isCalloutVisible) }; + const buttonId = useId('callout-button'); + const labelId = useId('callout-label'); + const descriptionId = useId('callout-description'); + const theme = getTheme(); + const { personaStyleToken , profileSpinnerStyles, permissionsLabelStyles, inactiveConsentStyles, + personaButtonStyles, profileContainerStyles, permissionPanelStyles, activeConsentStyles } = profileStyles(theme); useEffect(() => { - if (authenticated && !profile) { + if (authenticated) { dispatch(getProfileInfo()); } }, [authenticated]); if (!profile) { - return (); + return (); } - const getInitials = (name: string) => { - let initials = ''; - if (name && name !== '') { - const n = name.indexOf('('); - name = name.substring(0, n !== -1 ? n : name.length); - const parts = name.split(' '); - for (const part of parts) { - if (part.length > 0 && part !== '') { - initials += part[0]; - } - } - initials = initials.substring(0, 2); - } - return initials; - }; - const handleSignOut = () => { dispatch(signOut()); } + + const handleSignInOther = async () => { + props.signInWithOther(); + } + const persona: IPersonaSharedProps = { imageUrl: profile.profileImageUrl, imageInitials: getInitials(profile.displayName), @@ -56,74 +97,167 @@ const Profile = (props: any) => { secondaryText: profile.emailAddress }; - const classes = classNames(props); + const changePanelState = () => { + let open = !!permissionsPanelOpen; + open = !open; + dispatch(togglePermissionsPanel(open)); + setSelectedPermissions([]); + trackSelectPermissionsButtonClickEvent(); + }; - const menuProperties = { - shouldFocusOnMount: true, - alignTargetEdge: true, - items: [ - { - key: 'office-dev-program', - text: translateMessage('Office Dev Program'), - href: `https://developer.microsoft.com/${geLocale}/office/dev-program`, - target: '_blank', - iconProps: { - iconName: 'CommandPrompt' - } - }, - { - key: 'sign-out', - text: translateMessage('sign out'), - onClick: () => handleSignOut(), - iconProps: { - iconName: 'SignOut' - } - } - ] + const trackSelectPermissionsButtonClickEvent = () => { + telemetry.trackEvent(eventTypes.BUTTON_CLICK_EVENT, { + ComponentName: componentNames.VIEW_ALL_PERMISSIONS_BUTTON + }); }; - const personaStyleToken: any = { - primaryText: { - paddingBottom: 5 - }, - secondaryText: - { - paddingBottom: 10, - textTransform: 'lowercase' - } + const setPermissions = (permissions: []) => { + setSelectedPermissions(permissions); + }; + + const handleConsent = () => { + dispatch(consentToScopes(selectedPermissions)); + setSelectedPermissions([]); }; - const defaultSize = minimised ? PersonaSize.size32 : PersonaSize.size48; + const getSelectionDetails = () => { + const selectionCount = selectedPermissions.length; - const profileProperties = { - persona, - styles: personaStyleToken, - hidePersonaDetails: minimised, - size: graphExplorerMode === Mode.TryIt ? PersonaSize.size40 : defaultSize + switch (selectionCount) { + case 0: + return ''; + case 1: + return `1 ${translateMessage('selected')}: ` + selectedPermissions[0]; + default: + return `${selectionCount} ${translateMessage('selected')}`; + } }; + const onRenderFooterContent = () => { + return ( +
+ + handleConsent()} + style={(selectedPermissions.length === 0) ? activeConsentStyles: inactiveConsentStyles} + > + {translateMessage('Consent')} + + changePanelState()}> + {translateMessage('Cancel')} + +
+ ); + }; + + const classes = classNames(props); + + const panelOverlayProps: IOverlayProps = { + isDarkThemed: true + } + const onRenderSecondaryText = (prop: IPersonaProps): JSX.Element => { + return ( + + {prop.secondaryText} + + ); + } + + const showProfileComponent = (userPersona: any): React.ReactNode => { + + const smallPersona = ; + + const fullPersona = + + return (<> + + {smallPersona} + + + {isCalloutVisible && ( + + + {profile && + + } + handleSignOut()}> + {translateMessage('sign out')} + + + {fullPersona} + {graphExplorerMode === Mode.Complete && + changePanelState()} styles={permissionsLabelStyles}> + {translateMessage('view all permissions')} + + } + + handleSignInOther()} + iconProps={{iconName: 'AddFriend'}} + > + {translateMessage('sign in other account')} + + + + + )} + + ) + } + return ( -
- {showProfileComponent(profileProperties, graphExplorerMode, menuProperties)} +
+ {showProfileComponent(persona)} + changePanelState()} + type={PanelType.medium} + hasCloseButton={true} + headerText={translateMessage('Permissions')} + onRenderFooterContent={onRenderFooterContent} + isFooterAtBottom={true} + closeButtonAriaLabel='Close' + overlayProps={panelOverlayProps} + styles={permissionPanelStyles} + > + +
); } -function showProfileComponent(profileProperties: any, graphExplorerMode: Mode, menuProperties: any): React.ReactNode { - const persona = ; - - if (graphExplorerMode === Mode.TryIt) { - return - {persona} - ; +const styles = mergeStyleSets({ + callout: { + width: 320, + maxWidth: '90%' } - - return persona; -} +}); // @ts-ignore const styledProfile = styled(Profile, authenticationStyles); diff --git a/src/app/views/app-sections/FeedbackButton.spec.tsx b/src/app/views/main-header/FeedbackButton.spec.tsx similarity index 100% rename from src/app/views/app-sections/FeedbackButton.spec.tsx rename to src/app/views/main-header/FeedbackButton.spec.tsx diff --git a/src/app/views/app-sections/FeedbackButton.tsx b/src/app/views/main-header/FeedbackButton.tsx similarity index 86% rename from src/app/views/app-sections/FeedbackButton.tsx rename to src/app/views/main-header/FeedbackButton.tsx index cc8e0e183..d36a6d618 100644 --- a/src/app/views/app-sections/FeedbackButton.tsx +++ b/src/app/views/main-header/FeedbackButton.tsx @@ -1,4 +1,4 @@ -import { DirectionalHint, IconButton, IIconProps, TooltipHost } from '@fluentui/react'; +import { getTheme, IconButton, IIconProps, TooltipHost } from '@fluentui/react'; import React, { useState } from 'react'; import { translateMessage } from '../../utils/translate-messages'; import { useSelector } from 'react-redux'; @@ -9,6 +9,7 @@ import { ACCOUNT_TYPE } from '../../services/graph-constants'; export const FeedbackButton = () => { const [enableSurvey, setEnableSurvey] = useState(false); const { profile } = useSelector( (state: IRootState) => state ); + const currentTheme = getTheme(); const feedbackIcon : IIconProps = { iconName : 'Feedback' @@ -19,7 +20,11 @@ export const FeedbackButton = () => { const feedbackIconStyles = { root:{ height: '50px', - width: '50px' + width: '50px', + marginTop: '-8px', + ':hover': { + background: `${currentTheme.palette.neutralLight} !important` + } } } const calloutProps = { @@ -46,7 +51,6 @@ export const FeedbackButton = () => { content={content} calloutProps={calloutProps} styles={hostStyles} - directionalHint={DirectionalHint.leftCenter} > { + const { authToken } = useSelector((state: IRootState) => state); + const authenticated = authToken.token; + const [items, setItems] = useState([]); + const currentTheme = getTheme(); + + registerIcons({ + icons: { + GitHubLogo: + } + }); + + useEffect(() => { + const menuItems: any = [ + { + key: 'report-issue', + text: translateMessage('Report an Issue'), + href: 'https://github.com/microsoftgraph/microsoft-graph-explorer-v4/issues/new/choose', + target: '_blank', + iconProps: { + iconName: 'ReportWarning' + }, + onClick: () => trackLinkClickEvents(componentNames.REPORT_AN_ISSUE_LINK) + }, + { key: 'divider_1', itemType: ContextualMenuItemType.Divider }, + { + key: 'ge-documentation', + text: translateMessage('Get started with Graph Explorer'), + href: 'https://docs.microsoft.com/en-us/graph/graph-explorer/graph-explorer-overview?view=graph-rest-1.0', + target: '_blank', + iconProps: { + iconName: 'TextDocument' + }, + onClick: () => trackLinkClickEvents(componentNames.GE_DOCUMENTATION_LINK) + }, + { + key: 'graph-documentation', + text: translateMessage('Graph Documentation'), + href: ' https://docs.microsoft.com/en-us/graph/api/overview?view=graph-rest-1.0', + target: '_blank', + iconProps: { + iconName: 'Documentation' + }, + onClick: () => trackLinkClickEvents(componentNames.GRAPH_DOCUMENTATION_LINK) + }, + { + key: 'github', + text: 'GitHub', + href: 'https://github.com/microsoftgraph/microsoft-graph-explorer-v4#readme', + target: '_blank', + iconProps: { + iconName: 'GitHubLogo', + styles: { + root: { + position: 'relative', + top: '-2px' + } + } + }, + onClick: () => trackLinkClickEvents(componentNames.GITHUB_LINK) + } + ]; + setItems(menuItems); + }, [authenticated]); + + const trackLinkClickEvents = (componentName: string) => { + telemetry.trackEvent(eventTypes.LINK_CLICK_EVENT, { + ComponentName: componentName + }); + }; + + const calloutStyles: React.CSSProperties = { + overflowY: 'hidden' + } + const { iconButton: helpButtonStyles, tooltipStyles, helpContainerStyles } = mainHeaderStyles(currentTheme); + + const menuProperties: IContextualMenuProps = { + shouldFocusOnMount: true, + alignTargetEdge: true, + items, + directionalHint: DirectionalHint.bottomLeftEdge, + calloutProps: { + style: calloutStyles + }, + styles:{container: {border: '1px solid' + currentTheme.palette.neutralTertiary}} + }; + + return ( +
+ + {translateMessage('Help')} +
} + id={getId()} + calloutProps={{ gapSpace: 0 }} + styles={ tooltipStyles } + > + + +
+ ); +} + diff --git a/src/app/views/main-header/MainHeader.styles.ts b/src/app/views/main-header/MainHeader.styles.ts new file mode 100644 index 000000000..a1fd9352f --- /dev/null +++ b/src/app/views/main-header/MainHeader.styles.ts @@ -0,0 +1,72 @@ +import { FontSizes, FontWeights, ITheme } from '@fluentui/react'; + +export const mainHeaderStyles = (theme: ITheme, mobileScreen?: boolean) => { + return { + rootStyles: { + root: { + background: theme.palette.neutralLighter, + height: 50, + marginBottom: '9px' + } + }, + rightItemsStyles: { + root: { + alignItems: 'center', + flexBasis: mobileScreen ? '137px' : '' + } + }, + feedbackIconAdjustmentStyles: { + position: 'relative' as 'relative', + top: '4px' + }, + tenantIconStyles: { + paddingRight: 3 + }, + tenantLabelStyle: { + fontSize: FontSizes.size12, + height: 16 + }, + tenantContainerStyle: { + margin: '0px 10px' + }, + iconButton: { + menuIcon: { fontSize: 15 }, + root: { + height: '50px', + width: '50px', + ':hover': { + background: `${theme.palette.neutralLight} !important` + }, + flexGrow: '1' + } + }, + moreInformationStyles: { + flexGrow: 1, + flexShrink: 1, + flexBasis: '60px' + }, + settingsContainerStyles: { + display: 'flex', + alignItems: 'stretch', + height: 50, + width: 50 + }, + helpContainerStyles: { + height: 50, + width: 50 + }, + tooltipStyles: { + root: { + flexGrow: 1, + display: 'flex', + alignItems: 'stretch' + } + }, + graphExplorerLabelStyles: { + fontSize: mobileScreen ? FontSizes.medium : FontSizes.xLarge, + fontWeight: FontWeights.semibold, + position: mobileScreen ? 'relative' as 'relative' : 'static' as 'static', + top: '3px' + } + } +} \ No newline at end of file diff --git a/src/app/views/main-header/MainHeader.tsx b/src/app/views/main-header/MainHeader.tsx new file mode 100644 index 000000000..2888962ed --- /dev/null +++ b/src/app/views/main-header/MainHeader.tsx @@ -0,0 +1,122 @@ +import * as React from 'react'; +import { + FontIcon, + getId, + getTheme, + IconButton, + IStackTokens, + Label, + registerIcons, + Stack, + TooltipHost +} from '@fluentui/react'; +import { FormattedMessage } from 'react-intl'; + +import { Settings} from './settings/Settings'; +import { FeedbackButton } from './FeedbackButton'; +import { Authentication } from '../authentication'; +import { Help } from './Help'; +import { useSelector } from 'react-redux'; +import { IRootState } from '../../../types/root'; +import { mainHeaderStyles } from './MainHeader.styles'; +import TenantIcon from './tenantIcon'; +import { Mode } from '../../../types/enums'; + +interface MainHeaderProps { + minimised: boolean; + toggleSidebar: Function; + mobileScreen: boolean; +} +const sectionStackTokens: IStackTokens = { + childrenGap: 0 }; +const itemAlignmentsStackTokens: IStackTokens = { + childrenGap: 10 +}; + +registerIcons({ + icons: { + tenantIcon: + } +}); +export const MainHeader: React.FunctionComponent = (props: MainHeaderProps) => { + const { profile, graphExplorerMode } = useSelector( + (state: IRootState) => state + ); + const minimised = props.minimised; + const mobileScreen = props.mobileScreen; + const currentTheme = getTheme(); + const { rootStyles : itemAlignmentStackStyles, rightItemsStyles, graphExplorerLabelStyles, + feedbackIconAdjustmentStyles, tenantIconStyles, moreInformationStyles, + tenantLabelStyle, tenantContainerStyle } = mainHeaderStyles(currentTheme, mobileScreen); + + return ( + + + + + {graphExplorerMode === Mode.Complete && + + +
+ } + }}> + props.toggleSidebar()} /> + + } + + + + + {!mobileScreen && } + {!profile && !mobileScreen && +
+ + {' '} + + } + id={getId()} + calloutProps={{ gapSpace: 0 }} + > + + + +
+ } + {profile && !mobileScreen && +
+ + +
+ } + + + + +
+ + + ); +}; diff --git a/src/app/views/settings/Settings.spec.tsx b/src/app/views/main-header/settings/Settings.spec.tsx similarity index 87% rename from src/app/views/settings/Settings.spec.tsx rename to src/app/views/main-header/settings/Settings.spec.tsx index 61d0ed34a..8ac3abf1d 100644 --- a/src/app/views/settings/Settings.spec.tsx +++ b/src/app/views/main-header/settings/Settings.spec.tsx @@ -3,22 +3,18 @@ import { cleanup, render, screen } from '@testing-library/react'; import { IntlProvider } from 'react-intl'; import userEvent from '@testing-library/user-event'; -import { Settings } from '.'; -import { geLocale } from '../../../appLocale'; -import { ISettingsProps } from '../../../types/settings'; -import { messages_ } from '../../utils/get-messages'; +import { Settings } from './Settings'; +import { geLocale } from '../../../../appLocale'; +import { ISettingsProps } from '../../../../types/settings'; +import { messages_ } from '../../../utils/get-messages'; afterEach(cleanup); const renderSettings = (args?: any) => { const messages = (messages_ as { [key: string]: object })['en-US']; const settingsProps: ISettingsProps = { actions: { - signOut: jest.fn(), changeTheme: jest.fn(), consentToScopes: jest.fn() - }, - intl: { - message: messages } } diff --git a/src/app/views/main-header/settings/Settings.tsx b/src/app/views/main-header/settings/Settings.tsx new file mode 100644 index 000000000..013327b15 --- /dev/null +++ b/src/app/views/main-header/settings/Settings.tsx @@ -0,0 +1,170 @@ +import { + ChoiceGroup, + DefaultButton, + Dialog, + DialogFooter, + DialogType, + DirectionalHint, + getId, + getTheme, + IconButton, + IContextualMenuProps, + TooltipHost +} from '@fluentui/react'; +import React, { useEffect, useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; + +import '../../../utils/string-operations'; +import { componentNames, eventTypes, telemetry } from '../../../../telemetry'; +import { IRootState } from '../../../../types/root'; +import { ISettingsProps } from '../../../../types/settings'; +import { translateMessage } from '../../../utils/translate-messages'; +import { geLocale } from '../../../../appLocale'; +import { changeTheme } from '../../../services/actions/theme-action-creator'; +import { loadGETheme } from '../../../../themes'; +import { AppTheme } from '../../../../types/enums'; +import { mainHeaderStyles } from '../MainHeader.styles'; + +export const Settings: React.FunctionComponent = () => { + const dispatch = useDispatch(); + const { authToken, theme: appTheme } = useSelector((state: IRootState) => state); + const authenticated = authToken.token; + const [themeChooserDialogHidden, hideThemeChooserDialog] = useState(true); + const [items, setItems] = useState([]); + const currentTheme = getTheme(); + + useEffect(() => { + const menuItems: any = [ + { + key: 'change-theme', + text: translateMessage('Change theme'), + iconProps: { + iconName: 'Color' + }, + onClick: () => toggleThemeChooserDialogState() + }, + { + key: 'office-dev-program', + text: translateMessage('Office Dev Program'), + href: `https://developer.microsoft.com/${geLocale}/office/dev-program`, + target: '_blank', + iconProps: { + iconName: 'CommandPrompt' + }, + onClick: () => trackOfficeDevProgramLinkClickEvent() + } + ]; + setItems(menuItems); + }, [authenticated]); + + const toggleThemeChooserDialogState = () => { + let hidden = themeChooserDialogHidden; + hidden = !hidden; + hideThemeChooserDialog(hidden); + telemetry.trackEvent(eventTypes.BUTTON_CLICK_EVENT, { + ComponentName: componentNames.THEME_CHANGE_BUTTON + }); + }; + + const handleChangeTheme = (selectedTheme: any) => { + const newTheme: string = selectedTheme.key; + dispatch(changeTheme(newTheme)); + loadGETheme(newTheme); + telemetry.trackEvent(eventTypes.BUTTON_CLICK_EVENT, { + ComponentName: componentNames.SELECT_THEME_BUTTON, + SelectedTheme: selectedTheme.key.replace('-', ' ').toSentenceCase() + }); + }; + + const trackOfficeDevProgramLinkClickEvent = () => { + telemetry.trackEvent(eventTypes.LINK_CLICK_EVENT, { + ComponentName: componentNames.OFFICE_DEV_PROGRAM_LINK + }); + }; + + const calloutStyles: React.CSSProperties = { + overflowY: 'hidden' + } + + const { iconButton : settingsButtonStyles, settingsContainerStyles, + tooltipStyles} = mainHeaderStyles(currentTheme); + + const menuProperties: IContextualMenuProps = { + shouldFocusOnMount: true, + alignTargetEdge: true, + items, + directionalHint: DirectionalHint.bottomLeftEdge, + directionalHintFixed: true, + calloutProps: { + style: calloutStyles + }, + styles:{container: {border: '1px solid' + currentTheme.palette.neutralTertiary}} + }; + + return ( +
+ + {translateMessage('Settings')} +
+ } + id={getId()} + calloutProps={{ gapSpace: 0 }} + styles={ tooltipStyles } + > + + +
+ +
+ + ); +} + + diff --git a/src/app/views/main-header/tenantIcon.tsx b/src/app/views/main-header/tenantIcon.tsx new file mode 100644 index 000000000..01c1a2110 --- /dev/null +++ b/src/app/views/main-header/tenantIcon.tsx @@ -0,0 +1,16 @@ +/* eslint-disable max-len */ +import { getTheme } from '@fluentui/react'; +import React from 'react'; + +const TenantIcon = () => { + const theme = getTheme(); + return ( + + + + ) +} + +export default TenantIcon; + diff --git a/src/app/views/query-response/QueryResponse.tsx b/src/app/views/query-response/QueryResponse.tsx index 2d8a57221..9a6ffeca8 100644 --- a/src/app/views/query-response/QueryResponse.tsx +++ b/src/app/views/query-response/QueryResponse.tsx @@ -98,7 +98,7 @@ const QueryResponse = (props: IQueryResponseProps) => { marginTop: 10 }} bounds={'window'} - maxHeight={800} + maxHeight={810} minHeight={350} size={{ height: responseHeight, diff --git a/src/app/views/query-response/query-response.scss b/src/app/views/query-response/query-response.scss index 27c98ae84..0720121fd 100644 --- a/src/app/views/query-response/query-response.scss +++ b/src/app/views/query-response/query-response.scss @@ -1,7 +1,6 @@ @import "../../../styles/variables"; .query-response { - width:100%; margin-top: $gutter; border: 1px solid silver; overflow: hidden; @@ -13,14 +12,14 @@ } - @media screen and (min-width: 1320px){ - .pivot-response *[data-content='Expand xx'] { - float: right !important; - } + @media screen and (min-width: 1320px) { + .pivot-response *[data-content='Expand xx'] { + float: right !important; + } } - @media screen and (min-width: 1320px){ + @media screen and (min-width: 1320px) { .pivot-response *[data-content='Share xx'] { float: right !important; } @@ -49,5 +48,4 @@ width: '100%'; overflow-x: auto; overflow-y: hidden; -} - +} \ No newline at end of file diff --git a/src/app/views/query-response/response/Response.tsx b/src/app/views/query-response/response/Response.tsx index 8c771f9ff..84a853ebd 100644 --- a/src/app/views/query-response/response/Response.tsx +++ b/src/app/views/query-response/response/Response.tsx @@ -4,12 +4,12 @@ import { useDispatch, useSelector } from 'react-redux'; import { IRootState } from '../../../../types/root'; import { getContentType } from '../../../services/actions/query-action-creator-util'; -import { responseMessages } from '../../app-sections/ResponseMessages'; +import { responseMessages } from './ResponseMessages'; import { convertVhToPx, getResponseHeight } from '../../common/dimensions/dimensions-adjustment'; import ResponseDisplay from './ResponseDisplay'; const Response = () => { - const { dimensions: { response }, graphResponse, responseAreaExpanded, sampleQuery } = + const { dimensions: { response }, graphResponse, responseAreaExpanded, sampleQuery, authToken, graphExplorerMode } = useSelector((state: IRootState) => state); const { body, headers } = graphResponse; const dispatch = useDispatch(); @@ -21,7 +21,7 @@ const Response = () => { const contentType = getContentType(headers); return (
- {responseMessages(graphResponse, sampleQuery, dispatch)} + {responseMessages(graphResponse, sampleQuery, authToken, graphExplorerMode, dispatch)} {!contentDownloadUrl && !throwsCorsError && headers && { @@ -21,11 +23,14 @@ const renderResponseMessages = (): any => { sampleHeaders: [] }; + const authToken = {pending: false, token: false }; + const graphExplorerMode = Mode.Complete + const dispatch = jest.fn(); return render(
- {responseMessages(graphResponse, sampleQuery, dispatch)} + {responseMessages(graphResponse, sampleQuery, authToken, graphExplorerMode, dispatch)}
) } diff --git a/src/app/views/app-sections/ResponseMessages.tsx b/src/app/views/query-response/response/ResponseMessages.tsx similarity index 59% rename from src/app/views/app-sections/ResponseMessages.tsx rename to src/app/views/query-response/response/ResponseMessages.tsx index d1910b745..36dba19b2 100644 --- a/src/app/views/app-sections/ResponseMessages.tsx +++ b/src/app/views/query-response/response/ResponseMessages.tsx @@ -1,19 +1,27 @@ import { Link, MessageBar, MessageBarType } from '@fluentui/react'; -import React from 'react'; +import React, { useState } from 'react'; import { FormattedMessage } from 'react-intl'; -import { IGraphResponse } from '../../../types/query-response'; +import { IAuthenticateResult } from '../../../../types/authentication'; +import { Mode } from '../../../../types/enums'; +import { IGraphResponse } from '../../../../types/query-response'; -import { IQuery } from '../../../types/query-runner'; -import { runQuery } from '../../services/actions/query-action-creators'; -import { setSampleQuery } from '../../services/actions/query-input-action-creators'; -import { MOZILLA_CORS_DOCUMENTATION_LINK } from '../../services/graph-constants'; +import { IQuery } from '../../../../types/query-runner'; +import { runQuery } from '../../../services/actions/query-action-creators'; +import { setSampleQuery } from '../../../services/actions/query-input-action-creators'; +import { MOZILLA_CORS_DOCUMENTATION_LINK } from '../../../services/graph-constants'; +import { translateMessage } from '../../../utils/translate-messages'; interface ODataLink { link: string; name: string; } -export function responseMessages(graphResponse: IGraphResponse, sampleQuery: IQuery, dispatch: Function) { +export function responseMessages( + graphResponse: IGraphResponse, + sampleQuery: IQuery, + authToken: IAuthenticateResult, + graphExplorerMode: Mode, + dispatch: Function) { function getOdataLinkFromResponseBody(responseBody: any): ODataLink | null { const odataLinks = ['nextLink', 'deltaLink']; @@ -30,7 +38,8 @@ export function responseMessages(graphResponse: IGraphResponse, sampleQuery: IQu } return data; } - + const [displayMessage, setDisplayMessage] = useState(true); + const tokenPresent = !!authToken.token; const { body } = graphResponse; const odataLink = getOdataLinkFromResponseBody(body); @@ -45,7 +54,7 @@ export function responseMessages(graphResponse: IGraphResponse, sampleQuery: IQu if (odataLink) { return ( - : @odata.{odataLink!.name} + : @odata.{odataLink.name} setQuery()}>   @@ -80,4 +89,20 @@ export function responseMessages(graphResponse: IGraphResponse, sampleQuery: IQu
); } + + if(body && !tokenPresent && displayMessage && graphExplorerMode === Mode.Complete) { + return ( +
+ setDisplayMessage(false)} + dismissButtonAriaLabel={translateMessage('Close')} + > + {' '} + + +
+ ); + } } diff --git a/src/app/views/query-runner/request/Request.tsx b/src/app/views/query-runner/request/Request.tsx index d587c66b0..c63749b40 100644 --- a/src/app/views/query-runner/request/Request.tsx +++ b/src/app/views/query-runner/request/Request.tsx @@ -28,15 +28,10 @@ export class Request extends Component { constructor(props: IRequestComponent) { super(props); this.state = { - enableShowSurvey: false, selectedPivot: 'request-body' } } - private toggleCustomSurvey = (show: boolean = false) => { - this.setState({ enableShowSurvey: show }); - } - private getPivotItems = (height: string) => { const { handleOnEditorChange, @@ -178,8 +173,7 @@ export class Request extends Component { <> { if (ref && ref.style && ref.style.height) { diff --git a/src/app/views/settings/Settings.tsx b/src/app/views/settings/Settings.tsx deleted file mode 100644 index 5b1085a4b..000000000 --- a/src/app/views/settings/Settings.tsx +++ /dev/null @@ -1,288 +0,0 @@ -import { - ChoiceGroup, - DefaultButton, - Dialog, - DialogFooter, - DialogType, - DropdownMenuItemType, - getId, - getTheme, - IconButton, - ITheme, - Label, - Panel, - PanelType, - PrimaryButton, - TooltipHost -} from '@fluentui/react'; -import React, { useEffect, useState } from 'react'; -import { FormattedMessage, injectIntl } from 'react-intl'; -import { useDispatch, useSelector } from 'react-redux'; - -import '../../utils/string-operations'; -import { geLocale } from '../../../appLocale'; -import { componentNames, eventTypes, telemetry } from '../../../telemetry'; -import { loadGETheme } from '../../../themes'; -import { AppTheme } from '../../../types/enums'; -import { IRootState } from '../../../types/root'; -import { ISettingsProps } from '../../../types/settings'; -import { signOut } from '../../services/actions/auth-action-creators'; -import { consentToScopes } from '../../services/actions/permissions-action-creator'; -import { togglePermissionsPanel } from '../../services/actions/permissions-panel-action-creator'; -import { changeTheme } from '../../services/actions/theme-action-creator'; -import { Permission } from '../query-runner/request/permissions'; - -function Settings(props: ISettingsProps) { - const dispatch = useDispatch(); - 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([]); - const theme: ITheme = getTheme(); - - const { - intl: { messages } - }: any = props; - - useEffect(() => { - const menuItems: any = [ - { - key: 'office-dev-program', - text: messages['Office Dev Program'], - href: `https://developer.microsoft.com/${geLocale}/office/dev-program`, - target: '_blank', - iconProps: { - iconName: 'CommandPrompt' - }, - onClick: () => trackOfficeDevProgramLinkClickEvent() - }, - { - key: 'report-issue', - text: messages['Report an Issue'], - href: 'https://github.com/microsoftgraph/microsoft-graph-explorer-v4/issues/new/choose', - target: '_blank', - iconProps: { - iconName: 'ReportWarning' - }, - onClick: () => trackReportAnIssueLinkClickEvent() - }, - { - key: 'divider', - text: '-', - itemType: DropdownMenuItemType.Divider - }, - { - key: 'change-theme', - text: messages['Change theme'], - iconProps: { - iconName: 'Color' - }, - onClick: () => toggleThemeChooserDialogState() - } - ]; - - if (authenticated) { - menuItems.push( - { - key: 'view-all-permissions', - text: messages['view all permissions'], - iconProps: { - iconName: 'AzureKeyVault' - }, - onClick: () => changePanelState() - }, - { - key: 'sign-out', - text: messages['sign out'], - iconProps: { - iconName: 'SignOut' - }, - onClick: () => handleSignOut() - }, - ); - } - setItems(menuItems); - }, [authenticated]); - - const toggleThemeChooserDialogState = () => { - let hidden = themeChooserDialogHidden; - hidden = !hidden; - hideThemeChooserDialog(hidden); - telemetry.trackEvent(eventTypes.BUTTON_CLICK_EVENT, { - ComponentName: componentNames.THEME_CHANGE_BUTTON - }); - }; - - const handleSignOut = () => { - dispatch(signOut()); - }; - - const handleChangeTheme = (selectedTheme: any) => { - const newTheme: string = selectedTheme.key; - dispatch(changeTheme(newTheme)); - loadGETheme(newTheme); - telemetry.trackEvent(eventTypes.BUTTON_CLICK_EVENT, { - ComponentName: componentNames.SELECT_THEME_BUTTON, - SelectedTheme: selectedTheme.key.replace('-', ' ').toSentenceCase() - }); - }; - - const changePanelState = () => { - let open = !!permissionsPanelOpen; - open = !open; - dispatch(togglePermissionsPanel(open)); - setSelectedPermissions([]); - trackSelectPermissionsButtonClickEvent(); - }; - - const trackSelectPermissionsButtonClickEvent = () => { - telemetry.trackEvent(eventTypes.BUTTON_CLICK_EVENT, { - ComponentName: componentNames.VIEW_ALL_PERMISSIONS_BUTTON - }); - }; - - const trackReportAnIssueLinkClickEvent = () => { - telemetry.trackEvent(eventTypes.LINK_CLICK_EVENT, { - ComponentName: componentNames.REPORT_AN_ISSUE_LINK - }); - }; - - const setPermissions = (permissions: []) => { - setSelectedPermissions(permissions); - }; - - const handleConsent = () => { - dispatch(consentToScopes(selectedPermissions)); - setSelectedPermissions([]); - }; - - const trackOfficeDevProgramLinkClickEvent = () => { - telemetry.trackEvent(eventTypes.LINK_CLICK_EVENT, { - ComponentName: componentNames.OFFICE_DEV_PROGRAM_LINK - }); - }; - - const getSelectionDetails = () => { - const selectionCount = selectedPermissions.length; - - switch (selectionCount) { - case 0: - return ''; - case 1: - return `1 ${messages.selected}: ` + selectedPermissions[0]; - default: - return `${selectionCount} ${messages.selected}`; - } - }; - - const onRenderFooterContent = () => { - return ( -
- - handleConsent()} - style={{ marginRight: 10, - backgroundColor: (selectedPermissions.length === 0) ? theme.palette.themeDarker : - theme.palette.themeSecondary }} - > - - - changePanelState()}> - - -
- ); - }; - - const menuProperties = { - shouldFocusOnMount: true, - alignTargetEdge: true, - items - }; - - return ( -
- - - -
- - - changePanelState()} - type={PanelType.medium} - hasCloseButton={true} - headerText={messages.Permissions} - onRenderFooterContent={onRenderFooterContent} - isFooterAtBottom={true} - closeButtonAriaLabel='Close' - styles={{footer : {backgroundColor:theme.palette.white}}} - > - - -
-
- ); -} - -export default injectIntl(Settings); - diff --git a/src/app/views/settings/index.ts b/src/app/views/settings/index.ts deleted file mode 100644 index 5164b0526..000000000 --- a/src/app/views/settings/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import Settings from './Settings'; - -export { Settings }; diff --git a/src/app/views/sidebar/Sidebar.styles.ts b/src/app/views/sidebar/Sidebar.styles.ts index bec599525..a3598d3ce 100644 --- a/src/app/views/sidebar/Sidebar.styles.ts +++ b/src/app/views/sidebar/Sidebar.styles.ts @@ -16,7 +16,7 @@ export const sidebarStyles = (theme: ITheme) => { queryList: { marginBottom: theme.spacing.s1, cursor: 'pointer', - maxHeight: pageHeightInVh, + maxHeight: '72vh', minHeight: pageHeightInVh, overflow: 'hidden', fontSize: FontSizes.medium, @@ -115,6 +115,15 @@ export const sidebarStyles = (theme: ITheme) => { }, links: { color: `${theme.palette.blueMid} !important` + }, + sidebarButtons: { + root:{ + height: 50, + width: 50, + ':hover': { + background: `${theme.palette.neutralLight} !important` + } + } } }; }; diff --git a/src/app/views/sidebar/Sidebar.tsx b/src/app/views/sidebar/Sidebar.tsx index f52d0dd21..b21311847 100644 --- a/src/app/views/sidebar/Sidebar.tsx +++ b/src/app/views/sidebar/Sidebar.tsx @@ -1,4 +1,11 @@ -import { Pivot, PivotItem } from '@fluentui/react'; +import { DirectionalHint, + getTheme, + IconButton, + Pivot, + PivotItem, + Stack, + TooltipDelay, + TooltipHost } from '@fluentui/react'; import React from 'react'; import { telemetry } from '../../../telemetry'; @@ -6,12 +13,18 @@ import { translateMessage } from '../../utils/translate-messages'; import History from './history/History'; import { ResourceExplorer } from './resource-explorer'; import SampleQueries from './sample-queries/SampleQueries'; +import { sidebarStyles } from './Sidebar.styles'; interface ISidebar { currentTab: string; setSidebarTabSelection: Function; + showSidebar: boolean; + toggleSidebar: Function; + mobileScreen: boolean; } -export const Sidebar = (props: ISidebar) => { +export const Sidebar = (props: ISidebar) =>{ + const theme = getTheme(); + const styles = sidebarStyles(theme).sidebarButtons; const onPivotItemClick = (item?: PivotItem) => { if (!item) { return; } @@ -21,9 +34,14 @@ export const Sidebar = (props: ISidebar) => { telemetry.trackTabClickEvent(key); } } + const openComponent = (key: string) => { + props.toggleSidebar(); + props.setSidebarTabSelection(key); + } return (
+ {props.showSidebar && {
+ } + { !props.showSidebar && !props.mobileScreen && ( + + + {translateMessage('Sample Queries')} +
} + calloutProps={{gapSpace: 0}} + directionalHint={DirectionalHint.bottomCenter} + styles={{root: { display: 'inline-block'}}} + delay={TooltipDelay.zero} + > + openComponent('sample-queries')} + styles={styles} + /> + + + {translateMessage('Resources')} + } + calloutProps={{gapSpace: 0}} + directionalHint={DirectionalHint.bottomCenter} + styles={{root: { display: 'inline-block'}}} + delay={TooltipDelay.zero} + > + openComponent('resources')} + styles={styles} + /> + + + {translateMessage('History')} + } + calloutProps={{gapSpace: 0}} + directionalHint={DirectionalHint.bottomCenter} + styles={{root: { display: 'inline-block'}}} + delay={TooltipDelay.zero} + > + openComponent('history')} + styles={styles} + /> + + ) + } ); }; diff --git a/src/app/views/sidebar/history/History.tsx b/src/app/views/sidebar/history/History.tsx index 1cad5bf35..fa65f437b 100644 --- a/src/app/views/sidebar/history/History.tsx +++ b/src/app/views/sidebar/history/History.tsx @@ -259,7 +259,8 @@ export class History extends Component { style={{ display: 'flex', justifyContent: 'center', - alignItems: 'center' + alignItems: 'center', + paddingRight: 10 }} >
diff --git a/src/messages/GE.json b/src/messages/GE.json index b69fb721f..6d32c374a 100644 --- a/src/messages/GE.json +++ b/src/messages/GE.json @@ -13,7 +13,7 @@ "secure score control profiles (beta)": "secure score control profiles (beta)", "update alert": "update alert", "To try the explorer, please ": "To try the explorer, please ", - "sign in": "Sign in to Graph Explorer", + "sign in": "Sign in", " with your work or school account from Microsoft.": " with your work or school account from Microsoft.", "Submit": "Submit", "Using demo tenant": "You are currently using a sample account.", @@ -301,7 +301,7 @@ "Microsoft Graph API Reference docs": "Microsoft Graph API Reference docs.", "Fetching permissions": "Fetching permissions", "Authentication failed": "Authentication failed", - "view all permissions": "Select permissions", + "view all permissions": "Consent to permissions", "Fetching code snippet": "Fetching code snippet", "Snippet not available": "Snippet not available", "Select different permissions": "To try out different Microsoft Graph API endpoints, choose the permissions, and then click Consent.", @@ -443,5 +443,9 @@ "More items": "More items", "Update": "Update", "Edit request header": "Edit request header", - "Remove request header": "Remove request header" + "Remove request header": "Remove request header", + "Settings": "Settings", + "sign in other account": "Sign in with a different account", + "Help": "Help", + "Documentation": "Documentation" } \ No newline at end of file diff --git a/src/modules/authentication/AuthenticationWrapper.ts b/src/modules/authentication/AuthenticationWrapper.ts index 88743959e..820d4a30b 100644 --- a/src/modules/authentication/AuthenticationWrapper.ts +++ b/src/modules/authentication/AuthenticationWrapper.ts @@ -49,6 +49,28 @@ export class AuthenticationWrapper implements IAuthenticationWrapper { } } + public async logInWithOther() { + const popUpRequest: PopupRequest = { + scopes: defaultScopes, + authority: this.getAuthority(), + prompt: 'select_account', + redirectUri: getCurrentUri(), + extraQueryParameters: { mkt: geLocale } + }; + // eslint-disable-next-line no-useless-catch + try { + const result = await msalApplication.loginPopup(popUpRequest); + this.storeHomeAccountId(result.account!); + return result; + } catch (error: any) { + const {errorCode} = error; + if (errorCode === 'interaction_in_progress') { + this.eraseInteractionInProgressCookie(); + } + throw error; + } + } + public logOut() { this.deleteHomeAccountId(); msalApplication.logoutRedirect(); diff --git a/src/telemetry/component-names.ts b/src/telemetry/component-names.ts index 8bc024d55..12f40734e 100644 --- a/src/telemetry/component-names.ts +++ b/src/telemetry/component-names.ts @@ -70,6 +70,9 @@ export const CODE_SNIPPET_LANGUAGES = { sdk: 'PowerShell snippet SDK link', doc: 'PowerShell snippet docs link' } } +export const GE_DOCUMENTATION_LINK = 'GE documentation link'; +export const GITHUB_LINK = 'Github link'; +export const GRAPH_DOCUMENTATION_LINK = 'Graph documentation link' // Actions export const GET_SNIPPET_ACTION = 'Get snippet action'; diff --git a/src/types/profile.ts b/src/types/profile.ts index 29c4c2e6d..956a98030 100644 --- a/src/types/profile.ts +++ b/src/types/profile.ts @@ -25,4 +25,5 @@ export interface IUser { profileImageUrl: string; profileType: ACCOUNT_TYPE; ageGroup: number; + tenant: string; } diff --git a/src/types/settings.ts b/src/types/settings.ts index ef2e525a2..0ac34cca2 100644 --- a/src/types/settings.ts +++ b/src/types/settings.ts @@ -1,10 +1,6 @@ export interface ISettingsProps { actions?: { - signOut: Function; changeTheme: Function; consentToScopes: Function; }; - intl?: { - message: object; - }; }