From e46710a7dd280327fa4e449bb3f0d583bbb87de7 Mon Sep 17 00:00:00 2001 From: Charles Wahome Date: Mon, 3 May 2021 15:54:48 +0300 Subject: [PATCH 01/45] get graph version from content between slashes --- src/app/utils/sample-url-generation.ts | 8 +++++++- ...generation.spec.tsx => sample-url-generation.spec.ts} | 9 ++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) rename src/tests/utils/{sample-url-generation.spec.tsx => sample-url-generation.spec.ts} (84%) diff --git a/src/app/utils/sample-url-generation.ts b/src/app/utils/sample-url-generation.ts index 5752035a82..182cb7d7b0 100644 --- a/src/app/utils/sample-url-generation.ts +++ b/src/app/utils/sample-url-generation.ts @@ -10,7 +10,7 @@ export function parseSampleUrl(url: string, version?: string) { try { const urlObject: URL = new URL(url); requestUrl = decodeURIComponent(urlObject.pathname.substr(6).replace(/\/$/, '')); - queryVersion = (version) ? version : urlObject.pathname.substring(1, 5); + queryVersion = (version) ? version : getGraphVersion(url); search = generateSearchParameters(urlObject, search); sampleUrl = `${GRAPH_URL}/${queryVersion}/${requestUrl + search}`; } catch (error) { @@ -27,6 +27,12 @@ export function parseSampleUrl(url: string, version?: string) { }; } +export function getGraphVersion(url: string): string { + const urlObject: URL = new URL(url); + const parts = urlObject.pathname.substring(1).split('/'); + return parts[0]; +} + function generateSearchParameters(urlObject: URL, search: string) { const searchParameters = urlObject.search; if (searchParameters) { diff --git a/src/tests/utils/sample-url-generation.spec.tsx b/src/tests/utils/sample-url-generation.spec.ts similarity index 84% rename from src/tests/utils/sample-url-generation.spec.tsx rename to src/tests/utils/sample-url-generation.spec.ts index 3dc167e9d0..122a9be251 100644 --- a/src/tests/utils/sample-url-generation.spec.tsx +++ b/src/tests/utils/sample-url-generation.spec.ts @@ -1,4 +1,4 @@ -import { parseSampleUrl } from '../../app/utils/sample-url-generation'; +import { parseSampleUrl, getGraphVersion } from '../../app/utils/sample-url-generation'; describe('Sample Url Generation', () => { @@ -75,4 +75,11 @@ describe('Sample Url Generation', () => { expect(parsedUrl).toEqual(expectedUrl); }); + it('returns appropriate version number', () => { + const url = `https://graph.microsoft.com/v1.0/users`; + const expectedVersion = 'v1.0'; + const version = getGraphVersion(url); + expect(expectedVersion).toEqual(version); + }) + }); From 2f94af6a9f752fe7c6f8aae435a5843aa04437bf Mon Sep 17 00:00:00 2001 From: Charles Wahome Date: Mon, 3 May 2021 17:12:44 +0300 Subject: [PATCH 02/45] detect cloud from application locale --- src/app/utils/cloud-resolver.ts | 31 ++++++++++++++++++++ src/app/views/App.tsx | 52 ++++++++++++++++++++++++++++++++- 2 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 src/app/utils/cloud-resolver.ts diff --git a/src/app/utils/cloud-resolver.ts b/src/app/utils/cloud-resolver.ts new file mode 100644 index 0000000000..b9c5909a3a --- /dev/null +++ b/src/app/utils/cloud-resolver.ts @@ -0,0 +1,31 @@ +import { geLocale } from '../../appLocale'; + +export interface ICloud { + locale: string; + name: string; + baseUrl: string; + loginUrl: string; +} + +export const clouds: ICloud[] = [{ + locale: 'de-de', + name: 'German', + baseUrl: 'https://graph.microsoft.de', + loginUrl: 'https://login.microsoftonline.de' +}, +{ + locale: 'zh-cn', + name: 'China', + baseUrl: 'https://microsoftgraph.chinacloudapi.cn', + loginUrl: 'https://portal.azure.cn' +}, { + locale: 'en-us', + name: 'Canary', + baseUrl: 'https://canary.graph.microsoft.com', + loginUrl: 'https://login.microsoftonline.com' +}]; + +export function getCurrentCloud(): ICloud | undefined { + const localeValue = geLocale.toLowerCase(); + return clouds.find(k => k.locale === localeValue); +} \ No newline at end of file diff --git a/src/app/views/App.tsx b/src/app/views/App.tsx index 78f61a3337..3a3d185e2f 100644 --- a/src/app/views/App.tsx +++ b/src/app/views/App.tsx @@ -1,6 +1,8 @@ import { Announced, - IStackTokens, ITheme, styled + DefaultButton, Dialog, DialogFooter, DialogType, + IStackTokens, ITheme, PrimaryButton, + styled } from 'office-ui-fabric-react'; import React, { Component } from 'react'; import { InjectedIntl, injectIntl } from 'react-intl'; @@ -25,6 +27,7 @@ import { clearTermsOfUse } from '../services/actions/terms-of-use-action-creator import { changeThemeSuccess } from '../services/actions/theme-action-creator'; import { toggleSidebar } from '../services/actions/toggle-sidebar-action-creator'; import { GRAPH_URL } from '../services/graph-constants'; +import { getCurrentCloud } from '../utils/cloud-resolver'; import { parseSampleUrl } from '../utils/sample-url-generation'; import { substituteTokens } from '../utils/token-helpers'; import { translateMessage } from '../utils/translate-messages'; @@ -68,6 +71,8 @@ interface IAppState { selectedVerb: string; mobileScreen: boolean; hideDialog: boolean; + showCloudDialog: boolean; + cloud: string | undefined; } class App extends Component { @@ -78,6 +83,8 @@ class App extends Component { this.state = { selectedVerb: 'GET', mobileScreen: false, + showCloudDialog: false, + cloud: 'global service', hideDialog: true }; } @@ -118,8 +125,30 @@ class App extends Component { // Listens for messages from host document window.addEventListener('message', this.receiveMessage, false); this.handleSharedQueries(); + this.toggleConfirmCloud(); }; + public toggleConfirmCloud = () => { + if (this.state.showCloudDialog) { + this.setState({ + showCloudDialog: false + }) + } else { + const currentCloud = getCurrentCloud();; + if (currentCloud) { + this.setState({ + showCloudDialog: true, + cloud: currentCloud.name + }) + } + } + } + + public setCloud = () => { + localStorage.setItem('cloud', this.state.cloud!); + this.toggleConfirmCloud(); + } + public handleSharedQueries() { const { actions } = this.props; const queryStringParams = this.getQueryStringParams(); @@ -293,6 +322,7 @@ class App extends Component { } public render() { + const { showCloudDialog, cloud } = this.state; const classes = classNames(this.props); const { authenticated, graphExplorerMode, queryState, minimised, termsOfUse, sampleQuery, actions, sidebarProperties, intl: { messages } }: any = this.props; @@ -330,6 +360,12 @@ class App extends Component { sidebarWidth = layout = 'col-xs-12 col-sm-12'; } + const dialogContentProps = { + type: DialogType.largeHeader, + title: 'You have access to sovereign clouds', + subText: `Hey there! Would you like to access your information available in the ${cloud} cloud? You will need to log in once you choose yes` + } + return ( // @ts-ignore @@ -377,6 +413,20 @@ class App extends Component { + ); } From 240e61803bc2afe5ce130f78e1f69254ea34690b Mon Sep 17 00:00:00 2001 From: Charles Wahome Date: Mon, 3 May 2021 18:01:12 +0300 Subject: [PATCH 03/45] create type, action-creator and reducer --- .../services/actions/cloud-action-creator.ts | 9 +++++++ src/app/services/reducers/cloud-reducer.ts | 21 +++++++++++++++ src/app/services/reducers/index.ts | 4 ++- src/app/services/redux-constants.ts | 1 + src/app/utils/cloud-resolver.ts | 23 +++++++++++----- src/types/cloud.ts | 6 +++++ src/types/root.ts | 26 ++++++++++--------- 7 files changed, 70 insertions(+), 20 deletions(-) create mode 100644 src/app/services/actions/cloud-action-creator.ts create mode 100644 src/app/services/reducers/cloud-reducer.ts create mode 100644 src/types/cloud.ts diff --git a/src/app/services/actions/cloud-action-creator.ts b/src/app/services/actions/cloud-action-creator.ts new file mode 100644 index 0000000000..010d4f7cdd --- /dev/null +++ b/src/app/services/actions/cloud-action-creator.ts @@ -0,0 +1,9 @@ +import { IAction } from '../../../types/action'; +import { SET_ACTIVE_CLOUD_SUCCESS } from '../redux-constants'; + +export function setActiveCloud(response: object): IAction { + return { + type: SET_ACTIVE_CLOUD_SUCCESS, + response + }; +} \ No newline at end of file diff --git a/src/app/services/reducers/cloud-reducer.ts b/src/app/services/reducers/cloud-reducer.ts new file mode 100644 index 0000000000..ae86a12a1c --- /dev/null +++ b/src/app/services/reducers/cloud-reducer.ts @@ -0,0 +1,21 @@ +import { IAction } from '../../../types/action'; +import { ICloud } from '../../../types/cloud'; +import { AUTH_URL, GRAPH_URL } from '../graph-constants'; +import { SET_ACTIVE_CLOUD_SUCCESS } from '../redux-constants'; + +const initialState: ICloud = { + baseUrl: GRAPH_URL, + locale: 'global', + loginUrl: AUTH_URL, + name: 'global' +} + +export function cloud(state = initialState, action: IAction): ICloud { + switch (action.type) { + case SET_ACTIVE_CLOUD_SUCCESS: + return action.response; + + default: + return state; + } +} diff --git a/src/app/services/reducers/index.ts b/src/app/services/reducers/index.ts index 7cfb0d9c6d..21c7e3cd00 100644 --- a/src/app/services/reducers/index.ts +++ b/src/app/services/reducers/index.ts @@ -2,10 +2,11 @@ import { combineReducers } from 'redux'; import { adaptiveCard } from './adaptive-cards-reducer'; import { authToken, consentedScopes } from './auth-reducers'; import { autoComplete } from './autocomplete-reducer'; +import { cloud } from './cloud-reducer'; import { devxApi } from './devxApi-reducers'; import { dimensions } from './dimensions-reducers'; -import { permissionsPanelOpen } from './permissions-panel-reducer'; import { graphExplorerMode } from './graph-explorer-mode-reducer'; +import { permissionsPanelOpen } from './permissions-panel-reducer'; import { scopes } from './permissions-reducer'; import { profile } from './profile-reducer'; import { sampleQuery } from './query-input-reducers'; @@ -24,6 +25,7 @@ export default combineReducers({ adaptiveCard, authToken, autoComplete, + cloud, consentedScopes, graphExplorerMode, graphResponse, diff --git a/src/app/services/redux-constants.ts b/src/app/services/redux-constants.ts index a5984d5a57..1cc02fba73 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 SET_ACTIVE_CLOUD_SUCCESS = 'SET_ACTIVE_CLOUD_SUCCESS'; diff --git a/src/app/utils/cloud-resolver.ts b/src/app/utils/cloud-resolver.ts index b9c5909a3a..90e233e199 100644 --- a/src/app/utils/cloud-resolver.ts +++ b/src/app/utils/cloud-resolver.ts @@ -1,11 +1,5 @@ import { geLocale } from '../../appLocale'; - -export interface ICloud { - locale: string; - name: string; - baseUrl: string; - loginUrl: string; -} +import { ICloud } from '../../types/cloud'; export const clouds: ICloud[] = [{ locale: 'de-de', @@ -26,6 +20,21 @@ export const clouds: ICloud[] = [{ }]; export function getCurrentCloud(): ICloud | undefined { + const cloudName = localStorage.getItem('cloud'); + if (cloudName) { + return getCloudProperties(cloudName); + } + return undefined; +} + +export function getEligibleCloud(): ICloud | undefined { const localeValue = geLocale.toLowerCase(); return clouds.find(k => k.locale === localeValue); +} + +export function getCloudProperties(cloudName: string): ICloud | undefined { + if (cloudName) { + return clouds.find(k => k.name === cloudName) + } + return undefined; } \ No newline at end of file diff --git a/src/types/cloud.ts b/src/types/cloud.ts new file mode 100644 index 0000000000..3a638e3502 --- /dev/null +++ b/src/types/cloud.ts @@ -0,0 +1,6 @@ +export interface ICloud { + locale: string; + name: string; + baseUrl: string; + loginUrl: string; +} \ No newline at end of file diff --git a/src/types/root.ts b/src/types/root.ts index 609d9310df..186d59ac1d 100644 --- a/src/types/root.ts +++ b/src/types/root.ts @@ -1,3 +1,4 @@ +import { ICloud } from './cloud'; import { IAdaptiveCardResponse } from './adaptivecard'; import { IAutocompleteResponse } from './auto-complete'; import { IDimensions } from './dimensions'; @@ -12,26 +13,27 @@ import { ISnippet } from './snippets'; import { IStatus } from './status'; export interface IRootState { - theme: string; adaptiveCard: IAdaptiveCardResponse; + authToken: string; + autoComplete: IAutocompleteResponse; + cloud: ICloud; + consentedScopes: string[]; + dimensions: IDimensions; graphExplorerMode: Mode; + graphResponse: IGraphResponse; + history: IHistoryItem[]; + isLoadingData: boolean; + permissionsPanelOpen: boolean; profile: IUser | undefined | null; queryRunnerStatus: IStatus | null; + responseAreaExpanded: boolean; sampleQuery: IQuery; - termsOfUse: boolean; - sidebarProperties: ISidebarProps; - authToken: string; samples: ISampleQuery[]; - consentedScopes: string[]; scopes: IScopes; - history: IHistoryItem[]; - graphResponse: IGraphResponse; - permissionsPanelOpen: boolean; - isLoadingData: boolean; + sidebarProperties: ISidebarProps; snippets: ISnippet; - responseAreaExpanded: boolean; - dimensions: IDimensions; - autoComplete: IAutocompleteResponse; + termsOfUse: boolean; + theme: string; } export interface IApiFetch { From 2ca7d7cedd1008e580001e785c0aeab77c4911b0 Mon Sep 17 00:00:00 2001 From: Charles Wahome Date: Tue, 4 May 2021 13:51:57 +0300 Subject: [PATCH 04/45] change base-url on page load for pre-selected cloud --- src/index.tsx | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/index.tsx b/src/index.tsx index 35d7373b1f..36d5b46c48 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -15,10 +15,14 @@ import ru from 'react-intl/locale-data/ru'; import zh from 'react-intl/locale-data/zh'; import { Provider } from 'react-redux'; import { getAuthTokenSuccess, getConsentedScopesSuccess } from './app/services/actions/auth-action-creators'; +import { setActiveCloud } from './app/services/actions/cloud-action-creator'; import { setDevxApiUrl } from './app/services/actions/devxApi-action-creators'; import { setGraphExplorerMode } from './app/services/actions/explorer-mode-action-creator'; +import { setSampleQuery } from './app/services/actions/query-input-action-creators'; import { addHistoryItem } from './app/services/actions/request-history-action-creators'; import { changeThemeSuccess } from './app/services/actions/theme-action-creator'; +import { GRAPH_URL } from './app/services/graph-constants'; +import { getCloudProperties } from './app/utils/cloud-resolver'; import { isValidHttpsUrl } from './app/utils/external-link-validation'; import App from './app/views/App'; import { readHistoryData } from './app/views/sidebar/history/history-utils'; @@ -34,6 +38,7 @@ import { readTheme } from './themes/theme-utils'; import { IDevxAPI } from './types/devx-api'; import { Mode } from './types/enums'; import { IHistoryItem } from './types/history'; +import { IQuery } from './types/query-runner'; // removes the loading spinner from GE html after the app is loaded const spinner = document.getElementById('spinner'); @@ -59,7 +64,7 @@ const appState: any = store({ profile: null, queryRunnerStatus: null, sampleQuery: { - sampleUrl: 'https://graph.microsoft.com/v1.0/me', + sampleUrl: `${GRAPH_URL}/v1.0/me`, selectedVerb: 'GET', sampleBody: undefined, sampleHeaders: [], @@ -67,7 +72,6 @@ const appState: any = store({ }, termsOfUse: true, theme: currentTheme, - }); function refreshAccessToken() { @@ -131,6 +135,18 @@ readHistoryData().then((data: any) => { } }); +const cloudName = localStorage.getItem('cloud'); +if (cloudName) { + const cloud = getCloudProperties(cloudName); + const { baseUrl } = cloud!; + appState.dispatch(setActiveCloud(cloud!)); + const query: IQuery = appState.getState().sampleQuery; + const origin = new URL(query.sampleUrl).origin; + query.sampleUrl = query.sampleUrl.replace(origin, baseUrl); + + appState.dispatch(setSampleQuery(query)) +} + /** * Set's up Monaco Editor's Workers. */ From 787e42d9b3848534ad90825efbf325ff09404ee5 Mon Sep 17 00:00:00 2001 From: Charles Wahome Date: Tue, 4 May 2021 13:52:47 +0300 Subject: [PATCH 05/45] change base-url for app when config is set --- src/app/views/App.tsx | 13 +++++++++---- .../query-input/auto-complete/AutoComplete.tsx | 15 +++++++++++++-- src/types/auto-complete.ts | 2 ++ 3 files changed, 24 insertions(+), 6 deletions(-) diff --git a/src/app/views/App.tsx b/src/app/views/App.tsx index 3a3d185e2f..678c5ed14f 100644 --- a/src/app/views/App.tsx +++ b/src/app/views/App.tsx @@ -27,7 +27,7 @@ import { clearTermsOfUse } from '../services/actions/terms-of-use-action-creator import { changeThemeSuccess } from '../services/actions/theme-action-creator'; import { toggleSidebar } from '../services/actions/toggle-sidebar-action-creator'; import { GRAPH_URL } from '../services/graph-constants'; -import { getCurrentCloud } from '../utils/cloud-resolver'; +import { getCloudProperties, getCurrentCloud, getEligibleCloud } from '../utils/cloud-resolver'; import { parseSampleUrl } from '../utils/sample-url-generation'; import { substituteTokens } from '../utils/token-helpers'; import { translateMessage } from '../utils/translate-messages'; @@ -44,6 +44,7 @@ import { QueryRunner } from './query-runner'; import { parse } from './query-runner/util/iframe-message-parser'; import { Settings } from './settings'; import { Sidebar } from './sidebar/Sidebar'; +import { setActiveCloud } from '../services/actions/cloud-action-creator'; interface IAppProps { theme?: ITheme; @@ -64,6 +65,7 @@ interface IAppProps { toggleSidebar: Function; signIn: Function; storeScopes: Function; + setActiveCloud: Function; }; } @@ -134,11 +136,12 @@ class App extends Component { showCloudDialog: false }) } else { - const currentCloud = getCurrentCloud();; - if (currentCloud) { + const currentCloud = getCurrentCloud() || null; + const eligibleCloud = getEligibleCloud() || null; + if (!currentCloud && eligibleCloud) { this.setState({ showCloudDialog: true, - cloud: currentCloud.name + cloud: eligibleCloud.name }) } } @@ -146,6 +149,7 @@ class App extends Component { public setCloud = () => { localStorage.setItem('cloud', this.state.cloud!); + this.props.actions!.setActiveCloud(getCloudProperties(this.state.cloud!)) this.toggleConfirmCloud(); } @@ -459,6 +463,7 @@ const mapDispatchToProps = (dispatch: Dispatch) => { clearTermsOfUse, runQuery, setSampleQuery, + setActiveCloud, toggleSidebar, ...authActionCreators, changeTheme: (newTheme: string) => { diff --git a/src/app/views/query-runner/query-input/auto-complete/AutoComplete.tsx b/src/app/views/query-runner/query-input/auto-complete/AutoComplete.tsx index b489ef9f1e..f939064237 100644 --- a/src/app/views/query-runner/query-input/auto-complete/AutoComplete.tsx +++ b/src/app/views/query-runner/query-input/auto-complete/AutoComplete.tsx @@ -255,6 +255,16 @@ class AutoComplete extends Component { }); } } + + const origin = new URL(this.state.queryUrl).origin; + if (origin !== this.props.cloud.baseUrl) { + const queryUrl = this.state.queryUrl.replace(origin, this.props.cloud.baseUrl); + const userInput = this.state.userInput.replace(origin, this.props.cloud.baseUrl); + this.setState({ + queryUrl, + userInput + }); + } } private filterSuggestions(userInput: string, previousUserInput: string, compare: string, suggestions: string[]) { @@ -414,13 +424,14 @@ class AutoComplete extends Component { } } -function mapStateToProps({ sampleQuery, theme, autoComplete }: IRootState) { +function mapStateToProps({ sampleQuery, theme, autoComplete, cloud }: IRootState) { return { sampleQuery, appTheme: theme, autoCompleteOptions: autoComplete.data, fetchingSuggestions: autoComplete.pending, - autoCompleteError: autoComplete.error + autoCompleteError: autoComplete.error, + cloud }; } diff --git a/src/types/auto-complete.ts b/src/types/auto-complete.ts index b55e806be9..90c40768ea 100644 --- a/src/types/auto-complete.ts +++ b/src/types/auto-complete.ts @@ -1,4 +1,5 @@ import { IApiResponse } from './action'; +import { ICloud } from './cloud'; import { IParsedOpenApiResponse } from './open-api'; import { IQuery } from './query-runner'; @@ -13,6 +14,7 @@ export interface IAutoCompleteProps { url: string; parameters: any[]; }; + cloud: ICloud; actions?: { fetchAutoCompleteOptions: Function; }; From 5b7d25662bf9c7d45bb48ac90dc51c8eb23426c7 Mon Sep 17 00:00:00 2001 From: Charles Wahome Date: Thu, 6 May 2021 15:27:58 +0300 Subject: [PATCH 06/45] change parse function to handle long versions --- src/app/utils/sample-url-generation.ts | 14 ++++++---- src/tests/utils/sample-url-generation.spec.ts | 26 +++++++++++++++++-- 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/src/app/utils/sample-url-generation.ts b/src/app/utils/sample-url-generation.ts index 182cb7d7b0..fad701f84f 100644 --- a/src/app/utils/sample-url-generation.ts +++ b/src/app/utils/sample-url-generation.ts @@ -1,5 +1,3 @@ -import { GRAPH_URL } from '../services/graph-constants'; - export function parseSampleUrl(url: string, version?: string) { let requestUrl = ''; let queryVersion = ''; @@ -9,10 +7,11 @@ export function parseSampleUrl(url: string, version?: string) { if (url !== '') { try { const urlObject: URL = new URL(url); - requestUrl = decodeURIComponent(urlObject.pathname.substr(6).replace(/\/$/, '')); + const { origin } = urlObject; queryVersion = (version) ? version : getGraphVersion(url); + requestUrl = getRequestUrl(url, queryVersion); search = generateSearchParameters(urlObject, search); - sampleUrl = `${GRAPH_URL}/${queryVersion}/${requestUrl + search}`; + sampleUrl = `${origin}/${queryVersion}/${requestUrl + search}`; } catch (error) { if (error.message === `Failed to construct 'URL': Invalid URL`) { return { @@ -27,6 +26,12 @@ export function parseSampleUrl(url: string, version?: string) { }; } +export function getRequestUrl(url: string, version: string): string { + const { pathname } = new URL(url); + const requestContent = pathname.split(version + '/').pop(); + return decodeURIComponent(requestContent!.replace(/\/$/, '')); +} + export function getGraphVersion(url: string): string { const urlObject: URL = new URL(url); const parts = urlObject.pathname.substring(1).split('/'); @@ -47,4 +52,3 @@ function generateSearchParameters(urlObject: URL, search: string) { } return search; } - diff --git a/src/tests/utils/sample-url-generation.spec.ts b/src/tests/utils/sample-url-generation.spec.ts index 122a9be251..d49553e984 100644 --- a/src/tests/utils/sample-url-generation.spec.ts +++ b/src/tests/utils/sample-url-generation.spec.ts @@ -1,4 +1,4 @@ -import { parseSampleUrl, getGraphVersion } from '../../app/utils/sample-url-generation'; +import { parseSampleUrl, getGraphVersion, getRequestUrl } from '../../app/utils/sample-url-generation'; describe('Sample Url Generation', () => { @@ -80,6 +80,28 @@ describe('Sample Url Generation', () => { const expectedVersion = 'v1.0'; const version = getGraphVersion(url); expect(expectedVersion).toEqual(version); - }) + }); + + it('destructures sample url with long version number', () => { + const url = `https://graph.microsoft.com/longversionnumberv2/me/messages`; + + const expectedUrl = { + requestUrl: 'me/messages', + queryVersion: 'longversionnumberv2', + sampleUrl: url, + search: '' + }; + + const parsedUrl = parseSampleUrl(url); + expect(parsedUrl).toEqual(expectedUrl); + }); + + it('returns appropriate request url for long version number', () => { + const version = 'longversionnumberv2'; + const request = 'me/messages'; + const url = `https://graph.microsoft.com/${version}/${request}`; + const requestUrl = getRequestUrl(url, version); + expect(requestUrl).toEqual(request); + }); }); From 9f7640431e800264f4e4f19ecd81577d5234f853 Mon Sep 17 00:00:00 2001 From: Charles Wahome Date: Thu, 6 May 2021 17:16:38 +0300 Subject: [PATCH 07/45] destructure sample urls to parts regardless of version size --- src/app/utils/query-url-sanitization.ts | 4 +- src/app/utils/sample-url-generation.ts | 35 +++++++++------ .../sidebar/sample-queries/SampleQueries.tsx | 43 ++++++------------- src/tests/utils/sample-url-generation.spec.ts | 30 ++++++++++--- 4 files changed, 62 insertions(+), 50 deletions(-) diff --git a/src/app/utils/query-url-sanitization.ts b/src/app/utils/query-url-sanitization.ts index 2c1e453080..a26740cefc 100644 --- a/src/app/utils/query-url-sanitization.ts +++ b/src/app/utils/query-url-sanitization.ts @@ -1,5 +1,4 @@ /* eslint-disable no-useless-escape */ -import { GRAPH_URL } from '../services/graph-constants'; import { isAllAlpha, sanitizeQueryParameter, @@ -55,6 +54,7 @@ export function sanitizeGraphAPISandboxUrl(url: string): string { */ export function sanitizeQueryUrl(url: string): string { url = decodeURIComponent(url); + const { origin } = new URL(url); const { search, queryVersion, requestUrl } = parseSampleUrl(url); const queryString: string = search @@ -82,7 +82,7 @@ export function sanitizeQueryUrl(url: string): string { }); } - return `${GRAPH_URL}/${queryVersion}/${resourceUrl}${queryString}`; + return `${origin}/${queryVersion}/${resourceUrl}${queryString}`; } /** diff --git a/src/app/utils/sample-url-generation.ts b/src/app/utils/sample-url-generation.ts index fad701f84f..8e1a7dde3d 100644 --- a/src/app/utils/sample-url-generation.ts +++ b/src/app/utils/sample-url-generation.ts @@ -1,4 +1,11 @@ -export function parseSampleUrl(url: string, version?: string) { +interface IParsedSample { + queryVersion: string; + requestUrl: string; + sampleUrl: string; + search: string; +} + +export function parseSampleUrl(url: string, version?: string): IParsedSample { let requestUrl = ''; let queryVersion = ''; let sampleUrl = ''; @@ -6,12 +13,10 @@ export function parseSampleUrl(url: string, version?: string) { if (url !== '') { try { - const urlObject: URL = new URL(url); - const { origin } = urlObject; queryVersion = (version) ? version : getGraphVersion(url); requestUrl = getRequestUrl(url, queryVersion); - search = generateSearchParameters(urlObject, search); - sampleUrl = `${origin}/${queryVersion}/${requestUrl + search}`; + search = generateSearchParameters(url, search); + sampleUrl = generateSampleUrl(url, queryVersion, requestUrl, search); } catch (error) { if (error.message === `Failed to construct 'URL': Invalid URL`) { return { @@ -26,20 +31,21 @@ export function parseSampleUrl(url: string, version?: string) { }; } -export function getRequestUrl(url: string, version: string): string { +function getRequestUrl(url: string, version: string): string { const { pathname } = new URL(url); - const requestContent = pathname.split(version + '/').pop(); + const versionToReplace = (pathname.startsWith(`/${version}`)) ? version : getGraphVersion(url); + const requestContent = pathname.split(versionToReplace + '/').pop()!; return decodeURIComponent(requestContent!.replace(/\/$/, '')); } -export function getGraphVersion(url: string): string { - const urlObject: URL = new URL(url); - const parts = urlObject.pathname.substring(1).split('/'); +function getGraphVersion(url: string): string { + const { pathname } = new URL(url); + const parts = pathname.substring(1).split('/'); return parts[0]; } -function generateSearchParameters(urlObject: URL, search: string) { - const searchParameters = urlObject.search; +function generateSearchParameters(url: string, search: string) { + const { search: searchParameters } = new URL(url); if (searchParameters) { try { search = decodeURI(searchParameters); @@ -52,3 +58,8 @@ function generateSearchParameters(urlObject: URL, search: string) { } return search; } + +function generateSampleUrl(url: string, queryVersion: string, requestUrl: string, search: string): string { + const { origin } = new URL(url); + return `${origin}/${queryVersion}/${requestUrl + search}`; +} diff --git a/src/app/views/sidebar/sample-queries/SampleQueries.tsx b/src/app/views/sidebar/sample-queries/SampleQueries.tsx index 45e05aa986..0c1f69a769 100644 --- a/src/app/views/sidebar/sample-queries/SampleQueries.tsx +++ b/src/app/views/sidebar/sample-queries/SampleQueries.tsx @@ -1,21 +1,7 @@ import { - Announced, - DetailsList, - DetailsRow, - FontSizes, - FontWeights, - getId, - GroupHeader, - IColumn, - Icon, - MessageBar, - MessageBarType, - SearchBox, - SelectionMode, - Spinner, - SpinnerSize, - styled, - TooltipHost, + Announced, DetailsList, DetailsRow, FontSizes, FontWeights, getId, + GroupHeader, IColumn, Icon, MessageBar, MessageBarType, SearchBox, + SelectionMode, Spinner, SpinnerSize, styled, TooltipHost, } from 'office-ui-fabric-react'; import React, { Component } from 'react'; import { FormattedMessage, injectIntl } from 'react-intl'; @@ -24,21 +10,17 @@ import { bindActionCreators, Dispatch } from 'redux'; import { geLocale } from '../../../../appLocale'; import { componentNames, eventTypes, telemetry } from '../../../../telemetry'; -import { - IQuery, - ISampleQueriesProps, - ISampleQuery, -} from '../../../../types/query-runner'; +import { IQuery, ISampleQueriesProps, ISampleQuery } from '../../../../types/query-runner'; import { IRootState } from '../../../../types/root'; import * as queryActionCreators from '../../../services/actions/query-action-creators'; import * as queryInputActionCreators from '../../../services/actions/query-input-action-creators'; import * as queryStatusActionCreators from '../../../services/actions/query-status-action-creator'; import * as samplesActionCreators from '../../../services/actions/samples-action-creators'; -import { GRAPH_URL } from '../../../services/graph-constants'; import { getStyleFor } from '../../../utils/badge-color'; import { validateExternalLink } from '../../../utils/external-link-validation'; import { generateGroupsFromList } from '../../../utils/generate-groups'; import { sanitizeQueryUrl } from '../../../utils/query-url-sanitization'; +import { parseSampleUrl } from '../../../utils/sample-url-generation'; import { substituteTokens } from '../../../utils/token-helpers'; import { classNames } from '../../classnames'; import { sidebarStyles } from '../Sidebar.styles'; @@ -247,15 +229,16 @@ export class SampleQueries extends Component { }; private querySelected = (query: any) => { - const { actions, tokenPresent, profile } = this.props; + const { actions, cloud, tokenPresent, profile } = this.props; const selectedQuery = query; if (!selectedQuery) { return; } - const queryVersion = selectedQuery.requestUrl.substring(1, 5); + const sampleUrl = cloud.baseUrl + selectedQuery.requestUrl; + const { queryVersion } = parseSampleUrl(sampleUrl); const sampleQuery: IQuery = { - sampleUrl: GRAPH_URL + selectedQuery.requestUrl, + sampleUrl, selectedVerb: selectedQuery.method, sampleBody: selectedQuery.postBody, sampleHeaders: selectedQuery.headers || [], @@ -295,7 +278,8 @@ export class SampleQueries extends Component { }; private trackSampleQueryClickEvent(selectedQuery: ISampleQuery) { - const sanitizedUrl = sanitizeQueryUrl(GRAPH_URL + selectedQuery.requestUrl); + const { cloud } = this.props; + const sanitizedUrl = sanitizeQueryUrl(cloud.baseUrl + selectedQuery.requestUrl); telemetry.trackEvent( eventTypes.LISTITEM_CLICK_EVENT, { @@ -470,12 +454,13 @@ function displayTipMessage(actions: any, selectedQuery: ISampleQuery) { }); } -function mapStateToProps({ authToken, profile, samples, theme }: IRootState) { +function mapStateToProps({ authToken, profile, samples, theme, cloud }: IRootState) { return { tokenPresent: !!authToken, profile, samples, - appTheme: theme + appTheme: theme, + cloud }; } diff --git a/src/tests/utils/sample-url-generation.spec.ts b/src/tests/utils/sample-url-generation.spec.ts index d49553e984..834aca8d47 100644 --- a/src/tests/utils/sample-url-generation.spec.ts +++ b/src/tests/utils/sample-url-generation.spec.ts @@ -1,4 +1,4 @@ -import { parseSampleUrl, getGraphVersion, getRequestUrl } from '../../app/utils/sample-url-generation'; +import { parseSampleUrl } from '../../app/utils/sample-url-generation'; describe('Sample Url Generation', () => { @@ -78,8 +78,8 @@ describe('Sample Url Generation', () => { it('returns appropriate version number', () => { const url = `https://graph.microsoft.com/v1.0/users`; const expectedVersion = 'v1.0'; - const version = getGraphVersion(url); - expect(expectedVersion).toEqual(version); + const parsedUrl = parseSampleUrl(url); + expect(parsedUrl.queryVersion).toEqual(expectedVersion); }); it('destructures sample url with long version number', () => { @@ -98,10 +98,26 @@ describe('Sample Url Generation', () => { it('returns appropriate request url for long version number', () => { const version = 'longversionnumberv2'; - const request = 'me/messages'; - const url = `https://graph.microsoft.com/${version}/${request}`; - const requestUrl = getRequestUrl(url, version); - expect(requestUrl).toEqual(request); + const requestUrl = 'me/messages'; + const url = `https://graph.microsoft.com/${version}/${requestUrl}`; + const parsedUrl = parseSampleUrl(url); + expect(parsedUrl.requestUrl).toEqual(requestUrl); + }); + + it('replace version number with new one', () => { + const version = 'beta'; + const requestUrl = 'me/messages'; + const url = `https://graph.microsoft.com/v1.0/${requestUrl}`; + + const expectedUrl = { + requestUrl, + queryVersion: version, + sampleUrl: `https://graph.microsoft.com/${version}/${requestUrl}`, + search: '' + }; + + const parsedUrl = parseSampleUrl(url, version); + expect(parsedUrl).toEqual(expectedUrl); }); }); From ed412ae9247ad5dd8de92b0916d7d6c73b86e72f Mon Sep 17 00:00:00 2001 From: Charles Wahome Date: Thu, 6 May 2021 18:42:37 +0300 Subject: [PATCH 08/45] update base links with current cloud on reload and selection --- src/app/services/reducers/cloud-reducer.ts | 9 +-- src/app/utils/cloud-resolver.ts | 40 ------------- src/app/views/App.tsx | 16 ++++- .../auto-complete/AutoComplete.tsx | 10 ---- src/index.tsx | 21 ++----- src/modules/cloud-resolver/index.ts | 58 +++++++++++++++++++ src/types/query-runner.ts | 2 + 7 files changed, 79 insertions(+), 77 deletions(-) delete mode 100644 src/app/utils/cloud-resolver.ts create mode 100644 src/modules/cloud-resolver/index.ts diff --git a/src/app/services/reducers/cloud-reducer.ts b/src/app/services/reducers/cloud-reducer.ts index ae86a12a1c..ba79591283 100644 --- a/src/app/services/reducers/cloud-reducer.ts +++ b/src/app/services/reducers/cloud-reducer.ts @@ -1,14 +1,9 @@ +import { globalCloud } from '../../../modules/cloud-resolver'; import { IAction } from '../../../types/action'; import { ICloud } from '../../../types/cloud'; -import { AUTH_URL, GRAPH_URL } from '../graph-constants'; import { SET_ACTIVE_CLOUD_SUCCESS } from '../redux-constants'; -const initialState: ICloud = { - baseUrl: GRAPH_URL, - locale: 'global', - loginUrl: AUTH_URL, - name: 'global' -} +const initialState: ICloud = globalCloud; export function cloud(state = initialState, action: IAction): ICloud { switch (action.type) { diff --git a/src/app/utils/cloud-resolver.ts b/src/app/utils/cloud-resolver.ts deleted file mode 100644 index 90e233e199..0000000000 --- a/src/app/utils/cloud-resolver.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { geLocale } from '../../appLocale'; -import { ICloud } from '../../types/cloud'; - -export const clouds: ICloud[] = [{ - locale: 'de-de', - name: 'German', - baseUrl: 'https://graph.microsoft.de', - loginUrl: 'https://login.microsoftonline.de' -}, -{ - locale: 'zh-cn', - name: 'China', - baseUrl: 'https://microsoftgraph.chinacloudapi.cn', - loginUrl: 'https://portal.azure.cn' -}, { - locale: 'en-us', - name: 'Canary', - baseUrl: 'https://canary.graph.microsoft.com', - loginUrl: 'https://login.microsoftonline.com' -}]; - -export function getCurrentCloud(): ICloud | undefined { - const cloudName = localStorage.getItem('cloud'); - if (cloudName) { - return getCloudProperties(cloudName); - } - return undefined; -} - -export function getEligibleCloud(): ICloud | undefined { - const localeValue = geLocale.toLowerCase(); - return clouds.find(k => k.locale === localeValue); -} - -export function getCloudProperties(cloudName: string): ICloud | undefined { - if (cloudName) { - return clouds.find(k => k.name === cloudName) - } - return undefined; -} \ No newline at end of file diff --git a/src/app/views/App.tsx b/src/app/views/App.tsx index 678c5ed14f..3cfd87e3ea 100644 --- a/src/app/views/App.tsx +++ b/src/app/views/App.tsx @@ -27,7 +27,7 @@ import { clearTermsOfUse } from '../services/actions/terms-of-use-action-creator import { changeThemeSuccess } from '../services/actions/theme-action-creator'; import { toggleSidebar } from '../services/actions/toggle-sidebar-action-creator'; import { GRAPH_URL } from '../services/graph-constants'; -import { getCloudProperties, getCurrentCloud, getEligibleCloud } from '../utils/cloud-resolver'; +import { getCloudProperties, getCurrentCloud, getEligibleCloud, replaceBaseUrl, storeCloudValue } from '../../modules/cloud-resolver'; import { parseSampleUrl } from '../utils/sample-url-generation'; import { substituteTokens } from '../utils/token-helpers'; import { translateMessage } from '../utils/translate-messages'; @@ -148,8 +148,18 @@ class App extends Component { } public setCloud = () => { - localStorage.setItem('cloud', this.state.cloud!); - this.props.actions!.setActiveCloud(getCloudProperties(this.state.cloud!)) + const { cloud } = this.state; + + if (!cloud) { + return; + } + + storeCloudValue(cloud); + const cloudProperties = getCloudProperties(cloud); + this.props.actions!.setActiveCloud(cloudProperties); + const query = { ...this.props.sampleQuery }; + query.sampleUrl = replaceBaseUrl(query.sampleUrl); + this.props.actions!.setSampleQuery(query); this.toggleConfirmCloud(); } diff --git a/src/app/views/query-runner/query-input/auto-complete/AutoComplete.tsx b/src/app/views/query-runner/query-input/auto-complete/AutoComplete.tsx index f939064237..b4ca2df3c0 100644 --- a/src/app/views/query-runner/query-input/auto-complete/AutoComplete.tsx +++ b/src/app/views/query-runner/query-input/auto-complete/AutoComplete.tsx @@ -255,16 +255,6 @@ class AutoComplete extends Component { }); } } - - const origin = new URL(this.state.queryUrl).origin; - if (origin !== this.props.cloud.baseUrl) { - const queryUrl = this.state.queryUrl.replace(origin, this.props.cloud.baseUrl); - const userInput = this.state.userInput.replace(origin, this.props.cloud.baseUrl); - this.setState({ - queryUrl, - userInput - }); - } } private filterSuggestions(userInput: string, previousUserInput: string, compare: string, suggestions: string[]) { diff --git a/src/index.tsx b/src/index.tsx index 36d5b46c48..30068f58c8 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -15,14 +15,11 @@ import ru from 'react-intl/locale-data/ru'; import zh from 'react-intl/locale-data/zh'; import { Provider } from 'react-redux'; import { getAuthTokenSuccess, getConsentedScopesSuccess } from './app/services/actions/auth-action-creators'; -import { setActiveCloud } from './app/services/actions/cloud-action-creator'; import { setDevxApiUrl } from './app/services/actions/devxApi-action-creators'; import { setGraphExplorerMode } from './app/services/actions/explorer-mode-action-creator'; -import { setSampleQuery } from './app/services/actions/query-input-action-creators'; import { addHistoryItem } from './app/services/actions/request-history-action-creators'; import { changeThemeSuccess } from './app/services/actions/theme-action-creator'; -import { GRAPH_URL } from './app/services/graph-constants'; -import { getCloudProperties } from './app/utils/cloud-resolver'; +import { getCurrentCloud, globalCloud } from './modules/cloud-resolver'; import { isValidHttpsUrl } from './app/utils/external-link-validation'; import App from './app/views/App'; import { readHistoryData } from './app/views/sidebar/history/history-utils'; @@ -57,6 +54,8 @@ initializeIcons(); const currentTheme = readTheme(); loadGETheme(currentTheme); +const currentCloud = getCurrentCloud() || null; + const appState: any = store({ authToken: '', consentedScopes: [], @@ -64,7 +63,7 @@ const appState: any = store({ profile: null, queryRunnerStatus: null, sampleQuery: { - sampleUrl: `${GRAPH_URL}/v1.0/me`, + sampleUrl: `${(currentCloud) ? currentCloud.baseUrl : globalCloud.baseUrl}/v1.0/me`, selectedVerb: 'GET', sampleBody: undefined, sampleHeaders: [], @@ -135,18 +134,6 @@ readHistoryData().then((data: any) => { } }); -const cloudName = localStorage.getItem('cloud'); -if (cloudName) { - const cloud = getCloudProperties(cloudName); - const { baseUrl } = cloud!; - appState.dispatch(setActiveCloud(cloud!)); - const query: IQuery = appState.getState().sampleQuery; - const origin = new URL(query.sampleUrl).origin; - query.sampleUrl = query.sampleUrl.replace(origin, baseUrl); - - appState.dispatch(setSampleQuery(query)) -} - /** * Set's up Monaco Editor's Workers. */ diff --git a/src/modules/cloud-resolver/index.ts b/src/modules/cloud-resolver/index.ts new file mode 100644 index 0000000000..99225ceb15 --- /dev/null +++ b/src/modules/cloud-resolver/index.ts @@ -0,0 +1,58 @@ +import { AUTH_URL, GRAPH_URL } from '../../app/services/graph-constants'; +import { geLocale } from '../../appLocale'; +import { ICloud } from '../../types/cloud'; + +const storageKey = 'cloud'; + +export const clouds: ICloud[] = [ + { + locale: 'de-de', + name: 'German', + baseUrl: 'https://graph.microsoft.de', + loginUrl: 'https://login.microsoftonline.de' + }, + { + locale: 'zh-cn', + name: 'China', + baseUrl: 'https://microsoftgraph.chinacloudapi.cn', + loginUrl: 'https://portal.azure.cn' + }, + { + locale: 'global', + name: 'Canary', + baseUrl: 'https://canary.graph.microsoft.com', + loginUrl: 'https://login.microsoftonline.com' + } +]; + +export const globalCloud: ICloud = { + baseUrl: GRAPH_URL, + locale: 'global', + loginUrl: AUTH_URL, + name: 'Global' +} + +export function getCurrentCloud(): ICloud | undefined { + const cloudName = localStorage.getItem(storageKey); + return (cloudName) ? getCloudProperties(cloudName) : undefined; +} + +export function getEligibleCloud(): ICloud | undefined { + const localeValue = geLocale.toLowerCase(); + return clouds.find(k => k.locale === localeValue); +} + +export function getCloudProperties(cloudName: string): ICloud | undefined { + return clouds.find(k => k.name === cloudName) || undefined; +} + +export function replaceBaseUrl(url: string): string { + const { origin } = new URL(url); + const currentCloud = getCurrentCloud() || null; + const baseUrl = (currentCloud) ? currentCloud.baseUrl : globalCloud.baseUrl; + return url.replace(origin, baseUrl); +} + +export function storeCloudValue(value: string): void { + localStorage.setItem(storageKey, value); +} \ No newline at end of file diff --git a/src/types/query-runner.ts b/src/types/query-runner.ts index c3541d309d..766433e869 100644 --- a/src/types/query-runner.ts +++ b/src/types/query-runner.ts @@ -1,4 +1,5 @@ import { ITheme } from '@uifabric/styling'; +import { ICloud } from './cloud'; import { Mode } from './enums'; export interface IQueryRunnerState { @@ -87,6 +88,7 @@ export interface ISampleQueriesProps { styles?: object; tokenPresent: boolean; profile: object; + cloud: ICloud; samples: { pending: boolean; queries: ISampleQuery[]; From 050180eac9b59a4f752e4aaf2ca31297ef848280 Mon Sep 17 00:00:00 2001 From: Charles Wahome Date: Thu, 6 May 2021 19:15:06 +0300 Subject: [PATCH 09/45] replace references to GRAPH_URL constant --- src/app/views/app-sections/StatusMessages.tsx | 3 +- src/app/views/sidebar/history/History.tsx | 4 +-- src/telemetry/filters.ts | 33 ++++++++++++------- 3 files changed, 25 insertions(+), 15 deletions(-) diff --git a/src/app/views/app-sections/StatusMessages.tsx b/src/app/views/app-sections/StatusMessages.tsx index d53601aef2..f35ec3437f 100644 --- a/src/app/views/app-sections/StatusMessages.tsx +++ b/src/app/views/app-sections/StatusMessages.tsx @@ -1,6 +1,7 @@ import { Link, MessageBar } from 'office-ui-fabric-react'; import React, { Fragment } from 'react'; import { FormattedMessage } from 'react-intl'; +import { replaceBaseUrl } from '../../../modules/cloud-resolver'; import { IQuery } from '../../../types/query-runner'; import { GRAPH_URL } from '../../services/graph-constants'; @@ -40,7 +41,7 @@ export function statusMessages(queryState: any, sampleQuery: IQuery, actions: an function setQuery(link: string) { const query: IQuery = { ...sampleQuery }; - query.sampleUrl = link; + query.sampleUrl = replaceBaseUrl(link); actions.setSampleQuery(query); }; diff --git a/src/app/views/sidebar/history/History.tsx b/src/app/views/sidebar/history/History.tsx index f274a6f988..9d9130b5e1 100644 --- a/src/app/views/sidebar/history/History.tsx +++ b/src/app/views/sidebar/history/History.tsx @@ -19,7 +19,6 @@ import * as queryActionCreators from '../../../services/actions/query-action-cre import * as queryInputActionCreators from '../../../services/actions/query-input-action-creators'; import * as queryStatusActionCreators from '../../../services/actions/query-status-action-creator'; import * as requestHistoryActionCreators from '../../../services/actions/request-history-action-creators'; -import { GRAPH_URL } from '../../../services/graph-constants'; import { dynamicSort } from '../../../utils/dynamic-sort'; import { generateGroupsFromList } from '../../../utils/generate-groups'; import { sanitizeQueryUrl } from '../../../utils/query-url-sanitization'; @@ -143,6 +142,7 @@ export class History extends Component { if (column) { const queryContent = item[column.fieldName as keyof any] as string; + const { requestUrl, search, queryVersion } = parseSampleUrl(queryContent); let color = currentTheme.palette.green; if (item.status > 300) { color = currentTheme.palette.red; @@ -223,7 +223,7 @@ export class History extends Component { aria-describedby={hostId} className={classes.queryContent} > - {queryContent.replace(GRAPH_URL, '')} + {`/${queryVersion}/${requestUrl + search}`} diff --git a/src/telemetry/filters.ts b/src/telemetry/filters.ts index c1ce84db51..acf79f5777 100644 --- a/src/telemetry/filters.ts +++ b/src/telemetry/filters.ts @@ -1,14 +1,10 @@ import { ITelemetryItem } from '@microsoft/applicationinsights-web'; import { - DEVX_API_URL, - GRAPH_API_SANDBOX_URL, - GRAPH_URL, - HOME_ACCOUNT_KEY, + DEVX_API_URL, GRAPH_API_SANDBOX_URL, + GRAPH_URL, HOME_ACCOUNT_KEY } from '../app/services/graph-constants'; -import { - sanitizeGraphAPISandboxUrl, - sanitizeQueryUrl, -} from '../app/utils/query-url-sanitization'; +import { sanitizeGraphAPISandboxUrl, sanitizeQueryUrl } from '../app/utils/query-url-sanitization'; +import { clouds } from '../modules/cloud-resolver'; export function filterTelemetryTypes(envelope: ITelemetryItem) { const baseType = envelope.baseType || ''; @@ -28,14 +24,15 @@ export function filterRemoteDependencyData(envelope: ITelemetryItem): boolean { return true; } - const targetsToInclude = [GRAPH_URL, DEVX_API_URL, GRAPH_API_SANDBOX_URL]; - const urlObject = new URL(baseData.target || ''); - if (!targetsToInclude.includes(urlObject.origin)) { + const targetsToInclude = getRemoteTargets(); + + const { origin } = new URL(baseData.target || ''); + if (!targetsToInclude.includes(origin)) { return false; } const target = baseData.target || ''; - switch (urlObject.origin) { + switch (origin) { case GRAPH_URL: baseData.name = sanitizeQueryUrl(target); break; @@ -47,6 +44,18 @@ export function filterRemoteDependencyData(envelope: ITelemetryItem): boolean { return true; } +function getRemoteTargets() { + let targetsToInclude = [GRAPH_URL, DEVX_API_URL, GRAPH_API_SANDBOX_URL]; + + const urls: string[] = []; + clouds.forEach(cloud => { + urls.push(cloud.baseUrl); + }); + + targetsToInclude = targetsToInclude.concat(urls); + return targetsToInclude; +} + export function addCommonTelemetryItemProperties(envelope: ITelemetryItem) { const telemetryItem = envelope.baseData || {}; telemetryItem.properties = telemetryItem.properties || {}; From 263eafa4f51e5e6d97f63998d98eb982968929d1 Mon Sep 17 00:00:00 2001 From: Charles Wahome Date: Thu, 6 May 2021 19:20:58 +0300 Subject: [PATCH 10/45] sanitise urls with cloud base urls --- src/telemetry/filters.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/telemetry/filters.ts b/src/telemetry/filters.ts index acf79f5777..45c14672dd 100644 --- a/src/telemetry/filters.ts +++ b/src/telemetry/filters.ts @@ -24,7 +24,7 @@ export function filterRemoteDependencyData(envelope: ITelemetryItem): boolean { return true; } - const targetsToInclude = getRemoteTargets(); + const targetsToInclude = [GRAPH_URL, DEVX_API_URL, GRAPH_API_SANDBOX_URL].concat(getCloudUrls()); const { origin } = new URL(baseData.target || ''); if (!targetsToInclude.includes(origin)) { @@ -39,21 +39,20 @@ export function filterRemoteDependencyData(envelope: ITelemetryItem): boolean { case GRAPH_API_SANDBOX_URL: baseData.name = sanitizeGraphAPISandboxUrl(target); default: + if (getCloudUrls().includes(origin)) { + baseData.name = sanitizeQueryUrl(target); + } break; } return true; } -function getRemoteTargets() { - let targetsToInclude = [GRAPH_URL, DEVX_API_URL, GRAPH_API_SANDBOX_URL]; - +function getCloudUrls() { const urls: string[] = []; clouds.forEach(cloud => { urls.push(cloud.baseUrl); }); - - targetsToInclude = targetsToInclude.concat(urls); - return targetsToInclude; + return urls; } export function addCommonTelemetryItemProperties(envelope: ITelemetryItem) { From 92ff83c3827f4e57d19c5f076e925310bc466283 Mon Sep 17 00:00:00 2001 From: Charles Wahome Date: Mon, 10 May 2021 19:58:32 +0300 Subject: [PATCH 11/45] remove German cloud support --- src/modules/cloud-resolver/index.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/modules/cloud-resolver/index.ts b/src/modules/cloud-resolver/index.ts index 99225ceb15..e783393aae 100644 --- a/src/modules/cloud-resolver/index.ts +++ b/src/modules/cloud-resolver/index.ts @@ -5,12 +5,6 @@ import { ICloud } from '../../types/cloud'; const storageKey = 'cloud'; export const clouds: ICloud[] = [ - { - locale: 'de-de', - name: 'German', - baseUrl: 'https://graph.microsoft.de', - loginUrl: 'https://login.microsoftonline.de' - }, { locale: 'zh-cn', name: 'China', From 0d3cb55fa60f753fbf1e5518963371fa381c2934 Mon Sep 17 00:00:00 2001 From: Charles Wahome Date: Mon, 10 May 2021 19:58:49 +0300 Subject: [PATCH 12/45] change login url with cloud change --- src/modules/authentication/AuthenticationWrapper.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/modules/authentication/AuthenticationWrapper.ts b/src/modules/authentication/AuthenticationWrapper.ts index 554f65088f..808d62a6cf 100644 --- a/src/modules/authentication/AuthenticationWrapper.ts +++ b/src/modules/authentication/AuthenticationWrapper.ts @@ -4,7 +4,8 @@ import { PopupRequest, SilentRequest } from '@azure/msal-browser'; -import { AUTH_URL, DEFAULT_USER_SCOPES, HOME_ACCOUNT_KEY } from '../../app/services/graph-constants'; +import { DEFAULT_USER_SCOPES, HOME_ACCOUNT_KEY } from '../../app/services/graph-constants'; +import { getCurrentCloud, globalCloud } from '../cloud-resolver'; import { geLocale } from '../../appLocale'; import { getCurrentUri } from './authUtils'; import IAuthenticationWrapper from './IAuthenticationWrapper'; @@ -125,11 +126,14 @@ export class AuthenticationWrapper implements IAuthenticationWrapper { const urlParams = new URLSearchParams(location.search); let tenant = urlParams.get('tenant'); + const currentCloud = getCurrentCloud() || null; + const authUrl = (currentCloud) ? currentCloud.loginUrl : globalCloud.loginUrl; + if (!tenant) { tenant = 'common'; } - return `${AUTH_URL}/${tenant}/`; + return `${authUrl}/${tenant}/`; } private async loginWithInteraction(userScopes: string[], sessionId?: string) { From 39a555410fad209feeb6451477102fdb209d3320 Mon Sep 17 00:00:00 2001 From: Charles Wahome Date: Tue, 11 May 2021 16:32:26 +0300 Subject: [PATCH 13/45] display UI to manually select sovereign cloud --- src/app/views/App.tsx | 6 +- src/app/views/settings/Settings.tsx | 94 ++++++++++++++++++++++++++++- src/messages/GE.json | 4 +- 3 files changed, 100 insertions(+), 4 deletions(-) diff --git a/src/app/views/App.tsx b/src/app/views/App.tsx index 3cfd87e3ea..d67f5e6e7f 100644 --- a/src/app/views/App.tsx +++ b/src/app/views/App.tsx @@ -154,8 +154,12 @@ class App extends Component { return; } + const cloudProperties = getCloudProperties(cloud) || null; + if (!cloudProperties) { + return; + } + storeCloudValue(cloud); - const cloudProperties = getCloudProperties(cloud); this.props.actions!.setActiveCloud(cloudProperties); const query = { ...this.props.sampleQuery }; query.sampleUrl = replaceBaseUrl(query.sampleUrl); diff --git a/src/app/views/settings/Settings.tsx b/src/app/views/settings/Settings.tsx index ceb11d93d7..7d9eaa3efa 100644 --- a/src/app/views/settings/Settings.tsx +++ b/src/app/views/settings/Settings.tsx @@ -8,6 +8,7 @@ import { getId, IconButton, Label, + MessageBarType, Panel, PanelType, PrimaryButton, @@ -18,24 +19,29 @@ import { FormattedMessage, injectIntl } from 'react-intl'; import { useDispatch, useSelector } from 'react-redux'; import { geLocale } from '../../../appLocale'; +import { clouds, getCloudProperties, getCurrentCloud, globalCloud, replaceBaseUrl, storeCloudValue } from '../../../modules/cloud-resolver'; 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 { setActiveCloud } from '../../services/actions/cloud-action-creator'; import { consentToScopes } from '../../services/actions/permissions-action-creator'; import { togglePermissionsPanel } from '../../services/actions/permissions-panel-action-creator'; +import { setSampleQuery } from '../../services/actions/query-input-action-creators'; +import { setQueryResponseStatus } from '../../services/actions/query-status-action-creator'; import { changeTheme } from '../../services/actions/theme-action-creator'; +import { translateMessage } from '../../utils/translate-messages'; import { Permission } from '../query-runner/request/permissions'; - function Settings(props: ISettingsProps) { const dispatch = useDispatch(); - const { permissionsPanelOpen } = useSelector((state: IRootState) => state); + const { permissionsPanelOpen, profile, sampleQuery } = useSelector((state: IRootState) => state); const [themeChooserDialogHidden, hideThemeChooserDialog] = useState(true); const [items, setItems] = useState([]); const [selectedPermissions, setSelectedPermissions] = useState([]); + const [cloudSelectorOpen, setCloudSelectorOpen] = useState(false) const { intl: { messages } @@ -44,6 +50,9 @@ function Settings(props: ISettingsProps) { const authenticated = useSelector((state: any) => (!!state.authToken)); const appTheme = useSelector((state: any) => (state.theme)); + const cloudOptions = getCloudOptions(); + const currentCloud = (getCurrentCloud() !== undefined) ? getCurrentCloud() : globalCloud; + useEffect(() => { const menuItems: any = [ { @@ -90,6 +99,14 @@ function Settings(props: ISettingsProps) { }, onClick: () => changePanelState(), }, + { + key: 'select-cloud', + text: messages['Select cloud'], + iconProps: { + iconName: 'Cloud', + }, + onClick: () => toggleCloudSelector(), + }, { key: 'sign-out', text: messages['sign out'], @@ -118,6 +135,30 @@ function Settings(props: ISettingsProps) { dispatch(signOut()); }; + function getCloudOptions() { + const options: any[] = []; + const userProfile: any = { ...profile }; + const emailAddress = userProfile.mail || userProfile.userPrincipalName; + + clouds.forEach(cloud => { + options.push({ + key: cloud.name, + text: cloud.name + }); + }); + + options.unshift({ + key: globalCloud.name, + text: globalCloud.name + }); + + if (!emailAddress || (emailAddress && !emailAddress.includes('@microsoft.com'))) { + const filteredOptions = options.filter(k => k.key !== 'Canary'); + return filteredOptions; + } + return options; + } + const handleChangeTheme = (selectedTheme: any) => { const newTheme: AppTheme = selectedTheme.key; dispatch(changeTheme(newTheme)); @@ -130,6 +171,24 @@ function Settings(props: ISettingsProps) { }); }; + const handleCloudSelection = (cloud: any) => { + let activeCloud = getCloudProperties(cloud.key) || null; + activeCloud = (activeCloud) ? activeCloud : globalCloud; + storeCloudValue(cloud.key); + dispatch(setActiveCloud(activeCloud)); + + const query = { ...sampleQuery }; + query.sampleUrl = replaceBaseUrl(query.sampleUrl); + dispatch(setSampleQuery(query)); + + dispatch(setQueryResponseStatus({ + statusText: translateMessage('Cloud selected'), + status: cloud.key, + ok: true, + messageType: MessageBarType.success + })); + } + const changePanelState = () => { let open = !!permissionsPanelOpen; open = !open; @@ -138,6 +197,12 @@ function Settings(props: ISettingsProps) { trackSelectPermissionsButtonClickEvent(); }; + const toggleCloudSelector = () => { + let open = !!cloudSelectorOpen; + open = !open; + setCloudSelectorOpen(open); + } + const trackSelectPermissionsButtonClickEvent = () => { telemetry.trackEvent( eventTypes.BUTTON_CLICK_EVENT, @@ -269,9 +334,34 @@ function Settings(props: ISettingsProps) { > + + ); + + } export default injectIntl(Settings); diff --git a/src/messages/GE.json b/src/messages/GE.json index 4879db279e..42c938b4c5 100644 --- a/src/messages/GE.json +++ b/src/messages/GE.json @@ -350,5 +350,7 @@ "This response contains an @odata.nextLink property.": "This response contains an @odata.nextLink property.", "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" + "Scope consent failed": "Scope consent failed", + "Select cloud": "Select cloud", + "Cloud selected": "Cloud selected" } \ No newline at end of file From f36ca7260c6dd89b39239fcb24f5a18e9854b307 Mon Sep 17 00:00:00 2001 From: Charles Wahome Date: Tue, 11 May 2021 16:41:55 +0300 Subject: [PATCH 14/45] reset to global cloud when logging out --- src/app/views/settings/Settings.tsx | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/app/views/settings/Settings.tsx b/src/app/views/settings/Settings.tsx index 7d9eaa3efa..a9f9853b73 100644 --- a/src/app/views/settings/Settings.tsx +++ b/src/app/views/settings/Settings.tsx @@ -132,6 +132,7 @@ function Settings(props: ISettingsProps) { }; const handleSignOut = () => { + setSelectedCloud(''); dispatch(signOut()); }; @@ -171,15 +172,19 @@ function Settings(props: ISettingsProps) { }); }; - const handleCloudSelection = (cloud: any) => { + const setSelectedCloud = (cloud: any) => { let activeCloud = getCloudProperties(cloud.key) || null; activeCloud = (activeCloud) ? activeCloud : globalCloud; - storeCloudValue(cloud.key); + storeCloudValue(activeCloud.name); dispatch(setActiveCloud(activeCloud)); const query = { ...sampleQuery }; query.sampleUrl = replaceBaseUrl(query.sampleUrl); dispatch(setSampleQuery(query)); + } + + const handleCloudSelection = (cloud: any) => { + setSelectedCloud(cloud); dispatch(setQueryResponseStatus({ statusText: translateMessage('Cloud selected'), @@ -360,8 +365,6 @@ function Settings(props: ISettingsProps) { ); - - } export default injectIntl(Settings); From 3d559b2c29987dabbfd4961d559e792514022bfe Mon Sep 17 00:00:00 2001 From: Charles Wahome Date: Wed, 12 May 2021 16:11:24 +0300 Subject: [PATCH 15/45] display link only when options are available --- src/app/views/settings/Settings.tsx | 126 +++++++++++++++------------- 1 file changed, 68 insertions(+), 58 deletions(-) diff --git a/src/app/views/settings/Settings.tsx b/src/app/views/settings/Settings.tsx index a9f9853b73..60ef8f52e1 100644 --- a/src/app/views/settings/Settings.tsx +++ b/src/app/views/settings/Settings.tsx @@ -1,25 +1,17 @@ import { - ChoiceGroup, - DefaultButton, - Dialog, - DialogFooter, - DialogType, - DropdownMenuItemType, - getId, - IconButton, - Label, - MessageBarType, - Panel, - PanelType, - PrimaryButton, - TooltipHost + ChoiceGroup, DefaultButton, Dialog, DialogFooter, DialogType, DropdownMenuItemType, + getId, IconButton, Label, MessageBarType, Panel, + PanelType, PrimaryButton, TooltipHost } from 'office-ui-fabric-react'; import React, { useEffect, useState } from 'react'; import { FormattedMessage, injectIntl } from 'react-intl'; import { useDispatch, useSelector } from 'react-redux'; import { geLocale } from '../../../appLocale'; -import { clouds, getCloudProperties, getCurrentCloud, globalCloud, replaceBaseUrl, storeCloudValue } from '../../../modules/cloud-resolver'; +import { + clouds, getCloudProperties, getCurrentCloud, globalCloud, replaceBaseUrl, + storeCloudValue +} from '../../../modules/cloud-resolver'; import { componentNames, eventTypes, telemetry } from '../../../telemetry'; import { loadGETheme } from '../../../themes'; import { AppTheme } from '../../../types/enums'; @@ -39,7 +31,7 @@ function Settings(props: ISettingsProps) { const dispatch = useDispatch(); const { permissionsPanelOpen, profile, sampleQuery } = useSelector((state: IRootState) => state); const [themeChooserDialogHidden, hideThemeChooserDialog] = useState(true); - const [items, setItems] = useState([]); + const [items, setItems] = useState([]); const [selectedPermissions, setSelectedPermissions] = useState([]); const [cloudSelectorOpen, setCloudSelectorOpen] = useState(false) @@ -50,11 +42,64 @@ function Settings(props: ISettingsProps) { const authenticated = useSelector((state: any) => (!!state.authToken)); const appTheme = useSelector((state: any) => (state.theme)); - const cloudOptions = getCloudOptions(); const currentCloud = (getCurrentCloud() !== undefined) ? getCurrentCloud() : globalCloud; + + const toggleThemeChooserDialogState = () => { + let hidden = themeChooserDialogHidden; + hidden = !hidden; + hideThemeChooserDialog(hidden); + telemetry.trackEvent( + eventTypes.BUTTON_CLICK_EVENT, + { + ComponentName: componentNames.THEME_CHANGE_BUTTON + }); + }; + + const handleSignOut = () => { + setSelectedCloud(''); + dispatch(signOut()); + }; + + const getCloudOptions = () => { + let options: any[] = []; + + clouds.forEach(cloud => { + options.push({ + key: cloud.name, + text: cloud.name + }); + }); + + options.unshift({ + key: globalCloud.name, + text: globalCloud.name + }); + + if (!canAccessCanary()) { + options = options.filter(k => k.key !== 'Canary'); + } + + if (!canAccessChinaCloud()) { + options = options.filter(k => k.key !== 'China'); + } + return options; + } + + const canAccessChinaCloud = () => { + return geLocale === 'zh-CN'; + } + + const canAccessCanary = () => { + const userProfile: any = { ...profile }; + const emailAddress = userProfile.mail || userProfile.userPrincipalName; + return (emailAddress && emailAddress.includes('@microsoft.com')); + } + + const cloudOptions = getCloudOptions(); + useEffect(() => { - const menuItems: any = [ + let menuItems: any[] = [ { key: 'office-dev-program', text: messages['Office Dev Program'], @@ -117,48 +162,13 @@ function Settings(props: ISettingsProps) { }, ); } - 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 = () => { - setSelectedCloud(''); - dispatch(signOut()); - }; - - function getCloudOptions() { - const options: any[] = []; - const userProfile: any = { ...profile }; - const emailAddress = userProfile.mail || userProfile.userPrincipalName; - - clouds.forEach(cloud => { - options.push({ - key: cloud.name, - text: cloud.name - }); - }); - options.unshift({ - key: globalCloud.name, - text: globalCloud.name - }); - - if (!emailAddress || (emailAddress && !emailAddress.includes('@microsoft.com'))) { - const filteredOptions = options.filter(k => k.key !== 'Canary'); - return filteredOptions; + if (cloudOptions.length === 1) { + menuItems = menuItems.filter(k => k.key !== 'select-cloud'); } - return options; - } + + setItems(menuItems); + }, [authenticated]); const handleChangeTheme = (selectedTheme: any) => { const newTheme: AppTheme = selectedTheme.key; From 765283573e507400525249da1a5381e1e3c84454 Mon Sep 17 00:00:00 2001 From: Charles Wahome Date: Wed, 12 May 2021 19:25:55 +0300 Subject: [PATCH 16/45] move sovereign cloud logic to own component --- src/app/views/settings/Settings.tsx | 122 +++--------------- .../sovereign-clouds/SovereignClouds.tsx | 71 ++++++++++ .../sovereign-clouds/cloud-options.ts | 50 +++++++ src/messages/GE.json | 3 +- 4 files changed, 142 insertions(+), 104 deletions(-) create mode 100644 src/app/views/settings/sovereign-clouds/SovereignClouds.tsx create mode 100644 src/app/views/settings/sovereign-clouds/cloud-options.ts diff --git a/src/app/views/settings/Settings.tsx b/src/app/views/settings/Settings.tsx index 60ef8f52e1..331e82c830 100644 --- a/src/app/views/settings/Settings.tsx +++ b/src/app/views/settings/Settings.tsx @@ -1,6 +1,6 @@ import { ChoiceGroup, DefaultButton, Dialog, DialogFooter, DialogType, DropdownMenuItemType, - getId, IconButton, Label, MessageBarType, Panel, + getId, IconButton, Label, Panel, PanelType, PrimaryButton, TooltipHost } from 'office-ui-fabric-react'; import React, { useEffect, useState } from 'react'; @@ -8,32 +8,25 @@ import { FormattedMessage, injectIntl } from 'react-intl'; import { useDispatch, useSelector } from 'react-redux'; import { geLocale } from '../../../appLocale'; -import { - clouds, getCloudProperties, getCurrentCloud, globalCloud, replaceBaseUrl, - storeCloudValue -} from '../../../modules/cloud-resolver'; 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 { setActiveCloud } from '../../services/actions/cloud-action-creator'; import { consentToScopes } from '../../services/actions/permissions-action-creator'; import { togglePermissionsPanel } from '../../services/actions/permissions-panel-action-creator'; -import { setSampleQuery } from '../../services/actions/query-input-action-creators'; -import { setQueryResponseStatus } from '../../services/actions/query-status-action-creator'; import { changeTheme } from '../../services/actions/theme-action-creator'; -import { translateMessage } from '../../utils/translate-messages'; import { Permission } from '../query-runner/request/permissions'; +import { Sovereign } from './sovereign-clouds/cloud-options'; +import { SovereignClouds } from './sovereign-clouds/SovereignClouds'; function Settings(props: ISettingsProps) { const dispatch = useDispatch(); - const { permissionsPanelOpen, profile, sampleQuery } = useSelector((state: IRootState) => state); + const { permissionsPanelOpen, profile } = useSelector((state: IRootState) => state); const [themeChooserDialogHidden, hideThemeChooserDialog] = useState(true); const [items, setItems] = useState([]); const [selectedPermissions, setSelectedPermissions] = useState([]); - const [cloudSelectorOpen, setCloudSelectorOpen] = useState(false) const { intl: { messages } @@ -41,9 +34,7 @@ function Settings(props: ISettingsProps) { const authenticated = useSelector((state: any) => (!!state.authToken)); const appTheme = useSelector((state: any) => (state.theme)); - - const currentCloud = (getCurrentCloud() !== undefined) ? getCurrentCloud() : globalCloud; - + const [cloudSelectorOpen, setCloudSelectorOpen] = useState(false); const toggleThemeChooserDialogState = () => { let hidden = themeChooserDialogHidden; @@ -57,46 +48,10 @@ function Settings(props: ISettingsProps) { }; const handleSignOut = () => { - setSelectedCloud(''); dispatch(signOut()); }; - const getCloudOptions = () => { - let options: any[] = []; - - clouds.forEach(cloud => { - options.push({ - key: cloud.name, - text: cloud.name - }); - }); - - options.unshift({ - key: globalCloud.name, - text: globalCloud.name - }); - - if (!canAccessCanary()) { - options = options.filter(k => k.key !== 'Canary'); - } - - if (!canAccessChinaCloud()) { - options = options.filter(k => k.key !== 'China'); - } - return options; - } - - const canAccessChinaCloud = () => { - return geLocale === 'zh-CN'; - } - - const canAccessCanary = () => { - const userProfile: any = { ...profile }; - const emailAddress = userProfile.mail || userProfile.userPrincipalName; - return (emailAddress && emailAddress.includes('@microsoft.com')); - } - - const cloudOptions = getCloudOptions(); + const cloudOptions = new Sovereign(profile).getOptions(); useEffect(() => { let menuItems: any[] = [ @@ -131,6 +86,14 @@ function Settings(props: ISettingsProps) { iconName: 'Color', }, onClick: () => toggleThemeChooserDialogState(), + }, + { + key: 'select-cloud', + text: messages['Select cloud'], + iconProps: { + iconName: 'Cloud', + }, + onClick: () => toggleCloudSelector(), } ]; @@ -144,14 +107,6 @@ function Settings(props: ISettingsProps) { }, onClick: () => changePanelState(), }, - { - key: 'select-cloud', - text: messages['Select cloud'], - iconProps: { - iconName: 'Cloud', - }, - onClick: () => toggleCloudSelector(), - }, { key: 'sign-out', text: messages['sign out'], @@ -168,7 +123,7 @@ function Settings(props: ISettingsProps) { } setItems(menuItems); - }, [authenticated]); + }, [authenticated, profile]); const handleChangeTheme = (selectedTheme: any) => { const newTheme: AppTheme = selectedTheme.key; @@ -182,28 +137,6 @@ function Settings(props: ISettingsProps) { }); }; - const setSelectedCloud = (cloud: any) => { - let activeCloud = getCloudProperties(cloud.key) || null; - activeCloud = (activeCloud) ? activeCloud : globalCloud; - storeCloudValue(activeCloud.name); - dispatch(setActiveCloud(activeCloud)); - - const query = { ...sampleQuery }; - query.sampleUrl = replaceBaseUrl(query.sampleUrl); - dispatch(setSampleQuery(query)); - } - - const handleCloudSelection = (cloud: any) => { - setSelectedCloud(cloud); - - dispatch(setQueryResponseStatus({ - statusText: translateMessage('Cloud selected'), - status: cloud.key, - ok: true, - messageType: MessageBarType.success - })); - } - const changePanelState = () => { let open = !!permissionsPanelOpen; open = !open; @@ -350,28 +283,11 @@ function Settings(props: ISettingsProps) { - ); diff --git a/src/app/views/settings/sovereign-clouds/SovereignClouds.tsx b/src/app/views/settings/sovereign-clouds/SovereignClouds.tsx new file mode 100644 index 0000000000..6b4c6bc343 --- /dev/null +++ b/src/app/views/settings/sovereign-clouds/SovereignClouds.tsx @@ -0,0 +1,71 @@ +import { + ChoiceGroup, DefaultButton, Dialog, + DialogFooter, DialogType, MessageBarType +} from 'office-ui-fabric-react'; +import React from 'react'; +import { useDispatch, useSelector } from 'react-redux'; + +import { + getCloudProperties, getCurrentCloud, globalCloud, + replaceBaseUrl, storeCloudValue +} from '../../../../modules/cloud-resolver'; +import { IRootState } from '../../../../types/root'; +import { setActiveCloud } from '../../../services/actions/cloud-action-creator'; +import { setSampleQuery } from '../../../services/actions/query-input-action-creators'; +import { setQueryResponseStatus } from '../../../services/actions/query-status-action-creator'; +import { translateMessage } from '../../../utils/translate-messages'; +import { Sovereign } from './cloud-options'; + +export const SovereignClouds = ({ cloudSelectorOpen, toggleCloudSelector }: any) => { + const dispatch = useDispatch(); + const { sampleQuery, profile } = useSelector((state: IRootState) => state); + + const cloudOptions = new Sovereign(profile).getOptions(); + const currentCloud = (getCurrentCloud() !== undefined) ? getCurrentCloud() : globalCloud; + + const handleCloudSelection = (cloud: any) => { + setSelectedCloud(cloud); + + dispatch(setQueryResponseStatus({ + statusText: translateMessage('Cloud selected'), + status: cloud.key, + ok: true, + messageType: MessageBarType.success + })); + } + + const setSelectedCloud = (cloud: any) => { + let activeCloud = getCloudProperties(cloud.key) || null; + activeCloud = (activeCloud) ? activeCloud : globalCloud; + storeCloudValue(activeCloud.name); + dispatch(setActiveCloud(activeCloud)); + + const query = { ...sampleQuery }; + query.sampleUrl = replaceBaseUrl(query.sampleUrl); + dispatch(setSampleQuery(query)); + } + + return ( + + ) +} diff --git a/src/app/views/settings/sovereign-clouds/cloud-options.ts b/src/app/views/settings/sovereign-clouds/cloud-options.ts new file mode 100644 index 0000000000..2fac2029a1 --- /dev/null +++ b/src/app/views/settings/sovereign-clouds/cloud-options.ts @@ -0,0 +1,50 @@ +import { geLocale } from '../../../../appLocale'; +import { clouds, globalCloud } from '../../../../modules/cloud-resolver'; + +export class Sovereign { + protected profile = null; + + constructor(profile: any) { + this.profile = profile; + } + public getOptions() { + let options: any[] = []; + + clouds.forEach(cloud => { + options.push({ + key: cloud.name, + text: cloud.name + }); + }); + + options.unshift({ + key: globalCloud.name, + text: globalCloud.name + }); + + if (!this.canAccessCanary()) { + options = options.filter(k => k.key !== 'Canary'); + } + + if (!this.canAccessChinaCloud()) { + options = options.filter(k => k.key !== 'China'); + } + return options; + } + + private canAccessChinaCloud() { + return geLocale === 'zh-CN'; + } + + private canAccessCanary() { + if (!this.profile) { + return false; + } + + const { mail, userPrincipalName }: any = this.profile; + const emailAddress = mail || userPrincipalName; + return (emailAddress && emailAddress.includes('@microsoft.com')); + } +} + + diff --git a/src/messages/GE.json b/src/messages/GE.json index 42c938b4c5..0a53d12a55 100644 --- a/src/messages/GE.json +++ b/src/messages/GE.json @@ -352,5 +352,6 @@ "and experiment on": " and experiment on", "Scope consent failed": "Scope consent failed", "Select cloud": "Select cloud", - "Cloud selected": "Cloud selected" + "Cloud selected": "Cloud selected", + "You have access to sovereign clouds": "You have access to sovereign clouds" } \ No newline at end of file From d3c1c5a1c9eb018d87d7d7e7ae1ca219eb8a7d2b Mon Sep 17 00:00:00 2001 From: Charles Wahome Date: Wed, 12 May 2021 20:16:11 +0300 Subject: [PATCH 17/45] reuse dialog in cloud selection pop up --- src/app/views/App.tsx | 62 +++---------------- .../sovereign-clouds/SovereignClouds.tsx | 25 +++++--- .../sovereign-clouds/cloud-options.ts | 6 +- 3 files changed, 30 insertions(+), 63 deletions(-) diff --git a/src/app/views/App.tsx b/src/app/views/App.tsx index d67f5e6e7f..db7fac137e 100644 --- a/src/app/views/App.tsx +++ b/src/app/views/App.tsx @@ -1,8 +1,6 @@ import { - Announced, - DefaultButton, Dialog, DialogFooter, DialogType, - IStackTokens, ITheme, PrimaryButton, - styled + Announced, IStackTokens, + ITheme, styled } from 'office-ui-fabric-react'; import React, { Component } from 'react'; import { InjectedIntl, injectIntl } from 'react-intl'; @@ -11,6 +9,7 @@ import { bindActionCreators, Dispatch } from 'redux'; import { geLocale } from '../../appLocale'; import { authenticationWrapper } from '../../modules/authentication'; +import { getCurrentCloud, getEligibleCloud } from '../../modules/cloud-resolver'; import { componentNames, eventTypes, telemetry } from '../../telemetry'; import { loadGETheme } from '../../themes'; import { ThemeContext } from '../../themes/theme-context'; @@ -27,7 +26,6 @@ import { clearTermsOfUse } from '../services/actions/terms-of-use-action-creator import { changeThemeSuccess } from '../services/actions/theme-action-creator'; import { toggleSidebar } from '../services/actions/toggle-sidebar-action-creator'; import { GRAPH_URL } from '../services/graph-constants'; -import { getCloudProperties, getCurrentCloud, getEligibleCloud, replaceBaseUrl, storeCloudValue } from '../../modules/cloud-resolver'; import { parseSampleUrl } from '../utils/sample-url-generation'; import { substituteTokens } from '../utils/token-helpers'; import { translateMessage } from '../utils/translate-messages'; @@ -43,8 +41,8 @@ import { QueryResponse } from './query-response'; import { QueryRunner } from './query-runner'; import { parse } from './query-runner/util/iframe-message-parser'; import { Settings } from './settings'; +import { SovereignClouds } from './settings/sovereign-clouds/SovereignClouds'; import { Sidebar } from './sidebar/Sidebar'; -import { setActiveCloud } from '../services/actions/cloud-action-creator'; interface IAppProps { theme?: ITheme; @@ -65,7 +63,6 @@ interface IAppProps { toggleSidebar: Function; signIn: Function; storeScopes: Function; - setActiveCloud: Function; }; } @@ -74,7 +71,6 @@ interface IAppState { mobileScreen: boolean; hideDialog: boolean; showCloudDialog: boolean; - cloud: string | undefined; } class App extends Component { @@ -86,7 +82,6 @@ class App extends Component { selectedVerb: 'GET', mobileScreen: false, showCloudDialog: false, - cloud: 'global service', hideDialog: true }; } @@ -141,32 +136,11 @@ class App extends Component { if (!currentCloud && eligibleCloud) { this.setState({ showCloudDialog: true, - cloud: eligibleCloud.name }) } } } - public setCloud = () => { - const { cloud } = this.state; - - if (!cloud) { - return; - } - - const cloudProperties = getCloudProperties(cloud) || null; - if (!cloudProperties) { - return; - } - - storeCloudValue(cloud); - this.props.actions!.setActiveCloud(cloudProperties); - const query = { ...this.props.sampleQuery }; - query.sampleUrl = replaceBaseUrl(query.sampleUrl); - this.props.actions!.setSampleQuery(query); - this.toggleConfirmCloud(); - } - public handleSharedQueries() { const { actions } = this.props; const queryStringParams = this.getQueryStringParams(); @@ -340,7 +314,7 @@ class App extends Component { } public render() { - const { showCloudDialog, cloud } = this.state; + const { showCloudDialog } = this.state; const classes = classNames(this.props); const { authenticated, graphExplorerMode, queryState, minimised, termsOfUse, sampleQuery, actions, sidebarProperties, intl: { messages } }: any = this.props; @@ -378,12 +352,6 @@ class App extends Component { sidebarWidth = layout = 'col-xs-12 col-sm-12'; } - const dialogContentProps = { - type: DialogType.largeHeader, - title: 'You have access to sovereign clouds', - subText: `Hey there! Would you like to access your information available in the ${cloud} cloud? You will need to log in once you choose yes` - } - return ( // @ts-ignore @@ -431,20 +399,11 @@ class App extends Component { - + ); } @@ -477,7 +436,6 @@ const mapDispatchToProps = (dispatch: Dispatch) => { clearTermsOfUse, runQuery, setSampleQuery, - setActiveCloud, toggleSidebar, ...authActionCreators, changeTheme: (newTheme: string) => { diff --git a/src/app/views/settings/sovereign-clouds/SovereignClouds.tsx b/src/app/views/settings/sovereign-clouds/SovereignClouds.tsx index 6b4c6bc343..a50f4e2b95 100644 --- a/src/app/views/settings/sovereign-clouds/SovereignClouds.tsx +++ b/src/app/views/settings/sovereign-clouds/SovereignClouds.tsx @@ -1,10 +1,10 @@ import { ChoiceGroup, DefaultButton, Dialog, - DialogFooter, DialogType, MessageBarType + DialogFooter, DialogType, IChoiceGroupOption, + MessageBarType } from 'office-ui-fabric-react'; import React from 'react'; import { useDispatch, useSelector } from 'react-redux'; - import { getCloudProperties, getCurrentCloud, globalCloud, replaceBaseUrl, storeCloudValue @@ -16,11 +16,12 @@ import { setQueryResponseStatus } from '../../../services/actions/query-status-a import { translateMessage } from '../../../utils/translate-messages'; import { Sovereign } from './cloud-options'; -export const SovereignClouds = ({ cloudSelectorOpen, toggleCloudSelector }: any) => { + +export const SovereignClouds = ({ cloudSelectorOpen, toggleCloudSelector, prompt = false }: any) => { const dispatch = useDispatch(); const { sampleQuery, profile } = useSelector((state: IRootState) => state); - const cloudOptions = new Sovereign(profile).getOptions(); + const cloudOptions: IChoiceGroupOption[] = new Sovereign(profile).getOptions(); const currentCloud = (getCurrentCloud() !== undefined) ? getCurrentCloud() : globalCloud; const handleCloudSelection = (cloud: any) => { @@ -34,6 +35,12 @@ export const SovereignClouds = ({ cloudSelectorOpen, toggleCloudSelector }: any) })); } + const dialogContentProps = { + type: DialogType.largeHeader, + title: translateMessage('You have access to sovereign clouds'), + subText: (prompt) ? `Hey there! Would you like to access your information available in another cloud? You will need to log in once you choose a cloud` : '' + } + const setSelectedCloud = (cloud: any) => { let activeCloud = getCloudProperties(cloud.key) || null; activeCloud = (activeCloud) ? activeCloud : globalCloud; @@ -49,17 +56,17 @@ export const SovereignClouds = ({ cloudSelectorOpen, toggleCloudSelector }: any)