diff --git a/package-lock.json b/package-lock.json index 6dcf12d8e..7d575312d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "graph-explorer-v2", - "version": "4.9.0", + "version": "4.10.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 556323030..06e69cdfa 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "graph-explorer-v2", - "version": "4.9.0", + "version": "4.10.0", "private": true, "dependencies": { "@azure/msal-browser": "2.12.0", diff --git a/src/app/middleware/telemetryMiddleware.ts b/src/app/middleware/telemetryMiddleware.ts index 7504eae93..6148dbb96 100644 --- a/src/app/middleware/telemetryMiddleware.ts +++ b/src/app/middleware/telemetryMiddleware.ts @@ -4,6 +4,7 @@ import { IAction } from '../../types/action'; import { IQuery } from '../../types/query-runner'; import { IRootState } from '../../types/root'; import { + FETCH_ADAPTIVE_CARD_ERROR, FETCH_SCOPES_ERROR, GET_SNIPPET_ERROR, SAMPLES_FETCH_ERROR, @@ -45,6 +46,15 @@ const telemetryMiddleware = ); break; } + case FETCH_ADAPTIVE_CARD_ERROR: { + trackException( + componentNames.GET_ADAPTIVE_CARD_ACTION, + state.sampleQuery, + action.response, + {} + ); + break; + } } return next(action); }; diff --git a/src/app/services/actions/adaptive-cards-action-creator.ts b/src/app/services/actions/adaptive-cards-action-creator.ts index 393b762bc..d14251980 100644 --- a/src/app/services/actions/adaptive-cards-action-creator.ts +++ b/src/app/services/actions/adaptive-cards-action-creator.ts @@ -1,11 +1,9 @@ -import { SeverityLevel } from '@microsoft/applicationinsights-web'; import * as AdaptiveCardsTemplateAPI from 'adaptivecards-templating'; -import { componentNames, errorTypes, telemetry } from '../../../telemetry'; import { IAction } from '../../../types/action'; import { IAdaptiveCardContent } from '../../../types/adaptivecard'; import { IQuery } from '../../../types/query-runner'; import { lookupTemplate } from '../../utils/adaptive-cards-lookup'; -import { sanitizeQueryUrl } from '../../utils/query-url-sanitization'; +import { ADAPTIVE_CARD_URL } from '../graph-constants'; import { FETCH_ADAPTIVE_CARD_ERROR, FETCH_ADAPTIVE_CARD_PENDING, @@ -56,24 +54,16 @@ export function getAdaptiveCard( dispatch(getAdaptiveCardPending()); try { - const response = await fetch(`https://templates.adaptivecards.io/graph.microsoft.com/${templateFileName}`); + const response = await fetch(`${ADAPTIVE_CARD_URL}/${templateFileName}`); const templatePayload = await response.json(); const card = createCardFromTemplate(templatePayload, payload); - const adaptiveCardContent: IAdaptiveCardContent = { card, template: templatePayload }; + const adaptiveCardContent: IAdaptiveCardContent = { + card, + template: templatePayload, + }; return dispatch(getAdaptiveCardSuccess(adaptiveCardContent)); - } catch (error) { // something wrong happened - const sanitizedUrl = sanitizeQueryUrl(sampleQuery.sampleUrl); - telemetry.trackException( - new Error(errorTypes.NETWORK_ERROR), - SeverityLevel.Error, - { - ComponentName: componentNames.GET_ADAPTIVE_CARD_ACTION, - QuerySignature: `${sampleQuery.selectedVerb} ${sanitizedUrl}`, - Message: `${error}` - } - ); return dispatch(getAdaptiveCardError(error)); } }; diff --git a/src/app/services/actions/permissions-action-creator.ts b/src/app/services/actions/permissions-action-creator.ts index 69bacc4d1..fc0498767 100644 --- a/src/app/services/actions/permissions-action-creator.ts +++ b/src/app/services/actions/permissions-action-creator.ts @@ -3,8 +3,8 @@ import { MessageBarType } from 'office-ui-fabric-react'; import { geLocale } from '../../../appLocale'; import { authenticationWrapper } from '../../../modules/authentication'; import { IAction } from '../../../types/action'; -import { IQuery } from '../../../types/query-runner'; import { IRequestOptions } from '../../../types/request'; +import { IRootState } from '../../../types/root'; import { sanitizeQueryUrl } from '../../utils/query-url-sanitization'; import { parseSampleUrl } from '../../utils/sample-url-generation'; import { translateMessage } from '../../utils/translate-messages'; @@ -39,14 +39,14 @@ export function fetchScopesError(response: object): IAction { }; } -export function fetchScopes(query?: IQuery): Function { +export function fetchScopes(): Function { return async (dispatch: Function, getState: Function) => { let hasUrl = false; // whether permissions are for a specific url try { - const { devxApi } = getState(); + const { devxApi, permissionsPanelOpen, sampleQuery: query }: IRootState = getState(); let permissionsUrl = `${devxApi.baseUrl}/permissions`; - if (query) { + if (!permissionsPanelOpen) { const signature = sanitizeQueryUrl(query.sampleUrl); const { requestUrl, sampleUrl } = parseSampleUrl(signature); diff --git a/src/app/services/actions/profile-action-creators.ts b/src/app/services/actions/profile-action-creators.ts index 7ec1ab19b..3fe2f9837 100644 --- a/src/app/services/actions/profile-action-creators.ts +++ b/src/app/services/actions/profile-action-creators.ts @@ -27,6 +27,15 @@ export function getProfileInfo(query: IQuery): Function { return (dispatch: Function) => { const respHeaders: any = {}; + if (!query.sampleHeaders) { + query.sampleHeaders = []; + } + + query.sampleHeaders.push({ + name: 'Cache-Control', + value: 'no-cache' + }); + return authenticatedRequest(dispatch, query).then(async (response: Response) => { if (response && response.ok) { diff --git a/src/app/services/actions/query-action-creator-util.ts b/src/app/services/actions/query-action-creator-util.ts index 41bef322d..753f2f395 100644 --- a/src/app/services/actions/query-action-creator-util.ts +++ b/src/app/services/actions/query-action-creator-util.ts @@ -1,12 +1,14 @@ import { AuthenticationHandlerOptions, - ResponseType, + ResponseType } from '@microsoft/microsoft-graph-client'; import { MSALAuthenticationProviderOptions } from '@microsoft/microsoft-graph-client/lib/src/MSALAuthenticationProviderOptions'; + import { IAction } from '../../../types/action'; import { ContentType } from '../../../types/enums'; import { IQuery } from '../../../types/query-runner'; import { IRequestOptions } from '../../../types/request'; +import { encodeHashCharacters } from '../../utils/query-url-sanitization'; import { authProvider, GraphClient } from '../graph-client'; import { DEFAULT_USER_SCOPES, GRAPH_API_SANDBOX_URL } from '../graph-constants'; import { QUERY_GRAPH_SUCCESS } from '../redux-constants'; @@ -21,8 +23,9 @@ export function queryResponse(response: object): IAction { export async function anonymousRequest(dispatch: Function, query: IQuery) { const authToken = '{token:https://graph.microsoft.com/}'; - const escapedUrl = encodeURIComponent(query.sampleUrl); - const graphUrl = `${GRAPH_API_SANDBOX_URL}/svc?url=${escapedUrl}`; + const escapedUrl = encodeURIComponent(encodeHashCharacters(query)); + const graphUrl = `${GRAPH_API_SANDBOX_URL}?url=${escapedUrl}`; + const sampleHeaders: any = {}; if (query.sampleHeaders && query.sampleHeaders.length > 0) { query.sampleHeaders.forEach((header) => { @@ -113,7 +116,7 @@ const makeRequest = (httpVerb: string, scopes: string[]): Function => { msalAuthOptions ); const client = GraphClient.getInstance() - .api(query.sampleUrl) + .api(encodeHashCharacters(query)) .middlewareOptions([middlewareOptions]) .headers(sampleHeaders) .responseType(ResponseType.RAW); diff --git a/src/app/services/graph-constants.ts b/src/app/services/graph-constants.ts index 8820c2430..e2782f3dd 100644 --- a/src/app/services/graph-constants.ts +++ b/src/app/services/graph-constants.ts @@ -4,5 +4,6 @@ export const USER_PICTURE_URL = `${GRAPH_URL}/beta/me/photo/$value`; export const AUTH_URL = 'https://login.microsoftonline.com'; export const DEFAULT_USER_SCOPES = 'openid profile User.Read'; export const DEVX_API_URL = 'https://graphexplorerapi.azurewebsites.net'; -export const GRAPH_API_SANDBOX_URL = 'https://proxy.apisandbox.msdn.microsoft.com'; +export const GRAPH_API_SANDBOX_URL = 'https://proxy.apisandbox.msdn.microsoft.com/svc'; export const HOME_ACCOUNT_KEY = 'fbf1ecbe-27ab-42d7-96d4-3e6b03682ee4'; +export const ADAPTIVE_CARD_URL = 'https://templates.adaptivecards.io/graph.microsoft.com'; diff --git a/src/app/utils/generate-groups.ts b/src/app/utils/generate-groups.ts index f38a86ee7..d3145f67f 100644 --- a/src/app/utils/generate-groups.ts +++ b/src/app/utils/generate-groups.ts @@ -1,6 +1,8 @@ +import { IGroup } from "office-ui-fabric-react"; + export function generateGroupsFromList(list: any[], property: string) { const map = new Map(); - const groups: any[] = []; + const groups: IGroup[] = []; let isCollapsed = false; let previousCount = 0; @@ -23,6 +25,7 @@ export function generateGroupsFromList(list: any[], property: string) { startIndex: previousCount, isCollapsed, count, + ariaLabel: listItem[property] + ' has ' + count + ' results' }); previousCount += count; } diff --git a/src/app/utils/query-url-sanitization.ts b/src/app/utils/query-url-sanitization.ts index 3af4498a0..eeee34e92 100644 --- a/src/app/utils/query-url-sanitization.ts +++ b/src/app/utils/query-url-sanitization.ts @@ -1,4 +1,5 @@ /* eslint-disable no-useless-escape */ +import { IQuery } from '../../types/query-runner'; import { GRAPH_URL } from '../services/graph-constants'; import { isAllAlpha, @@ -142,3 +143,7 @@ function sanitizeQueryParameters(queryString: string): string { queryString = queryString.substring(1); return queryString.split('&').map(sanitizeQueryParameter).join('&'); } + +export function encodeHashCharacters(query: IQuery): string { + return query.sampleUrl.replace(/#/g, '%2523'); +} diff --git a/src/app/views/authentication/Authentication.tsx b/src/app/views/authentication/Authentication.tsx index e28bf6a94..e014d2810 100644 --- a/src/app/views/authentication/Authentication.tsx +++ b/src/app/views/authentication/Authentication.tsx @@ -1,6 +1,6 @@ import { SeverityLevel } from '@microsoft/applicationinsights-web'; -import { Icon, Label, MessageBarType, Spinner, SpinnerSize, styled } from 'office-ui-fabric-react'; +import { Icon, Label, MessageBar, MessageBarType, Spinner, SpinnerSize, styled } from 'office-ui-fabric-react'; import React, { useState } from 'react'; import { FormattedMessage, injectIntl } from 'react-intl'; import { useDispatch, useSelector } from 'react-redux'; @@ -79,9 +79,9 @@ const Authentication = (props: any) => {
- + ; } diff --git a/src/app/views/authentication/auth-util-components/UtilComponents.tsx b/src/app/views/authentication/auth-util-components/UtilComponents.tsx index 5a701f1e8..8e2f0c475 100644 --- a/src/app/views/authentication/auth-util-components/UtilComponents.tsx +++ b/src/app/views/authentication/auth-util-components/UtilComponents.tsx @@ -1,6 +1,7 @@ import { IconButton, PrimaryButton } from 'office-ui-fabric-react'; import React from 'react'; import { FormattedMessage } from 'react-intl'; +import { translateMessage } from '../../../utils/translate-messages'; import Profile from '../profile/Profile'; export function showSignInButtonOrProfile( @@ -11,12 +12,12 @@ export function showSignInButtonOrProfile( ) { const signInButton = minimised ? signIn()} /> : signIn()} diff --git a/src/app/views/common/copy.ts b/src/app/views/common/copy.ts index 0007d6e50..f56d96b58 100644 --- a/src/app/views/common/copy.ts +++ b/src/app/views/common/copy.ts @@ -1,3 +1,6 @@ +import { telemetry } from '../../../telemetry'; +import { IQuery } from '../../../types/query-runner'; + export function genericCopy(text: string) { const element = document.createElement('textarea'); element.value = text; @@ -6,7 +9,7 @@ export function genericCopy(text: string) { document.execCommand('copy'); document.body.removeChild(element); - + return Promise.resolve('copied'); } @@ -21,4 +24,14 @@ export function copy(id: string) { textArea.blur(); return Promise.resolve('copied'); -} \ No newline at end of file +} + +export function trackedGenericCopy( + text: string, + componentName: string, + sampleQuery?: IQuery, + properties?: { [key: string]: string } +) { + genericCopy(text); + telemetry.trackCopyButtonClickEvent(componentName, sampleQuery, properties); +} diff --git a/src/app/views/common/share.ts b/src/app/views/common/share.ts index 8b8f3e54b..129b226a9 100644 --- a/src/app/views/common/share.ts +++ b/src/app/views/common/share.ts @@ -1,6 +1,7 @@ import { geLocale } from '../../../appLocale'; import { authenticationWrapper } from '../../../modules/authentication'; import { IQuery } from '../../../types/query-runner'; +import { encodeHashCharacters } from '../../utils/query-url-sanitization'; import { parseSampleUrl } from '../../utils/sample-url-generation'; /** @@ -10,7 +11,8 @@ import { parseSampleUrl } from '../../utils/sample-url-generation'; */ export const createShareLink = (sampleQuery: IQuery, authenticated?: boolean): string => { const { sampleBody, selectedVerb, sampleHeaders } = sampleQuery; - const { queryVersion, requestUrl, sampleUrl, search } = parseSampleUrl(sampleQuery.sampleUrl); + const { queryVersion, requestUrl, sampleUrl, search } = + parseSampleUrl(encodeHashCharacters(sampleQuery)); if (!sampleUrl) { return ''; diff --git a/src/app/views/query-response/QueryResponse.tsx b/src/app/views/query-response/QueryResponse.tsx index 83aca9325..f8f5c6ce2 100644 --- a/src/app/views/query-response/QueryResponse.tsx +++ b/src/app/views/query-response/QueryResponse.tsx @@ -194,6 +194,7 @@ const QueryResponse = (props: IQueryResponseProps) => { id='share-query-text' className='share-query-params' defaultValue={query} + aria-label={translateMessage('Share Query')} /> diff --git a/src/app/views/query-response/adaptive-cards/AdaptiveCard.tsx b/src/app/views/query-response/adaptive-cards/AdaptiveCard.tsx index e6c265897..4c712617c 100644 --- a/src/app/views/query-response/adaptive-cards/AdaptiveCard.tsx +++ b/src/app/views/query-response/adaptive-cards/AdaptiveCard.tsx @@ -5,16 +5,15 @@ import { FormattedMessage, injectIntl } from 'react-intl'; import { connect } from 'react-redux'; import { bindActionCreators, Dispatch } from 'redux'; -import { componentNames, eventTypes, telemetry } from '../../../../telemetry'; +import { componentNames, telemetry } from '../../../../telemetry'; import { IAdaptiveCardProps } from '../../../../types/adaptivecard'; import { IQuery } from '../../../../types/query-runner'; import { IRootState } from '../../../../types/root'; import { getAdaptiveCard } from '../../../services/actions/adaptive-cards-action-creator'; -import { sanitizeQueryUrl } from '../../../utils/query-url-sanitization'; import { translateMessage } from '../../../utils/translate-messages'; import { classNames } from '../../classnames'; import { Monaco } from '../../common'; -import { genericCopy } from '../../common/copy'; +import { trackedGenericCopy } from '../../common/copy'; import { queryResponseStyles } from './../queryResponse.styles'; class AdaptiveCard extends Component { @@ -146,13 +145,15 @@ class AdaptiveCard extends Component { { - genericCopy(JSON.stringify(data.template, null, 4)); - trackJsonSchemaCopyEvent(sampleQuery) - }} + onClick={async () => + trackedGenericCopy( + JSON.stringify(data.template, null, 4), + componentNames.JSON_SCHEMA_COPY_BUTTON, + sampleQuery)} /> { constructor(props: any) { @@ -36,7 +37,7 @@ class GraphToolkit extends Component { . -