From 2aa16281c6ba6bc86cb5a32be95cc8d9e321e7be Mon Sep 17 00:00:00 2001 From: Musale Martin Date: Thu, 28 Nov 2024 17:50:24 +0300 Subject: [PATCH 01/20] feat: add new components for Monaco and response display in v9 --- src/app/views/common/monaco/MonacoV9.tsx | 5 +++++ src/app/views/query-response/response/ResponseDisplayV9.tsx | 5 +++++ src/app/views/query-response/response/ResponseMessagesV9.tsx | 5 +++++ src/app/views/query-response/response/ResponseV9.tsx | 5 +++++ 4 files changed, 20 insertions(+) create mode 100644 src/app/views/common/monaco/MonacoV9.tsx create mode 100644 src/app/views/query-response/response/ResponseDisplayV9.tsx create mode 100644 src/app/views/query-response/response/ResponseMessagesV9.tsx create mode 100644 src/app/views/query-response/response/ResponseV9.tsx diff --git a/src/app/views/common/monaco/MonacoV9.tsx b/src/app/views/common/monaco/MonacoV9.tsx new file mode 100644 index 000000000..45871d083 --- /dev/null +++ b/src/app/views/common/monaco/MonacoV9.tsx @@ -0,0 +1,5 @@ +const MonacoV9 = ()=>{ + return

Monaco v9

+} + +export { MonacoV9 } diff --git a/src/app/views/query-response/response/ResponseDisplayV9.tsx b/src/app/views/query-response/response/ResponseDisplayV9.tsx new file mode 100644 index 000000000..454ccdf4c --- /dev/null +++ b/src/app/views/query-response/response/ResponseDisplayV9.tsx @@ -0,0 +1,5 @@ +const ResponseDisplayV9 = ()=>{ + return

Response display

+} + +export { ResponseDisplayV9 } diff --git a/src/app/views/query-response/response/ResponseMessagesV9.tsx b/src/app/views/query-response/response/ResponseMessagesV9.tsx new file mode 100644 index 000000000..a2e076168 --- /dev/null +++ b/src/app/views/query-response/response/ResponseMessagesV9.tsx @@ -0,0 +1,5 @@ +const ResponseMessagesV9 = ()=>{ + return

Response Messages

+} + +export { ResponseMessagesV9 } diff --git a/src/app/views/query-response/response/ResponseV9.tsx b/src/app/views/query-response/response/ResponseV9.tsx new file mode 100644 index 000000000..2ca62aca1 --- /dev/null +++ b/src/app/views/query-response/response/ResponseV9.tsx @@ -0,0 +1,5 @@ +const ResponseV9 = ()=>{ + return

Response

+} + +export { ResponseV9 } From 2ec1728c833a4e6dc32f08da956af774626a6b4e Mon Sep 17 00:00:00 2001 From: Musale Martin Date: Fri, 29 Nov 2024 14:24:04 +0300 Subject: [PATCH 02/20] feat: implement Monaco v9 editor and enhance response display functionality --- .../dimensions/dimensions-adjustment.ts | 2 +- src/app/views/common/index.ts | 5 +- src/app/views/common/monaco/MonacoV9.tsx | 51 +++++++++++++++++-- .../views/common/monaco/util/format-xml.ts | 4 +- .../response/ResponseDisplayV9.tsx | 34 +++++++++++-- .../query-response/response/ResponseV9.tsx | 42 +++++++++++++-- 6 files changed, 123 insertions(+), 15 deletions(-) diff --git a/src/app/views/common/dimensions/dimensions-adjustment.ts b/src/app/views/common/dimensions/dimensions-adjustment.ts index a8d8ef894..97960bb16 100644 --- a/src/app/views/common/dimensions/dimensions-adjustment.ts +++ b/src/app/views/common/dimensions/dimensions-adjustment.ts @@ -10,7 +10,7 @@ export function convertPxToVh(px: number){ return convertedHeight+ 'vh'; } -export function getResponseHeight(height: any, responseAreaExpanded: any) { +export function getResponseHeight(height: string, responseAreaExpanded: boolean) { let responseHeight = height; if (responseAreaExpanded) { responseHeight = '90vh'; diff --git a/src/app/views/common/index.ts b/src/app/views/common/index.ts index a9439b0bf..bbeec95f7 100644 --- a/src/app/views/common/index.ts +++ b/src/app/views/common/index.ts @@ -1,7 +1,8 @@ import { Image } from './image/Image'; import { Monaco } from './monaco/Monaco'; +import { MonacoV9 } from './monaco/MonacoV9'; export { - Monaco, - Image + Image, Monaco, MonacoV9 }; + diff --git a/src/app/views/common/monaco/MonacoV9.tsx b/src/app/views/common/monaco/MonacoV9.tsx index 45871d083..4b060d7b4 100644 --- a/src/app/views/common/monaco/MonacoV9.tsx +++ b/src/app/views/common/monaco/MonacoV9.tsx @@ -1,5 +1,50 @@ -const MonacoV9 = ()=>{ - return

Monaco v9

+import { Editor, OnChange } from '@monaco-editor/react'; +import { editor } from 'monaco-editor'; +import { formatJsonStringForAllBrowsers } from './util/format-json'; + +interface MonacoProps { + body: object | string | undefined; + onChange?: OnChange; + verb?: string; + language?: string; + readOnly?: boolean; + height?: string; + extraInfoElement?: JSX.Element; } -export { MonacoV9 } + +const MonacoV9 = (props: MonacoProps)=>{ + const { onChange, language, readOnly, height} = props; + const editorOptions: editor.IStandaloneEditorConstructionOptions={ + lineNumbers: 'off' as 'off', + automaticLayout: true, + minimap: { enabled: false }, + readOnly, + wordWrap: 'on' as 'on', + folding: true, + foldingStrategy: 'indentation', + showFoldingControls: 'always', + renderLineHighlight: 'none', + scrollBeyondLastLine: true, + overviewRulerBorder: false, + wordSeparators: '"' + } + let body = props.body; + const editorHeight = height ? height : '300px'; + if (body && typeof body !== 'string') { + body = formatJsonStringForAllBrowsers(body); + } + + + return
+
+} + +export { MonacoV9 }; + diff --git a/src/app/views/common/monaco/util/format-xml.ts b/src/app/views/common/monaco/util/format-xml.ts index a3623accc..41d15563f 100644 --- a/src/app/views/common/monaco/util/format-xml.ts +++ b/src/app/views/common/monaco/util/format-xml.ts @@ -3,14 +3,14 @@ * Formats the xml content so that it can be readable. Monaco does not have an inbuilt xml formatter * @returns string */ -export function formatXml(xml: any) { +export function formatXml(xml: string) { const PADDING = ' '.repeat(2); const reg = /(>)(<)(\/*)/g; let pad = 0; xml = xml.replace(reg, '$1\r\n$2$3'); - return xml.split('\r\n').map((node: any) => { + return xml.split('\r\n').map((node: string) => { let indent = 0; if (node.match(/.+<\/\w[^>]*>$/)) { indent = 0; diff --git a/src/app/views/query-response/response/ResponseDisplayV9.tsx b/src/app/views/query-response/response/ResponseDisplayV9.tsx index 454ccdf4c..60d46691d 100644 --- a/src/app/views/query-response/response/ResponseDisplayV9.tsx +++ b/src/app/views/query-response/response/ResponseDisplayV9.tsx @@ -1,5 +1,33 @@ -const ResponseDisplayV9 = ()=>{ - return

Response display

+import { ContentType } from '../../../../types/enums'; +import { isImageResponse } from '../../../services/actions/query-action-creator-util'; +import { Image, MonacoV9 } from '../../common'; +import { formatXml } from '../../common/monaco/util/format-xml'; + +interface ResponseDisplayProps { + contentType: ContentType; + body: string; + height: number; +} + +const ResponseDisplayV9 = (props: ResponseDisplayProps) => { + const { contentType, body, height } = props; + + switch (contentType) { + case ContentType.XML: + return ; + + case ContentType.HTML: + return ; + + default: + if (isImageResponse(contentType)) { + return profile image; + } + return ; + } } -export { ResponseDisplayV9 } +export default ResponseDisplayV9; diff --git a/src/app/views/query-response/response/ResponseV9.tsx b/src/app/views/query-response/response/ResponseV9.tsx index 2ca62aca1..ff2cb58b4 100644 --- a/src/app/views/query-response/response/ResponseV9.tsx +++ b/src/app/views/query-response/response/ResponseV9.tsx @@ -1,5 +1,39 @@ -const ResponseV9 = ()=>{ - return

Response

-} -export { ResponseV9 } + +import { useAppSelector } from '../../../../store'; +import { getContentType } from '../../../services/actions/query-action-creator-util'; +import { + convertVhToPx, getResponseEditorHeight, + getResponseHeight +} from '../../common/dimensions/dimensions-adjustment'; +import ResponseDisplay from './ResponseDisplay'; +import { ResponseMessages } from './ResponseMessages'; + +const Response = () => { + const response = useAppSelector((state) => state.dimensions.response); + const body = useAppSelector((state) => state.graphResponse.response.body); + const headers = useAppSelector((state) => state.graphResponse.response.headers); + const responseAreaExpanded = useAppSelector((state) => state.responseAreaExpanded); + + const defaultHeight = convertVhToPx(getResponseHeight(response.height, responseAreaExpanded), 220); + const monacoHeight = getResponseEditorHeight(150); + + const contentDownloadUrl = body?.contentDownloadUrl; + const throwsCorsError = body?.throwsCorsError; + const contentType = getContentType(headers); + + return ( +
+ + {!contentDownloadUrl && !throwsCorsError && headers && + } +
+ ); + +}; + +export default Response; \ No newline at end of file From 8e40ecb1aecc007eefdeea1bb80fa43093e743a5 Mon Sep 17 00:00:00 2001 From: Musale Martin Date: Fri, 29 Nov 2024 14:56:33 +0300 Subject: [PATCH 03/20] feat: implement ResponseV9 component and update pivot items to use it --- .../pivot-items/pivot-items.tsx | 9 +- .../response/ResponseMessagesV9.tsx | 127 +++++++++++++++++- .../query-response/response/ResponseV9.tsx | 10 +- .../views/query-response/response/index.ts | 4 +- 4 files changed, 137 insertions(+), 13 deletions(-) diff --git a/src/app/views/query-response/pivot-items/pivot-items.tsx b/src/app/views/query-response/pivot-items/pivot-items.tsx index 2ad2208a8..022439467 100644 --- a/src/app/views/query-response/pivot-items/pivot-items.tsx +++ b/src/app/views/query-response/pivot-items/pivot-items.tsx @@ -8,13 +8,13 @@ import { lookupTemplate } from '../../../utils/adaptive-cards-lookup'; import { validateExternalLink } from '../../../utils/external-link-validation'; import { lookupToolkitUrl } from '../../../utils/graph-toolkit-lookup'; import { translateMessage } from '../../../utils/translate-messages'; -import { darkThemeHostConfig, lightThemeHostConfig } from '../adaptive-cards/AdaptiveHostConfig'; -import { queryResponseStyles } from '../queryResponse.styles'; -import { Response } from '../response'; import { AdaptiveCards, GraphToolkit, ResponseHeaders, Snippets } from '../../common/lazy-loader/component-registry'; +import { darkThemeHostConfig, lightThemeHostConfig } from '../adaptive-cards/AdaptiveHostConfig'; +import { queryResponseStyles } from '../queryResponse.styles'; +import { Response, ResponseV9 } from '../response'; export const GetPivotItems = () => { const mode = useAppSelector((state)=> state.graphExplorerMode); @@ -73,7 +73,8 @@ export const GetPivotItems = () => { 'aria-controls': 'response-tab' }} > -
+ {/*
*/} +
, { - return

Response Messages

+import { Link, MessageBar, MessageBarType } from '@fluentui/react'; +import { useState } from 'react'; + +import { useAppDispatch, useAppSelector } from '../../../../store'; +import { Mode } from '../../../../types/enums'; +import { IQuery } from '../../../../types/query-runner'; +import { getContentType } from '../../../services/actions/query-action-creator-util'; +import { MOZILLA_CORS_DOCUMENTATION_LINK } from '../../../services/graph-constants'; +import { runQuery } from '../../../services/slices/graph-response.slice'; +import { setSampleQuery } from '../../../services/slices/sample-query.slice'; +import { translateMessage } from '../../../utils/translate-messages'; + +interface ODataLink { + link: string; + name: string; +} + +function getOdataLinkFromResponseBody(responseBody: any): ODataLink | null { + const odataLinks = ['nextLink', 'deltaLink']; + let data = null; + if (responseBody) { + odataLinks.forEach(link => { + if (responseBody[`@odata.${link}`]) { + data = { + link: responseBody[`@odata.${link}`], + name: link + }; + } + }); + } + return data; } -export { ResponseMessagesV9 } +export const ResponseMessagesV9 = () => { + const dispatch = useAppDispatch(); + const messageBars = []; + const body = useAppSelector((state)=> state.graphResponse.response.body); + const headers = useAppSelector((state)=> state.graphResponse.response.headers); + const sampleQuery = useAppSelector((state)=> state.sampleQuery); + const authToken= useAppSelector((state)=> state.auth.authToken); + const graphExplorerMode = useAppSelector((state)=> state.graphExplorerMode); + const [displayMessage, setDisplayMessage] = useState(true); + + const tokenPresent = !!authToken.token; + const contentType = getContentType(headers); + const odataLink = getOdataLinkFromResponseBody(body); + + const setQuery = () => { + const query: IQuery = { ...sampleQuery }; + query.sampleUrl = odataLink!.link; + dispatch(setSampleQuery(query)); + dispatch(runQuery(query)); + } + + // Display link to step to next result + if (odataLink) { + messageBars.push( + + {translateMessage('This response contains an @odata property.')}: @odata.{odataLink.name} + setQuery()} underline> +  {translateMessage('Click here to follow the link')} + + + ); + } + + // Display link to download file response + if (body?.contentDownloadUrl) { + messageBars.push( +
+ + {translateMessage('This response contains unviewable content')} + + {translateMessage('Click to download file')} +   + +
+ ); + } + + // Show CORS compliance message + if (body?.throwsCorsError) { + messageBars.push( +
+ + {translateMessage('Response content not available due to CORS policy')} + + {translateMessage('here')} + . + +
+ ); + } + + if (body && !tokenPresent && displayMessage && graphExplorerMode === Mode.Complete) { + messageBars.push( +
+ setDisplayMessage(false)} + dismissButtonAriaLabel={translateMessage('Close')} + > + {translateMessage('Using demo tenant')}{' '} + {translateMessage('To access your own data:')} + +
+ ); + } + + if (contentType === 'application/json' && typeof body === 'string') { + messageBars.push( +
+ setDisplayMessage(false)} + dismissButtonAriaLabel={translateMessage('Close')} + > + {translateMessage('Malformed JSON body')} + +
+ ); + } + + return messageBars; +} \ No newline at end of file diff --git a/src/app/views/query-response/response/ResponseV9.tsx b/src/app/views/query-response/response/ResponseV9.tsx index ff2cb58b4..454f18aa0 100644 --- a/src/app/views/query-response/response/ResponseV9.tsx +++ b/src/app/views/query-response/response/ResponseV9.tsx @@ -7,11 +7,11 @@ import { getResponseHeight } from '../../common/dimensions/dimensions-adjustment'; import ResponseDisplay from './ResponseDisplay'; -import { ResponseMessages } from './ResponseMessages'; +import { ResponseMessagesV9 } from './ResponseMessagesV9'; -const Response = () => { +const ResponseV9 = () => { const response = useAppSelector((state) => state.dimensions.response); - const body = useAppSelector((state) => state.graphResponse.response.body); + const body = useAppSelector((state) => state.graphResponse.response.body); const headers = useAppSelector((state) => state.graphResponse.response.headers); const responseAreaExpanded = useAppSelector((state) => state.responseAreaExpanded); @@ -24,7 +24,7 @@ const Response = () => { return (
- + {!contentDownloadUrl && !throwsCorsError && headers && { }; -export default Response; \ No newline at end of file +export default ResponseV9; \ No newline at end of file diff --git a/src/app/views/query-response/response/index.ts b/src/app/views/query-response/response/index.ts index 95ba6078c..d4b0f7765 100644 --- a/src/app/views/query-response/response/index.ts +++ b/src/app/views/query-response/response/index.ts @@ -1,2 +1,4 @@ import Response from './Response'; -export { Response }; +import ResponseV9 from './ResponseV9'; +export { Response, ResponseV9 }; + From c5a8cea737974aea3c8d672b24880e5ae4329b45 Mon Sep 17 00:00:00 2001 From: Musale Martin Date: Fri, 29 Nov 2024 16:33:55 +0300 Subject: [PATCH 04/20] feat: add OneNote and PDF content types, update response headers handling, and enhance response display components --- .../autocomplete-action-creators.spec.ts | 2 +- .../permissions-action-creator.spec.ts | 2 +- .../permissions-action-creator.util.ts | 6 +- src/app/services/actions/profile-actions.ts | 3 +- .../actions/query-action-creator-util.ts | 44 ++-- .../actions/query-action-creators.spec.ts | 6 +- .../resource-explorer-action-creators.spec.ts | 2 +- .../services/slices/graph-response.slice.ts | 54 +++-- .../pivot-items/pivot-items.tsx | 2 +- .../query-response/response/Response.tsx | 74 +++--- .../response/ResponseDisplay.tsx | 44 ++-- .../response/ResponseDisplayV9.tsx | 8 +- .../response/ResponseMessages.tsx | 228 +++++++++--------- .../response/ResponseMessagesV9.tsx | 9 +- .../query-response/response/ResponseV9.tsx | 15 +- .../views/query-response/response/index.ts | 4 +- src/app/views/sidebar/history/har-utils.ts | 4 +- src/types/enums.ts | 2 + src/types/history.ts | 3 +- src/types/query-response.ts | 13 +- 20 files changed, 267 insertions(+), 258 deletions(-) diff --git a/src/app/services/actions/autocomplete-action-creators.spec.ts b/src/app/services/actions/autocomplete-action-creators.spec.ts index e9b932faf..306fa1ffc 100644 --- a/src/app/services/actions/autocomplete-action-creators.spec.ts +++ b/src/app/services/actions/autocomplete-action-creators.spec.ts @@ -61,7 +61,7 @@ const mockState: ApplicationState = { isLoadingData: false, response: { body: undefined, - headers: undefined + headers: {} } }, snippets: { diff --git a/src/app/services/actions/permissions-action-creator.spec.ts b/src/app/services/actions/permissions-action-creator.spec.ts index 999336a6b..65df26a3e 100644 --- a/src/app/services/actions/permissions-action-creator.spec.ts +++ b/src/app/services/actions/permissions-action-creator.spec.ts @@ -82,7 +82,7 @@ const mockState: ApplicationState = { isLoadingData: false, response: { body: undefined, - headers: undefined + headers: {} } }, snippets: { diff --git a/src/app/services/actions/permissions-action-creator.util.ts b/src/app/services/actions/permissions-action-creator.util.ts index 5dc808d92..cf80dc28d 100644 --- a/src/app/services/actions/permissions-action-creator.util.ts +++ b/src/app/services/actions/permissions-action-creator.util.ts @@ -235,16 +235,14 @@ export class RevokePermissionsUtil { private static async makeExponentialFetch(scopes: string[], query: IQuery, condition?: (args?: any) => Promise) { - const respHeaders: any = {}; const response = await exponentialFetchRetry(() => makeGraphRequest(scopes)(query), 8, 100, condition); - return parseResponse(response, respHeaders); + return parseResponse(response); } private static async makeGraphRequest(scopes: string[], query: IQuery) { - const respHeaders: any = {}; const response = await makeGraphRequest(scopes)(query); - return parseResponse(response, respHeaders); + return parseResponse(response); } private trackRevokeConsentEvent = (status: string, permissionObject: any) => { diff --git a/src/app/services/actions/profile-actions.ts b/src/app/services/actions/profile-actions.ts index 573f6730e..f17f7508b 100644 --- a/src/app/services/actions/profile-actions.ts +++ b/src/app/services/actions/profile-actions.ts @@ -103,10 +103,9 @@ export async function getProfileImage(): Promise { export async function getProfileResponse(): Promise { const scopes = DEFAULT_USER_SCOPES.split(' '); - const respHeaders: Record = {}; const response = await makeGraphRequest(scopes)(query); - const userInfo = await parseResponse(response, respHeaders); + const userInfo = await parseResponse(response); return { userInfo, response diff --git a/src/app/services/actions/query-action-creator-util.ts b/src/app/services/actions/query-action-creator-util.ts index 4042dc9e8..45b37fbad 100644 --- a/src/app/services/actions/query-action-creator-util.ts +++ b/src/app/services/actions/query-action-creator-util.ts @@ -11,6 +11,7 @@ import { import { authenticationWrapper } from '../../../modules/authentication'; import { ApplicationState } from '../../../store'; import { ContentType } from '../../../types/enums'; +import { ResponseBody } from '../../../types/query-response'; import { IQuery } from '../../../types/query-runner'; import { IRequestOptions } from '../../../types/request'; import { IStatus } from '../../../types/status'; @@ -146,32 +147,35 @@ export function isBetaURLResponse(json: any) { return !!json?.account?.[0]?.source?.type?.[0]; } -export function getContentType(headers: any) { - let contentType = null; +export function getContentType(headers: Headers | Record): ContentType { + let contentType: ContentType = '' as unknown as ContentType; if (headers) { - let contentTypes = headers['content-type']; + let contentTypes: string | null = null; if (headers instanceof Headers) { contentTypes = headers.get('content-type'); + } else { + contentTypes = headers['content-type']; } if (contentTypes) { /* Example: application/json;odata.metadata=minimal;odata.streaming=true;IEEE754Compatible=false;charset=utf-8 * Take the first option after splitting since it is the only value useful in the description of the content */ const splitContentTypes = contentTypes.split(';'); - if (splitContentTypes.length > 0) { - contentType = splitContentTypes[0].toLowerCase(); - } + contentType = (splitContentTypes.length > 0) ? + splitContentTypes[0].toLowerCase() as ContentType : contentTypes as ContentType; } } return contentType; } -export function isFileResponse(headers: any) { - const contentDisposition = headers['content-disposition']; +export function isFileResponse(headers: Headers | Record) { + const contentDisposition: string | null = (headers instanceof Headers) ? + headers.get('content-disposition') : headers['content-disposition']; + if (contentDisposition) { const directives = contentDisposition.split(';'); - if (directives.contains('attachment')) { + if (directives.includes('attachment')) { return true; } } @@ -191,19 +195,16 @@ export function isFileResponse(headers: any) { return false; } -export async function generateResponseDownloadUrl( - response: Response, - respHeaders: any -) { +export async function generateResponseDownloadUrl(response: Response) { try { - const fileContents = await parseResponse(response, respHeaders); - const contentType = getContentType(respHeaders); + const fileContents = await parseResponse(response); + const contentType = getContentType(response.headers); if (fileContents) { const buffer = await response.arrayBuffer(); const blob = new Blob([buffer], { type: contentType }); return URL.createObjectURL(blob); } - } catch (error) { + } catch { return null; } } @@ -211,20 +212,13 @@ export async function generateResponseDownloadUrl( async function tryParseJson(textValue: string) { try { return JSON.parse(textValue); - } catch (error) { + } catch { return textValue; } } -export function parseResponse( - response: Response, - respHeaders: { [key: string]: string } = {} -): Promise { +export const parseResponse = (response: Response): Promise => { if (response && response.headers) { - response.headers.forEach((val: string, key: string) => { - respHeaders[key] = val; - }); - const contentType = getContentType(response.headers); switch (contentType) { case ContentType.Json: diff --git a/src/app/services/actions/query-action-creators.spec.ts b/src/app/services/actions/query-action-creators.spec.ts index 1641476cd..d24215858 100644 --- a/src/app/services/actions/query-action-creators.spec.ts +++ b/src/app/services/actions/query-action-creators.spec.ts @@ -1,11 +1,11 @@ /* eslint-disable max-len */ import configureMockStore from 'redux-mock-store'; +import { AnyAction } from '@reduxjs/toolkit'; +import { IQuery } from '../../../types/query-runner'; import { ADD_HISTORY_ITEM_SUCCESS, QUERY_GRAPH_RUNNING, QUERY_GRAPH_STATUS, QUERY_GRAPH_SUCCESS } from '../redux-constants'; import { runQuery } from '../slices/graph-response.slice'; import { mockThunkMiddleware } from './mockThunkMiddleware'; -import { AnyAction } from '@reduxjs/toolkit'; -import { IQuery } from '../../../types/query-runner'; const mockStore = configureMockStore([mockThunkMiddleware]); @@ -34,7 +34,7 @@ describe('Query action creators', () => { body: undefined, createdAt, duration: undefined, - headers: undefined, + headers: {}, method: undefined, responseHeaders: { diff --git a/src/app/services/actions/resource-explorer-action-creators.spec.ts b/src/app/services/actions/resource-explorer-action-creators.spec.ts index edba19e18..0cac92d6c 100644 --- a/src/app/services/actions/resource-explorer-action-creators.spec.ts +++ b/src/app/services/actions/resource-explorer-action-creators.spec.ts @@ -62,7 +62,7 @@ const mockState: ApplicationState = { isLoadingData: false, response: { body: undefined, - headers: undefined + headers: {} } }, snippets: { diff --git a/src/app/services/slices/graph-response.slice.ts b/src/app/services/slices/graph-response.slice.ts index 5c81b1caa..c7b5c98ab 100644 --- a/src/app/services/slices/graph-response.slice.ts +++ b/src/app/services/slices/graph-response.slice.ts @@ -8,7 +8,7 @@ import { historyCache } from '../../../modules/cache/history-utils'; import { ApplicationState } from '../../../store'; import { ContentType } from '../../../types/enums'; import { IHistoryItem } from '../../../types/history'; -import { IGraphResponse } from '../../../types/query-response'; +import { IGraphResponse, ResponseBody } from '../../../types/query-response'; import { IQuery } from '../../../types/query-runner'; import { IStatus } from '../../../types/status'; import { ClientError } from '../../utils/error-utils/ClientError'; @@ -31,15 +31,15 @@ const MAX_NUMBER_OF_RETRIES = 3; let CURRENT_RETRIES = 0; interface Result { - body: any; - headers: { [key: string]: string }; + body: ResponseBody; + headers: Headers | Record; } const initialState: IGraphResponse = { isLoadingData: false, response: { body: undefined, - headers: undefined + headers: {} } }; @@ -48,7 +48,7 @@ export const runQuery = createAsyncThunk( async (query: IQuery, { dispatch, getState, rejectWithValue }) => { const state = getState() as ApplicationState; const tokenPresent = !!state?.auth?.authToken?.token; - const respHeaders: { [key: string]: string } = {}; + const respHeaders = {}; const createdAt = new Date().toISOString(); try { @@ -56,12 +56,13 @@ export const runQuery = createAsyncThunk( ? await authenticatedRequest(query) : await anonymousRequest(query, getState); - const result: Result = await processResponse(response, respHeaders, dispatch, query); + const result: Result = await processResponse(response, dispatch, query); const duration = new Date().getTime() - new Date(createdAt).getTime(); const status = generateStatus({ duration, response }); dispatch(setQueryResponseStatus(status)); + // TODO: fix this api args const historyItem = generateHistoryItem(status, respHeaders, query, createdAt, result, duration); dispatch(addHistoryItem(historyItem)); @@ -96,7 +97,7 @@ const querySlice = createSlice({ state.isLoadingData = true; state.response = { body: undefined, - headers: undefined + headers: {} }; }) .addCase(runQuery.rejected, (state, action) => { @@ -111,7 +112,7 @@ const querySlice = createSlice({ state.isLoadingData = false; state.response = { body: undefined, - headers: undefined + headers: {} }; }) .addCase(runQuery.fulfilled, (state, action) => { @@ -130,13 +131,12 @@ const querySlice = createSlice({ export const { setQueryResponse } = querySlice.actions; export default querySlice.reducer; -async function processResponse(response: Response, respHeaders: { [key: string]: string }, - dispatch: Function, query: IQuery): Promise { - let result = await parseResponse(response, respHeaders); +async function processResponse(response: Response, dispatch: Function, query: IQuery): Promise { + let result = await parseResponse(response); if (response && response.ok) { CURRENT_RETRIES = 0; - if (isFileResponse(respHeaders)) { - const contentDownloadUrl = await generateResponseDownloadUrl(response, respHeaders); + if (isFileResponse(response.headers)) { + const contentDownloadUrl = await generateResponseDownloadUrl(response); if (contentDownloadUrl) { result = { contentDownloadUrl }; } @@ -151,7 +151,7 @@ async function processResponse(response: Response, respHeaders: { [key: string]: } } - return { body: result, headers: respHeaders }; + return { body: result, headers: response.headers }; } const generateStatus = ({ duration, response }: { duration: number; response: Response }): IStatus => { @@ -192,23 +192,27 @@ async function runReAuthenticatedRequest(response: Response, query: IQuery): Pro function generateHistoryItem( status: IStatus, - respHeaders: { [key: string]: string }, + respHeaders: Headers | Record, query: IQuery, createdAt: string, result: Result, duration: number ): IHistoryItem { - let response = { ...result }; - const responseHeaders = { ...respHeaders }; - const contentType = respHeaders['content-type']; + let response: Result = {body: {}, headers: {}}; + let contentType_: ContentType = '' as ContentType; + if (respHeaders instanceof Headers) { + contentType_ = respHeaders.get('content-type') as ContentType; + } else { + contentType_ = respHeaders['content-type'] + } - if (isImageResponse(contentType)) { - response = { ...response, body: 'Run the query to view the image' }; - responseHeaders['content-type'] = ContentType.Json; + if (isImageResponse(contentType_)) { + response = { ...result, body: 'Run the query to view the image' }; + Object.assign(respHeaders, {'content-type': ContentType.Json}) } if (isFileResponse(respHeaders)) { - response = { ...response, body: 'Run the query to generate file download URL' }; + response = { ...result, body: 'Run the query to generate file download URL' }; } const historyItem: IHistoryItem = { @@ -217,12 +221,12 @@ function generateHistoryItem( method: query.selectedVerb, headers: query.sampleHeaders, body: query.sampleBody, - responseHeaders, + responseHeaders: respHeaders, createdAt, status: status.status as number, statusText: status.statusText, duration, - result: response.body + result: response.body as object }; historyCache.writeHistoryData(historyItem); @@ -230,7 +234,7 @@ function generateHistoryItem( } async function handleError(error: Error, query: IQuery) { - let body = null; + let body: ResponseBody = {}; const status: IStatus = { messageType: MessageBarType.error, ok: false, diff --git a/src/app/views/query-response/pivot-items/pivot-items.tsx b/src/app/views/query-response/pivot-items/pivot-items.tsx index 022439467..e57a155b7 100644 --- a/src/app/views/query-response/pivot-items/pivot-items.tsx +++ b/src/app/views/query-response/pivot-items/pivot-items.tsx @@ -14,7 +14,7 @@ import { } from '../../common/lazy-loader/component-registry'; import { darkThemeHostConfig, lightThemeHostConfig } from '../adaptive-cards/AdaptiveHostConfig'; import { queryResponseStyles } from '../queryResponse.styles'; -import { Response, ResponseV9 } from '../response'; +import { ResponseV9 } from '../response'; export const GetPivotItems = () => { const mode = useAppSelector((state)=> state.graphExplorerMode); diff --git a/src/app/views/query-response/response/Response.tsx b/src/app/views/query-response/response/Response.tsx index c758604cd..598c43ba3 100644 --- a/src/app/views/query-response/response/Response.tsx +++ b/src/app/views/query-response/response/Response.tsx @@ -1,39 +1,39 @@ -import { useAppSelector } from '../../../../store'; -import { getContentType } from '../../../services/actions/query-action-creator-util'; -import { - convertVhToPx, getResponseEditorHeight, - getResponseHeight -} from '../../common/dimensions/dimensions-adjustment'; -import ResponseDisplay from './ResponseDisplay'; -import { ResponseMessages } from './ResponseMessages'; - -const Response = () => { - const response = useAppSelector((state) => state.dimensions.response); - const body = useAppSelector((state) => state.graphResponse.response.body); - const headers = useAppSelector((state) => state.graphResponse.response.headers); - const responseAreaExpanded = useAppSelector((state) => state.responseAreaExpanded); - - const defaultHeight = convertVhToPx(getResponseHeight(response.height, responseAreaExpanded), 220); - const monacoHeight = getResponseEditorHeight(150); - - const contentDownloadUrl = body?.contentDownloadUrl; - const throwsCorsError = body?.throwsCorsError; - const contentType = getContentType(headers); - - return ( -
- - {!contentDownloadUrl && !throwsCorsError && headers && - } -
- ); - -}; - -export default Response; \ No newline at end of file +// import { useAppSelector } from '../../../../store'; +// import { getContentType } from '../../../services/actions/query-action-creator-util'; +// import { +// convertVhToPx, getResponseEditorHeight, +// getResponseHeight +// } from '../../common/dimensions/dimensions-adjustment'; +// import ResponseDisplay from './ResponseDisplay'; +// import { ResponseMessages } from './ResponseMessages'; + +// const Response = () => { +// const response = useAppSelector((state) => state.dimensions.response); +// const body = useAppSelector((state) => state.graphResponse.response.body); +// const headers = useAppSelector((state) => state.graphResponse.response.headers); +// const responseAreaExpanded = useAppSelector((state) => state.responseAreaExpanded); + +// const defaultHeight = convertVhToPx(getResponseHeight(response.height, responseAreaExpanded), 220); +// const monacoHeight = getResponseEditorHeight(150); + +// const contentDownloadUrl = body?.contentDownloadUrl; +// const throwsCorsError = body?.throwsCorsError; +// const contentType = getContentType(headers); + +// return ( +//
+// +// {!contentDownloadUrl && !throwsCorsError && headers && +// } +//
+// ); + +// }; + +// export default Response; \ No newline at end of file diff --git a/src/app/views/query-response/response/ResponseDisplay.tsx b/src/app/views/query-response/response/ResponseDisplay.tsx index e85218bff..2eb8b5de1 100644 --- a/src/app/views/query-response/response/ResponseDisplay.tsx +++ b/src/app/views/query-response/response/ResponseDisplay.tsx @@ -1,27 +1,27 @@ -import { ContentType } from '../../../../types/enums'; -import { isImageResponse } from '../../../services/actions/query-action-creator-util'; -import { Image, Monaco } from '../../common'; -import { formatXml } from '../../common/monaco/util/format-xml'; +// import { ContentType } from '../../../../types/enums'; +// import { isImageResponse } from '../../../services/actions/query-action-creator-util'; +// import { Image, Monaco } from '../../common'; +// import { formatXml } from '../../common/monaco/util/format-xml'; -const ResponseDisplay = (properties: any) => { - const { contentType, body, height } = properties; +// const ResponseDisplay = (properties: any) => { +// const { contentType, body, height } = properties; - switch (contentType) { - case ContentType.XML: - return ; +// switch (contentType) { +// case ContentType.XML: +// return ; - case ContentType.HTML: - return ; +// case ContentType.HTML: +// return ; - default: - if (isImageResponse(contentType)) { - return profile image; - } - return ; - } -} +// default: +// if (isImageResponse(contentType)) { +// return profile image; +// } +// return ; +// } +// } -export default ResponseDisplay; +// export default ResponseDisplay; diff --git a/src/app/views/query-response/response/ResponseDisplayV9.tsx b/src/app/views/query-response/response/ResponseDisplayV9.tsx index 60d46691d..23f59fd63 100644 --- a/src/app/views/query-response/response/ResponseDisplayV9.tsx +++ b/src/app/views/query-response/response/ResponseDisplayV9.tsx @@ -6,7 +6,7 @@ import { formatXml } from '../../common/monaco/util/format-xml'; interface ResponseDisplayProps { contentType: ContentType; body: string; - height: number; + height: string; } const ResponseDisplayV9 = (props: ResponseDisplayProps) => { @@ -14,10 +14,10 @@ const ResponseDisplayV9 = (props: ResponseDisplayProps) => { switch (contentType) { case ContentType.XML: - return ; + return ; case ContentType.HTML: - return ; + return ; default: if (isImageResponse(contentType)) { @@ -26,7 +26,7 @@ const ResponseDisplayV9 = (props: ResponseDisplayProps) => { body={body} alt='profile image' />; } - return ; + return ; } } diff --git a/src/app/views/query-response/response/ResponseMessages.tsx b/src/app/views/query-response/response/ResponseMessages.tsx index 0f6dddc3a..4fe51ebae 100644 --- a/src/app/views/query-response/response/ResponseMessages.tsx +++ b/src/app/views/query-response/response/ResponseMessages.tsx @@ -1,126 +1,126 @@ -import { Link, MessageBar, MessageBarType } from '@fluentui/react'; -import { useState } from 'react'; +// import { Link, MessageBar, MessageBarType } from '@fluentui/react'; +// import { useState } from 'react'; -import { useAppDispatch, useAppSelector } from '../../../../store'; -import { Mode } from '../../../../types/enums'; -import { IQuery } from '../../../../types/query-runner'; -import { getContentType } from '../../../services/actions/query-action-creator-util'; -import { MOZILLA_CORS_DOCUMENTATION_LINK } from '../../../services/graph-constants'; -import { runQuery } from '../../../services/slices/graph-response.slice'; -import { setSampleQuery } from '../../../services/slices/sample-query.slice'; -import { translateMessage } from '../../../utils/translate-messages'; +// import { useAppDispatch, useAppSelector } from '../../../../store'; +// import { Mode } from '../../../../types/enums'; +// import { IQuery } from '../../../../types/query-runner'; +// import { getContentType } from '../../../services/actions/query-action-creator-util'; +// import { MOZILLA_CORS_DOCUMENTATION_LINK } from '../../../services/graph-constants'; +// import { runQuery } from '../../../services/slices/graph-response.slice'; +// import { setSampleQuery } from '../../../services/slices/sample-query.slice'; +// import { translateMessage } from '../../../utils/translate-messages'; -interface ODataLink { - link: string; - name: string; -} +// interface ODataLink { +// link: string; +// name: string; +// } -function getOdataLinkFromResponseBody(responseBody: any): ODataLink | null { - const odataLinks = ['nextLink', 'deltaLink']; - let data = null; - if (responseBody) { - odataLinks.forEach(link => { - if (responseBody[`@odata.${link}`]) { - data = { - link: responseBody[`@odata.${link}`], - name: link - }; - } - }); - } - return data; -} +// function getOdataLinkFromResponseBody(responseBody: any): ODataLink | null { +// const odataLinks = ['nextLink', 'deltaLink']; +// let data = null; +// if (responseBody) { +// odataLinks.forEach(link => { +// if (responseBody[`@odata.${link}`]) { +// data = { +// link: responseBody[`@odata.${link}`], +// name: link +// }; +// } +// }); +// } +// return data; +// } -export const ResponseMessages = () => { - const dispatch = useAppDispatch(); - const messageBars = []; - const body = useAppSelector((state)=> state.graphResponse.response.body); - const headers = useAppSelector((state)=> state.graphResponse.response.headers); - const sampleQuery = useAppSelector((state)=> state.sampleQuery); - const authToken= useAppSelector((state)=> state.auth.authToken); - const graphExplorerMode = useAppSelector((state)=> state.graphExplorerMode); - const [displayMessage, setDisplayMessage] = useState(true); +// export const ResponseMessages = () => { +// const dispatch = useAppDispatch(); +// const messageBars = []; +// const body = useAppSelector((state)=> state.graphResponse.response.body); +// const headers = useAppSelector((state)=> state.graphResponse.response.headers); +// const sampleQuery = useAppSelector((state)=> state.sampleQuery); +// const authToken= useAppSelector((state)=> state.auth.authToken); +// const graphExplorerMode = useAppSelector((state)=> state.graphExplorerMode); +// const [displayMessage, setDisplayMessage] = useState(true); - const tokenPresent = !!authToken.token; - const contentType = getContentType(headers); - const odataLink = getOdataLinkFromResponseBody(body); +// const tokenPresent = !!authToken.token; +// const contentType = getContentType(headers); +// const odataLink = getOdataLinkFromResponseBody(body); - const setQuery = () => { - const query: IQuery = { ...sampleQuery }; - query.sampleUrl = odataLink!.link; - dispatch(setSampleQuery(query)); - dispatch(runQuery(query)); - } +// const setQuery = () => { +// const query: IQuery = { ...sampleQuery }; +// query.sampleUrl = odataLink!.link; +// dispatch(setSampleQuery(query)); +// dispatch(runQuery(query)); +// } - // Display link to step to next result - if (odataLink) { - messageBars.push( - - {translateMessage('This response contains an @odata property.')}: @odata.{odataLink.name} - setQuery()} underline> -  {translateMessage('Click here to follow the link')} - - - ); - } +// // Display link to step to next result +// if (odataLink) { +// messageBars.push( +// +// {translateMessage('This response contains an @odata property.')}: @odata.{odataLink.name} +// setQuery()} underline> +//  {translateMessage('Click here to follow the link')} +// +// +// ); +// } - // Display link to download file response - if (body?.contentDownloadUrl) { - messageBars.push( -
- - {translateMessage('This response contains unviewable content')} - - {translateMessage('Click to download file')} -   - -
- ); - } +// // Display link to download file response +// if (body?.contentDownloadUrl) { +// messageBars.push( +//
+// +// {translateMessage('This response contains unviewable content')} +// +// {translateMessage('Click to download file')} +//   +// +//
+// ); +// } - // Show CORS compliance message - if (body?.throwsCorsError) { - messageBars.push( -
- - {translateMessage('Response content not available due to CORS policy')} - - {translateMessage('here')} - . - -
- ); - } +// // Show CORS compliance message +// if (body?.throwsCorsError) { +// messageBars.push( +//
+// +// {translateMessage('Response content not available due to CORS policy')} +// +// {translateMessage('here')} +// . +// +//
+// ); +// } - if (body && !tokenPresent && displayMessage && graphExplorerMode === Mode.Complete) { - messageBars.push( -
- setDisplayMessage(false)} - dismissButtonAriaLabel={translateMessage('Close')} - > - {translateMessage('Using demo tenant')}{' '} - {translateMessage('To access your own data:')} - -
- ); - } +// if (body && !tokenPresent && displayMessage && graphExplorerMode === Mode.Complete) { +// messageBars.push( +//
+// setDisplayMessage(false)} +// dismissButtonAriaLabel={translateMessage('Close')} +// > +// {translateMessage('Using demo tenant')}{' '} +// {translateMessage('To access your own data:')} +// +//
+// ); +// } - if (contentType === 'application/json' && typeof body === 'string') { - messageBars.push( -
- setDisplayMessage(false)} - dismissButtonAriaLabel={translateMessage('Close')} - > - {translateMessage('Malformed JSON body')} - -
- ); - } +// if (contentType === 'application/json' && typeof body === 'string') { +// messageBars.push( +//
+// setDisplayMessage(false)} +// dismissButtonAriaLabel={translateMessage('Close')} +// > +// {translateMessage('Malformed JSON body')} +// +//
+// ); +// } - return messageBars; -} \ No newline at end of file +// return messageBars; +// } \ No newline at end of file diff --git a/src/app/views/query-response/response/ResponseMessagesV9.tsx b/src/app/views/query-response/response/ResponseMessagesV9.tsx index c3bc08d51..b731422a1 100644 --- a/src/app/views/query-response/response/ResponseMessagesV9.tsx +++ b/src/app/views/query-response/response/ResponseMessagesV9.tsx @@ -3,6 +3,7 @@ import { useState } from 'react'; import { useAppDispatch, useAppSelector } from '../../../../store'; import { Mode } from '../../../../types/enums'; +import { CustomBody } from '../../../../types/query-response'; import { IQuery } from '../../../../types/query-runner'; import { getContentType } from '../../../services/actions/query-action-creator-util'; import { MOZILLA_CORS_DOCUMENTATION_LINK } from '../../../services/graph-constants'; @@ -65,12 +66,13 @@ export const ResponseMessagesV9 = () => { } // Display link to download file response - if (body?.contentDownloadUrl) { + const contentDownloadUrl = (body as CustomBody)?.contentDownloadUrl + if (contentDownloadUrl) { messageBars.push(
{translateMessage('This response contains unviewable content')} - + {translateMessage('Click to download file')}   @@ -79,7 +81,8 @@ export const ResponseMessagesV9 = () => { } // Show CORS compliance message - if (body?.throwsCorsError) { + const throwsCorsError = (body as CustomBody)?.throwsCorsError + if (throwsCorsError) { messageBars.push(
diff --git a/src/app/views/query-response/response/ResponseV9.tsx b/src/app/views/query-response/response/ResponseV9.tsx index 454f18aa0..ce8df983b 100644 --- a/src/app/views/query-response/response/ResponseV9.tsx +++ b/src/app/views/query-response/response/ResponseV9.tsx @@ -1,34 +1,35 @@ import { useAppSelector } from '../../../../store'; +import { CustomBody, ResponseBody } from '../../../../types/query-response'; import { getContentType } from '../../../services/actions/query-action-creator-util'; import { convertVhToPx, getResponseEditorHeight, getResponseHeight } from '../../common/dimensions/dimensions-adjustment'; -import ResponseDisplay from './ResponseDisplay'; +import ResponseDisplayV9 from './ResponseDisplayV9'; import { ResponseMessagesV9 } from './ResponseMessagesV9'; const ResponseV9 = () => { const response = useAppSelector((state) => state.dimensions.response); - const body = useAppSelector((state) => state.graphResponse.response.body); + const body = useAppSelector((state) => state.graphResponse.response.body); const headers = useAppSelector((state) => state.graphResponse.response.headers); const responseAreaExpanded = useAppSelector((state) => state.responseAreaExpanded); const defaultHeight = convertVhToPx(getResponseHeight(response.height, responseAreaExpanded), 220); const monacoHeight = getResponseEditorHeight(150); - const contentDownloadUrl = body?.contentDownloadUrl; - const throwsCorsError = body?.throwsCorsError; + const contentDownloadUrl = (body as CustomBody)?.contentDownloadUrl; + const throwsCorsError = (body as CustomBody)?.throwsCorsError; const contentType = getContentType(headers); return ( -
+
{!contentDownloadUrl && !throwsCorsError && headers && - }
diff --git a/src/app/views/query-response/response/index.ts b/src/app/views/query-response/response/index.ts index d4b0f7765..5b45d5e27 100644 --- a/src/app/views/query-response/response/index.ts +++ b/src/app/views/query-response/response/index.ts @@ -1,4 +1,4 @@ -import Response from './Response'; +// import Response from './Response'; import ResponseV9 from './ResponseV9'; -export { Response, ResponseV9 }; +export { ResponseV9 }; diff --git a/src/app/views/sidebar/history/har-utils.ts b/src/app/views/sidebar/history/har-utils.ts index 365bbfa6d..2afd86120 100644 --- a/src/app/views/sidebar/history/har-utils.ts +++ b/src/app/views/sidebar/history/har-utils.ts @@ -17,10 +17,10 @@ export function createHarEntry(query: IHistoryItem): Entry { }); } const responseHeaders: HarHeader[] = []; - Object.keys(query.responseHeaders).forEach((key) => { + Object.keys(query.responseHeaders as Record).forEach((key) => { const head: HarHeader = { name: key, - value: query.responseHeaders[key] + value: (query.responseHeaders as Record)[key] }; responseHeaders.push(head); }); diff --git a/src/types/enums.ts b/src/types/enums.ts index 2b6d03a18..e9287019f 100644 --- a/src/types/enums.ts +++ b/src/types/enums.ts @@ -16,6 +16,8 @@ export enum ContentType { HTML = 'text/html', BinaryResponse = 'application/octet-stream', TextCsv = 'text/csv', + OneNote = 'application/onenote', + Pdf = 'application/pdf' } export enum AppTheme { diff --git a/src/types/history.ts b/src/types/history.ts index b57e59531..567b3d4d2 100644 --- a/src/types/history.ts +++ b/src/types/history.ts @@ -1,4 +1,5 @@ import { ITheme } from '@fluentui/react'; +import { ContentType } from './enums'; import { Header } from './query-runner'; export interface IHistoryItem extends IHistory { @@ -17,7 +18,7 @@ interface IHistory { duration: number; body?: string; category?: string; - responseHeaders: { [key: string]: string }; + responseHeaders: Headers | Record; } export interface IHistoryProps { diff --git a/src/types/query-response.ts b/src/types/query-response.ts index 5d569395e..7060d9474 100644 --- a/src/types/query-response.ts +++ b/src/types/query-response.ts @@ -1,4 +1,4 @@ -import { Mode } from './enums'; +import { ContentType, Mode } from './enums'; import { IQuery } from './query-runner'; export interface IQueryResponseProps { @@ -18,7 +18,14 @@ export interface IQueryResponseProps { export interface IGraphResponse { isLoadingData: boolean; response: { - body: any | undefined; - headers: { [key: string]: string } | undefined; + body: ResponseBody; + headers: Headers | Record; } } + + +export interface CustomBody { + throwsCorsError: boolean, + contentDownloadUrl: string +} +export type ResponseBody = Partial | Response | string | object | null | undefined; From 4934e83855246cb71956c598361ae45fc14ef7b4 Mon Sep 17 00:00:00 2001 From: Musale Martin Date: Fri, 29 Nov 2024 17:20:17 +0300 Subject: [PATCH 05/20] feat: add @microsoft/microsoft-graph-types dependency, update response handling, and improve type safety in graph request functions --- package-lock.json | 7 +++ package.json | 3 +- .../permissions-action-creator.util.ts | 43 +++++++++---------- src/app/services/actions/profile-actions.ts | 2 +- .../actions/query-action-creator-util.ts | 14 +++--- .../services/slices/graph-response.slice.ts | 13 +++--- src/app/utils/fetch-retry-handler.ts | 6 +-- src/types/query-response.ts | 9 +++- 8 files changed, 54 insertions(+), 43 deletions(-) diff --git a/package-lock.json b/package-lock.json index 40ee443a1..23a68e3a2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "@microsoft/applicationinsights-react-js": "17.3.4", "@microsoft/applicationinsights-web": "3.3.3", "@microsoft/microsoft-graph-client": "3.0.7", + "@microsoft/microsoft-graph-types": "2.40.0", "@monaco-editor/react": "4.6.0", "@ms-ofb/officebrowserfeedbacknpm": "file:packages/officebrowserfeedbacknpm-1.6.6.tgz", "@reduxjs/toolkit": "2.2.7", @@ -5989,6 +5990,12 @@ } } }, + "node_modules/@microsoft/microsoft-graph-types": { + "version": "2.40.0", + "resolved": "https://registry.npmjs.org/@microsoft/microsoft-graph-types/-/microsoft-graph-types-2.40.0.tgz", + "integrity": "sha512-1fcPVrB/NkbNcGNfCy+Cgnvwxt6/sbIEEFgZHFBJ670zYLegENYJF8qMo7x3LqBjWX2/Eneq5BVVRCLTmlJN+g==", + "license": "MIT" + }, "node_modules/@microsoft/recognizers-text-data-types-timex-expression": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@microsoft/recognizers-text-data-types-timex-expression/-/recognizers-text-data-types-timex-expression-1.3.0.tgz", diff --git a/package.json b/package.json index 4d087c5a0..d7a4c1959 100644 --- a/package.json +++ b/package.json @@ -5,8 +5,6 @@ "dependencies": { "@augloop/types-core": "file:packages/types-core-2.16.189.tgz", "@axe-core/webdriverjs": "4.10.0", - "@fluentui/react-components": "9.55.1", - "@fluentui/react-icons": "2.0.264", "@azure/msal-browser": "3.26.1", "@babel/core": "7.26.0", "@babel/runtime": "7.26.0", @@ -17,6 +15,7 @@ "@microsoft/applicationinsights-react-js": "17.3.4", "@microsoft/applicationinsights-web": "3.3.3", "@microsoft/microsoft-graph-client": "3.0.7", + "@microsoft/microsoft-graph-types": "2.40.0", "@monaco-editor/react": "4.6.0", "@ms-ofb/officebrowserfeedbacknpm": "file:packages/officebrowserfeedbacknpm-1.6.6.tgz", "@reduxjs/toolkit": "2.2.7", diff --git a/src/app/services/actions/permissions-action-creator.util.ts b/src/app/services/actions/permissions-action-creator.util.ts index cf80dc28d..ef933117d 100644 --- a/src/app/services/actions/permissions-action-creator.util.ts +++ b/src/app/services/actions/permissions-action-creator.util.ts @@ -1,11 +1,13 @@ +import { User } from '@microsoft/microsoft-graph-types'; import { componentNames, eventTypes, telemetry } from '../../../telemetry'; import { IOAuthGrantPayload, IPermissionGrant } from '../../../types/permissions'; import { IUser } from '../../../types/profile'; +import { CustomBody, ResponseValue } from '../../../types/query-response'; import { IQuery } from '../../../types/query-runner'; import { RevokeScopesError } from '../../utils/error-utils/RevokeScopesError'; import { exponentialFetchRetry } from '../../utils/fetch-retry-handler'; import { GRAPH_URL } from '../graph-constants'; -import { makeGraphRequest, parseResponse } from './query-action-creator-util'; +import { parseResponse, makeGraphRequest as queryMakeGraphRequest } from './query-action-creator-util'; interface IPreliminaryChecksObject { defaultUserScopes: string[]; @@ -107,7 +109,9 @@ export class RevokePermissionsUtil { const tenantAdminQuery = { ...genericQuery }; tenantAdminQuery.sampleUrl = `${GRAPH_URL}/v1.0/me/memberOf`; const response = await RevokePermissionsUtil.makeExponentialFetch([], tenantAdminQuery); - return response ? response.value.some((value: any) => value.displayName === 'Global Administrator') : false + const value = (response as CustomBody).value + const isAdmin = value ? value.some((v: Partial)=>v?.displayName === 'Global Administrator') : false + return isAdmin } public async getUserPermissionChecks(preliminaryObject: PartialCheckObject): Promise<{ @@ -191,7 +195,7 @@ export class RevokePermissionsUtil { genericQuery.sampleUrl = `${GRAPH_URL}/v1.0/oauth2PermissionGrants?$filter=clientId eq '${servicePrincipalAppId}'`; genericQuery.sampleHeaders = [{ name: 'ConsistencyLevel', value: 'eventual' }]; const oAuthGrant = await RevokePermissionsUtil.makeExponentialFetch(scopes, genericQuery); - return oAuthGrant; + return oAuthGrant as IOAuthGrantPayload; } public permissionToRevokeInGrant(permissionsGrant: IPermissionGrant, allPrincipalGrant: IPermissionGrant, @@ -207,7 +211,8 @@ export class RevokePermissionsUtil { const currentAppId = process.env.REACT_APP_CLIENT_ID; genericQuery.sampleUrl = `${GRAPH_URL}/v1.0/servicePrincipals?$filter=appId eq '${currentAppId}'`; const response = await this.makeGraphRequest(scopes, genericQuery); - return response ? response.value[0].id : ''; + const value = (response as CustomBody)?.value + return value ? value[0]?.id ?? '' : '' } private async revokePermission(permissionGrantId: string, newScopes: string): Promise { @@ -219,30 +224,24 @@ export class RevokePermissionsUtil { patchQuery.sampleUrl = `${GRAPH_URL}/v1.0/oauth2PermissionGrants/${permissionGrantId}`; genericQuery.sampleHeaders = [{ name: 'ConsistencyLevel', value: 'eventual' }]; patchQuery.selectedVerb = 'PATCH'; - // eslint-disable-next-line no-useless-catch - try { - const response = await RevokePermissionsUtil.makeGraphRequest([], patchQuery); - const { error } = response; - if (error) { - return false; - } - return true; - } - catch (error: any) { - throw error; + + const response = await RevokePermissionsUtil.makeGraphRequest([], patchQuery); + const error = (response as CustomBody).error; + if (error) { + return false; } + return true; } - private static async makeExponentialFetch(scopes: string[], query: IQuery, condition?: - (args?: any) => Promise) { - const response = await exponentialFetchRetry(() => makeGraphRequest(scopes)(query), - 8, 100, condition); - return parseResponse(response); + private static async makeExponentialFetch( + scopes: string[], query: IQuery, condition?: (args?: unknown) => Promise) { + const response = await exponentialFetchRetry(() => queryMakeGraphRequest(scopes)(query), 8, 100, condition); + return parseResponse(response as Response); } private static async makeGraphRequest(scopes: string[], query: IQuery) { - const response = await makeGraphRequest(scopes)(query); - return parseResponse(response); + const response = await queryMakeGraphRequest(scopes)(query); + return parseResponse(response as Response); } private trackRevokeConsentEvent = (status: string, permissionObject: any) => { diff --git a/src/app/services/actions/profile-actions.ts b/src/app/services/actions/profile-actions.ts index f17f7508b..01bc5ccd4 100644 --- a/src/app/services/actions/profile-actions.ts +++ b/src/app/services/actions/profile-actions.ts @@ -105,7 +105,7 @@ export async function getProfileResponse(): Promise { const scopes = DEFAULT_USER_SCOPES.split(' '); const response = await makeGraphRequest(scopes)(query); - const userInfo = await parseResponse(response); + const userInfo = await parseResponse(response as Response); return { userInfo, response diff --git a/src/app/services/actions/query-action-creator-util.ts b/src/app/services/actions/query-action-creator-util.ts index 45b37fbad..d6ba29668 100644 --- a/src/app/services/actions/query-action-creator-util.ts +++ b/src/app/services/actions/query-action-creator-util.ts @@ -107,30 +107,30 @@ function createAuthenticatedRequest( export function makeGraphRequest(scopes: string[]) { return async (query: IQuery) => { - let response; + let response: ResponseBody; const graphRequest: GraphRequest = createAuthenticatedRequest(scopes, query); switch (query.selectedVerb) { case 'GET': - response = await graphRequest.get(); + response = await graphRequest.get() as ResponseBody; break; case 'POST': - response = await graphRequest.post(query.sampleBody); + response = await graphRequest.post(query.sampleBody) as ResponseBody; break; case 'PUT': - response = await graphRequest.put(query.sampleBody); + response = await graphRequest.put(query.sampleBody) as ResponseBody; break; case 'PATCH': - response = await graphRequest.patch(query.sampleBody); + response = await graphRequest.patch(query.sampleBody) as ResponseBody; break; case 'DELETE': - response = await graphRequest.delete(); + response = await graphRequest.delete() as ResponseBody; break; default: return; } - return Promise.resolve(response); + return Promise.resolve(response) as ResponseBody; }; } diff --git a/src/app/services/slices/graph-response.slice.ts b/src/app/services/slices/graph-response.slice.ts index c7b5c98ab..78da5fd32 100644 --- a/src/app/services/slices/graph-response.slice.ts +++ b/src/app/services/slices/graph-response.slice.ts @@ -48,23 +48,22 @@ export const runQuery = createAsyncThunk( async (query: IQuery, { dispatch, getState, rejectWithValue }) => { const state = getState() as ApplicationState; const tokenPresent = !!state?.auth?.authToken?.token; - const respHeaders = {}; const createdAt = new Date().toISOString(); try { - const response: Response = tokenPresent + const response: ResponseBody = tokenPresent ? await authenticatedRequest(query) : await anonymousRequest(query, getState); + const resp = response as Response; + const respHeaders = (resp).headers; - const result: Result = await processResponse(response, dispatch, query); + const result: Result = await processResponse(resp, dispatch, query); const duration = new Date().getTime() - new Date(createdAt).getTime(); - const status = generateStatus({ duration, response }); + const status = generateStatus({ duration, response: resp }); dispatch(setQueryResponseStatus(status)); - // TODO: fix this api args - const historyItem = generateHistoryItem(status, respHeaders, - query, createdAt, result, duration); + const historyItem = generateHistoryItem(status, respHeaders, query, createdAt, result, duration); dispatch(addHistoryItem(historyItem)); return result; diff --git a/src/app/utils/fetch-retry-handler.ts b/src/app/utils/fetch-retry-handler.ts index 2c1ccdd92..e8f850f03 100644 --- a/src/app/utils/fetch-retry-handler.ts +++ b/src/app/utils/fetch-retry-handler.ts @@ -1,4 +1,4 @@ -export async function exponentialFetchRetry( fn: () => Promise, retriesLeft: number, +export async function exponentialFetchRetry( fn: () => Promise, retriesLeft: number, interval: number, condition?: (result: T, retriesLeft?: number) => Promise ): Promise { try { @@ -6,10 +6,10 @@ export async function exponentialFetchRetry( fn: () => Promise, retriesL if (condition) { const isConditionSatisfied = await condition(result, retriesLeft); if(isConditionSatisfied){ - throw new Error('An error occured during the execution of the request'); + throw new Error('An error occurred during the execution of the request'); } } - if (result && result.status && result.status >= 500){ + if (result && result instanceof Response && result.status && result.status >= 500){ throw new Error('Encountered a server error during execution of the request'); } return result; diff --git a/src/types/query-response.ts b/src/types/query-response.ts index 7060d9474..5ad15f12c 100644 --- a/src/types/query-response.ts +++ b/src/types/query-response.ts @@ -1,3 +1,4 @@ +import { Person, ResponseType, User } from '@microsoft/microsoft-graph-types'; import { ContentType, Mode } from './enums'; import { IQuery } from './query-runner'; @@ -23,9 +24,15 @@ export interface IGraphResponse { } } +export interface ResponseValue { + id: string +} export interface CustomBody { throwsCorsError: boolean, - contentDownloadUrl: string + contentDownloadUrl: string, + error: Error, + value: Partial[] | undefined + } export type ResponseBody = Partial | Response | string | object | null | undefined; From f2d4bbe19726dd291890cf727ea399ed858f4c41 Mon Sep 17 00:00:00 2001 From: Musale Martin Date: Mon, 2 Dec 2024 12:37:04 +0300 Subject: [PATCH 06/20] feat: update graph response handling to initialize headers and remove unused response headers in history item --- src/app/services/slices/graph-response.slice.ts | 4 ++-- src/app/views/query-runner/QueryRunner.tsx | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/app/services/slices/graph-response.slice.ts b/src/app/services/slices/graph-response.slice.ts index 78da5fd32..76ffd51cf 100644 --- a/src/app/services/slices/graph-response.slice.ts +++ b/src/app/services/slices/graph-response.slice.ts @@ -39,6 +39,7 @@ const initialState: IGraphResponse = { isLoadingData: false, response: { body: undefined, + headers: {} } }; @@ -55,7 +56,6 @@ export const runQuery = createAsyncThunk( ? await authenticatedRequest(query) : await anonymousRequest(query, getState); const resp = response as Response; - const respHeaders = (resp).headers; const result: Result = await processResponse(resp, dispatch, query); @@ -63,7 +63,7 @@ export const runQuery = createAsyncThunk( const status = generateStatus({ duration, response: resp }); dispatch(setQueryResponseStatus(status)); - const historyItem = generateHistoryItem(status, respHeaders, query, createdAt, result, duration); + const historyItem = generateHistoryItem(status, {}, query, createdAt, result, duration); dispatch(addHistoryItem(historyItem)); return result; diff --git a/src/app/views/query-runner/QueryRunner.tsx b/src/app/views/query-runner/QueryRunner.tsx index 13acff72f..6c4b76714 100644 --- a/src/app/views/query-runner/QueryRunner.tsx +++ b/src/app/views/query-runner/QueryRunner.tsx @@ -45,6 +45,7 @@ const QueryRunner = (props: any) => { }; const handleOnRunQuery = (query?: IQuery) => { + console.log('query stuff', query) let sample = { ...sampleQuery }; if (sampleBody && sample.selectedVerb !== 'GET') { const headers = sample.sampleHeaders; @@ -75,6 +76,7 @@ const QueryRunner = (props: any) => { } } + console.log('sample before dispatch', sample) dispatch(runQuery(sample)); const sanitizedUrl = sanitizeQueryUrl(sample.sampleUrl); const deviceCharacteristics = telemetry.getDeviceCharacteristicsData(); From dd7ffa6191237ac91368aa062fc7205ef5d22767 Mon Sep 17 00:00:00 2001 From: Musale Martin Date: Mon, 9 Dec 2024 13:50:50 +0300 Subject: [PATCH 07/20] Use a Record to allow serialization of headers --- .../actions/query-action-creator-util.ts | 11 +++++++---- .../services/slices/graph-response.slice.ts | 18 +++++++----------- src/app/utils/http-methods.utils.ts | 11 +++++++++++ src/types/history.ts | 2 +- src/types/query-response.ts | 2 +- 5 files changed, 27 insertions(+), 17 deletions(-) diff --git a/src/app/services/actions/query-action-creator-util.ts b/src/app/services/actions/query-action-creator-util.ts index d6ba29668..0370394a8 100644 --- a/src/app/services/actions/query-action-creator-util.ts +++ b/src/app/services/actions/query-action-creator-util.ts @@ -16,6 +16,7 @@ import { IQuery } from '../../../types/query-runner'; import { IRequestOptions } from '../../../types/request'; import { IStatus } from '../../../types/status'; import { ClientError } from '../../utils/error-utils/ClientError'; +import { getHeaders } from '../../utils/http-methods.utils'; import { encodeHashCharacters } from '../../utils/query-url-sanitization'; import { translateMessage } from '../../utils/translate-messages'; import { authProvider, GraphClient } from '../graph-client'; @@ -147,7 +148,7 @@ export function isBetaURLResponse(json: any) { return !!json?.account?.[0]?.source?.type?.[0]; } -export function getContentType(headers: Headers | Record): ContentType { +export function getContentType(headers: Record): ContentType { let contentType: ContentType = '' as unknown as ContentType; if (headers) { @@ -169,7 +170,7 @@ export function getContentType(headers: Headers | Record): return contentType; } -export function isFileResponse(headers: Headers | Record) { +export function isFileResponse(headers: Record) { const contentDisposition: string | null = (headers instanceof Headers) ? headers.get('content-disposition') : headers['content-disposition']; @@ -196,9 +197,10 @@ export function isFileResponse(headers: Headers | Record) { } export async function generateResponseDownloadUrl(response: Response) { + const headers = getHeaders(response) try { const fileContents = await parseResponse(response); - const contentType = getContentType(response.headers); + const contentType = getContentType(headers); if (fileContents) { const buffer = await response.arrayBuffer(); const blob = new Blob([buffer], { type: contentType }); @@ -219,7 +221,8 @@ async function tryParseJson(textValue: string) { export const parseResponse = (response: Response): Promise => { if (response && response.headers) { - const contentType = getContentType(response.headers); + const headers = getHeaders(response) + const contentType = getContentType(headers); switch (contentType) { case ContentType.Json: return response.text().then(tryParseJson); diff --git a/src/app/services/slices/graph-response.slice.ts b/src/app/services/slices/graph-response.slice.ts index 76ffd51cf..f968b25c5 100644 --- a/src/app/services/slices/graph-response.slice.ts +++ b/src/app/services/slices/graph-response.slice.ts @@ -12,6 +12,7 @@ import { IGraphResponse, ResponseBody } from '../../../types/query-response'; import { IQuery } from '../../../types/query-runner'; import { IStatus } from '../../../types/status'; import { ClientError } from '../../utils/error-utils/ClientError'; +import { getHeaders } from '../../utils/http-methods.utils'; import { setStatusMessage } from '../../utils/status-message'; import { translateMessage } from '../../utils/translate-messages'; import { @@ -32,14 +33,13 @@ let CURRENT_RETRIES = 0; interface Result { body: ResponseBody; - headers: Headers | Record; + headers: Record; } const initialState: IGraphResponse = { isLoadingData: false, response: { body: undefined, - headers: {} } }; @@ -132,9 +132,10 @@ export default querySlice.reducer; async function processResponse(response: Response, dispatch: Function, query: IQuery): Promise { let result = await parseResponse(response); + const headers: Record = getHeaders(response); if (response && response.ok) { CURRENT_RETRIES = 0; - if (isFileResponse(response.headers)) { + if (isFileResponse(headers)) { const contentDownloadUrl = await generateResponseDownloadUrl(response); if (contentDownloadUrl) { result = { contentDownloadUrl }; @@ -150,7 +151,7 @@ async function processResponse(response: Response, dispatch: Function, query: IQ } } - return { body: result, headers: response.headers }; + return { body: result, headers }; } const generateStatus = ({ duration, response }: { duration: number; response: Response }): IStatus => { @@ -191,19 +192,14 @@ async function runReAuthenticatedRequest(response: Response, query: IQuery): Pro function generateHistoryItem( status: IStatus, - respHeaders: Headers | Record, + respHeaders: Record, query: IQuery, createdAt: string, result: Result, duration: number ): IHistoryItem { let response: Result = {body: {}, headers: {}}; - let contentType_: ContentType = '' as ContentType; - if (respHeaders instanceof Headers) { - contentType_ = respHeaders.get('content-type') as ContentType; - } else { - contentType_ = respHeaders['content-type'] - } + const contentType_ = respHeaders['content-type']; if (isImageResponse(contentType_)) { response = { ...result, body: 'Run the query to view the image' }; diff --git a/src/app/utils/http-methods.utils.ts b/src/app/utils/http-methods.utils.ts index 8439c6651..3d071deec 100644 --- a/src/app/utils/http-methods.utils.ts +++ b/src/app/utils/http-methods.utils.ts @@ -24,3 +24,14 @@ export function getStyleFor(method: string) { return currentTheme.palette.orangeLight; } } + +export function getHeaders(response: Response) { + const headers: Record = {}; + for (const entry of response.headers.entries()) { + if (Object.prototype.hasOwnProperty.call(response.headers, entry[0])) { + headers[entry[0]] = entry[1]; + } + } + return headers; +} + diff --git a/src/types/history.ts b/src/types/history.ts index 567b3d4d2..efcb36c54 100644 --- a/src/types/history.ts +++ b/src/types/history.ts @@ -18,7 +18,7 @@ interface IHistory { duration: number; body?: string; category?: string; - responseHeaders: Headers | Record; + responseHeaders: Record; } export interface IHistoryProps { diff --git a/src/types/query-response.ts b/src/types/query-response.ts index 5ad15f12c..e591364a5 100644 --- a/src/types/query-response.ts +++ b/src/types/query-response.ts @@ -20,7 +20,7 @@ export interface IGraphResponse { isLoadingData: boolean; response: { body: ResponseBody; - headers: Headers | Record; + headers: Record; } } From 06c61330866d085ae9064fd0a70abdbde4f44cde Mon Sep 17 00:00:00 2001 From: Musale Martin Date: Mon, 9 Dec 2024 15:10:07 +0300 Subject: [PATCH 08/20] refactor: update response handling to use ResponseBody type and improve header processing --- .../actions/query-action-creator-util.ts | 50 ++++++++----------- .../services/slices/graph-response.slice.ts | 44 +++++++++------- src/app/utils/http-methods.utils.ts | 7 +-- .../response/ResponseDisplayV9.tsx | 10 ++-- src/types/history.ts | 1 - src/types/query-response.ts | 2 +- 6 files changed, 57 insertions(+), 57 deletions(-) diff --git a/src/app/services/actions/query-action-creator-util.ts b/src/app/services/actions/query-action-creator-util.ts index 0370394a8..c2e6d274a 100644 --- a/src/app/services/actions/query-action-creator-util.ts +++ b/src/app/services/actions/query-action-creator-util.ts @@ -10,7 +10,6 @@ import { import { authenticationWrapper } from '../../../modules/authentication'; import { ApplicationState } from '../../../store'; -import { ContentType } from '../../../types/enums'; import { ResponseBody } from '../../../types/query-response'; import { IQuery } from '../../../types/query-runner'; import { IRequestOptions } from '../../../types/request'; @@ -38,7 +37,7 @@ export async function anonymousRequest( export function createAnonymousRequest(query: IQuery, proxyUrl: string, queryRunnerStatus: IStatus) { const escapedUrl = encodeURIComponent(query.sampleUrl); const graphUrl = `${proxyUrl}?url=${escapedUrl}`; - const sampleHeaders: any = {}; + const sampleHeaders: Record = {}; if (query.sampleHeaders && query.sampleHeaders.length > 0) { query.sampleHeaders.forEach((header) => { @@ -148,26 +147,17 @@ export function isBetaURLResponse(json: any) { return !!json?.account?.[0]?.source?.type?.[0]; } -export function getContentType(headers: Record): ContentType { - let contentType: ContentType = '' as unknown as ContentType; - - if (headers) { - let contentTypes: string | null = null; - if (headers instanceof Headers) { - contentTypes = headers.get('content-type'); - } else { - contentTypes = headers['content-type']; - } - if (contentTypes) { - /* Example: application/json;odata.metadata=minimal;odata.streaming=true;IEEE754Compatible=false;charset=utf-8 - * Take the first option after splitting since it is the only value useful in the description of the content - */ - const splitContentTypes = contentTypes.split(';'); - contentType = (splitContentTypes.length > 0) ? - splitContentTypes[0].toLowerCase() as ContentType : contentTypes as ContentType; - } +export function getContentType(headers: Record): string { + const contentTypeHeader = Object.keys(headers).find(header => header.toLowerCase() === 'content-type'); + let contentType = contentTypeHeader ? headers[contentTypeHeader] : ''; + /* Example: application/json;odata.metadata=minimal;odata.streaming=true;IEEE754Compatible=false;charset=utf-8 + * Take the first option after splitting since it is the only value useful in the description of the content + */ + if (contentType) { + const splitContentTypes = contentType.split(';'); + contentType = (splitContentTypes.length > 0) ? splitContentTypes[0].toLowerCase() : contentType; } - return contentType; + return contentType.toLowerCase(); } export function isFileResponse(headers: Record) { @@ -219,23 +209,27 @@ async function tryParseJson(textValue: string) { } } -export const parseResponse = (response: Response): Promise => { - if (response && response.headers) { +export const parseResponse = (response: ResponseBody): Promise => { + if (response instanceof Response && response.headers) { const headers = getHeaders(response) const contentType = getContentType(headers); switch (contentType) { - case ContentType.Json: + case 'application/json': return response.text().then(tryParseJson); - case ContentType.XML: - case ContentType.HTML: - case ContentType.TextCsv: - case ContentType.TextPlain: + case 'application/xml': + case 'text/html': + case 'text/csv': + case 'text/plain': return response.text(); default: + console.log('what default response', contentType, response.headers.get('Content-Type'), response.headers.entries()) + console.log(response) return Promise.resolve(response); } } + console.log('what response') + console.log(response) return Promise.resolve(response); } diff --git a/src/app/services/slices/graph-response.slice.ts b/src/app/services/slices/graph-response.slice.ts index f968b25c5..99b08f214 100644 --- a/src/app/services/slices/graph-response.slice.ts +++ b/src/app/services/slices/graph-response.slice.ts @@ -1,6 +1,6 @@ import { BrowserAuthError } from '@azure/msal-browser'; import { MessageBarType } from '@fluentui/react'; -import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit'; +import { createAsyncThunk, createSlice, PayloadAction, ThunkDispatch, UnknownAction } from '@reduxjs/toolkit'; import { authenticationWrapper } from '../../../modules/authentication'; import { ClaimsChallenge } from '../../../modules/authentication/ClaimsChallenge'; @@ -55,12 +55,11 @@ export const runQuery = createAsyncThunk( const response: ResponseBody = tokenPresent ? await authenticatedRequest(query) : await anonymousRequest(query, getState); - const resp = response as Response; - const result: Result = await processResponse(resp, dispatch, query); + const result: Result = await processResponse(response, dispatch, query); const duration = new Date().getTime() - new Date(createdAt).getTime(); - const status = generateStatus({ duration, response: resp }); + const status = generateStatus({ duration, response }); dispatch(setQueryResponseStatus(status)); const historyItem = generateHistoryItem(status, {}, query, createdAt, result, duration); @@ -130,10 +129,11 @@ const querySlice = createSlice({ export const { setQueryResponse } = querySlice.actions; export default querySlice.reducer; -async function processResponse(response: Response, dispatch: Function, query: IQuery): Promise { +async function processResponse( + response: ResponseBody, dispatch: ThunkDispatch, query: IQuery): Promise { let result = await parseResponse(response); const headers: Record = getHeaders(response); - if (response && response.ok) { + if (response instanceof Response && response.ok) { CURRENT_RETRIES = 0; if (isFileResponse(headers)) { const contentDownloadUrl = await generateResponseDownloadUrl(response); @@ -143,7 +143,7 @@ async function processResponse(response: Response, dispatch: Function, query: IQ } } - if (response && response.status === 401 && CURRENT_RETRIES < MAX_NUMBER_OF_RETRIES) { + if (response instanceof Response && response.status === 401 && CURRENT_RETRIES < MAX_NUMBER_OF_RETRIES) { const successful = await runReAuthenticatedRequest(response, query); if (successful) { dispatch(runQuery(query)); @@ -154,25 +154,31 @@ async function processResponse(response: Response, dispatch: Function, query: IQ return { body: result, headers }; } -const generateStatus = ({ duration, response }: { duration: number; response: Response }): IStatus => { +interface Status { + duration: number + response: ResponseBody +} + +const generateStatus = (statusValues: Status): IStatus => { + const {duration, response} = statusValues; const status: IStatus = { messageType: MessageBarType.error, ok: false, duration, - status: response.status || 400, + status: 400, statusText: '' - }; - - if (response) { - status.status = response.status; - status.statusText = response.statusText === '' ? setStatusMessage(response.status) : response.statusText; } + if(response instanceof Response) { + if (response) { + status.status = response.status; + status.statusText = response.statusText === '' ? setStatusMessage(response.status) : response.statusText; + } - if (response && response.ok) { - CURRENT_RETRIES = 0; - status.ok = true; - status.messageType = MessageBarType.success; - } + if (response && response.ok) { + CURRENT_RETRIES = 0; + status.ok = true; + status.messageType = MessageBarType.success; + }} return status; } diff --git a/src/app/utils/http-methods.utils.ts b/src/app/utils/http-methods.utils.ts index 3d071deec..23e566b59 100644 --- a/src/app/utils/http-methods.utils.ts +++ b/src/app/utils/http-methods.utils.ts @@ -1,4 +1,5 @@ import { getTheme } from '@fluentui/react'; +import { ResponseBody } from '../../types/query-response'; export function getStyleFor(method: string) { const currentTheme = getTheme(); @@ -25,10 +26,10 @@ export function getStyleFor(method: string) { } } -export function getHeaders(response: Response) { +export function getHeaders(response: ResponseBody) { const headers: Record = {}; - for (const entry of response.headers.entries()) { - if (Object.prototype.hasOwnProperty.call(response.headers, entry[0])) { + if(response instanceof Response){ + for (const entry of response.headers.entries()) { headers[entry[0]] = entry[1]; } } diff --git a/src/app/views/query-response/response/ResponseDisplayV9.tsx b/src/app/views/query-response/response/ResponseDisplayV9.tsx index 23f59fd63..94485e5de 100644 --- a/src/app/views/query-response/response/ResponseDisplayV9.tsx +++ b/src/app/views/query-response/response/ResponseDisplayV9.tsx @@ -4,7 +4,7 @@ import { Image, MonacoV9 } from '../../common'; import { formatXml } from '../../common/monaco/util/format-xml'; interface ResponseDisplayProps { - contentType: ContentType; + contentType: string; body: string; height: string; } @@ -13,11 +13,11 @@ const ResponseDisplayV9 = (props: ResponseDisplayProps) => { const { contentType, body, height } = props; switch (contentType) { - case ContentType.XML: - return ; + case 'application/xml': + return ; - case ContentType.HTML: - return ; + case 'text/html': + return ; default: if (isImageResponse(contentType)) { diff --git a/src/types/history.ts b/src/types/history.ts index efcb36c54..3d108189b 100644 --- a/src/types/history.ts +++ b/src/types/history.ts @@ -1,5 +1,4 @@ import { ITheme } from '@fluentui/react'; -import { ContentType } from './enums'; import { Header } from './query-runner'; export interface IHistoryItem extends IHistory { diff --git a/src/types/query-response.ts b/src/types/query-response.ts index e591364a5..407737d65 100644 --- a/src/types/query-response.ts +++ b/src/types/query-response.ts @@ -35,4 +35,4 @@ export interface CustomBody { value: Partial[] | undefined } -export type ResponseBody = Partial | Response | string | object | null | undefined; +export type ResponseBody = Partial | string | object | null | undefined; From ee934831631c4833142598fc067f3c4cf59d82bc Mon Sep 17 00:00:00 2001 From: Musale Martin Date: Mon, 9 Dec 2024 15:48:59 +0300 Subject: [PATCH 09/20] feat: add CopyButtonV9 and ResponseHeadersV9 components, update component registry --- .../views/common/copy-button/CopyButtonV9.tsx | 37 +++++++++++++++++ .../lazy-loader/component-registry/index.tsx | 36 ++++++++++++----- .../headers/ResponseHeadersV9.tsx | 40 +++++++++++++++++++ .../pivot-items/pivot-items.tsx | 5 +-- 4 files changed, 104 insertions(+), 14 deletions(-) create mode 100644 src/app/views/common/copy-button/CopyButtonV9.tsx create mode 100644 src/app/views/query-response/headers/ResponseHeadersV9.tsx diff --git a/src/app/views/common/copy-button/CopyButtonV9.tsx b/src/app/views/common/copy-button/CopyButtonV9.tsx new file mode 100644 index 000000000..b2a9d40b6 --- /dev/null +++ b/src/app/views/common/copy-button/CopyButtonV9.tsx @@ -0,0 +1,37 @@ +import { Button } from '@fluentui/react-components'; +import { CheckmarkRegular, CopyRegular } from '@fluentui/react-icons'; +import { useState } from 'react'; +import { translateMessage } from '../../../utils/translate-messages'; + +interface ICopyButtonProps { + handleOnClick: () => void; + isIconButton: boolean; +} + +export default function CopyButton(props:ICopyButtonProps) { + const [copied, setCopied] = useState(false); + + const CopyIcon = !copied ? CopyRegular : CheckmarkRegular; + const copyLabel: string = !copied ? translateMessage('Copy') : translateMessage('Copied'); + + + const handleCopyClick = async () => { + props.handleOnClick(); + setCopied(true); + handleTimeout(); + }; + + const handleTimeout = () => { + const timer = setTimeout(() => { setCopied(false) }, 3000); // 3 seconds + return () => clearTimeout(timer); + } + + return ( + <> + {props.isIconButton ? + + } + + ) +} \ No newline at end of file diff --git a/src/app/views/common/lazy-loader/component-registry/index.tsx b/src/app/views/common/lazy-loader/component-registry/index.tsx index 08218c510..79f882383 100644 --- a/src/app/views/common/lazy-loader/component-registry/index.tsx +++ b/src/app/views/common/lazy-loader/component-registry/index.tsx @@ -1,16 +1,18 @@ import { IPermissionProps } from '../../../../../types/permissions'; -import LazySpecificPermissions from '../../../query-runner/request/permissions'; -import LazyStatusMessages from '../../../app-sections/StatusMessages'; -import LazyResponseHeaders from '../../../query-response/headers/ResponseHeaders'; -import LazyAdaptiveCard from '../../../query-response/adaptive-cards/AdaptiveCard'; -import LazyGraphToolkit from '../../../query-response/graph-toolkit/GraphToolkit'; -import LazySnippets from '../../../query-response/snippets/Snippets'; -import LazyCopyButton from '../../copy-button/CopyButton'; -import LazyAuth from '../../../query-runner/request/auth/Auth'; -import LazyRequestHeaders from '../../../query-runner/request/headers/RequestHeaders'; -import LazyHistory from '../../../sidebar/history/History'; -import LazyResourceExplorer from '../../../sidebar/resource-explorer/ResourceExplorer'; +import LazyStatusMessages from '../../../app-sections/StatusMessages'; +import LazyAdaptiveCard from '../../../query-response/adaptive-cards/AdaptiveCard'; +import LazyGraphToolkit from '../../../query-response/graph-toolkit/GraphToolkit'; +import LazyResponseHeaders from '../../../query-response/headers/ResponseHeaders'; +import LazyResponseHeadersV9 from '../../../query-response/headers/ResponseHeadersV9'; +import LazySnippets from '../../../query-response/snippets/Snippets'; +import LazyAuth from '../../../query-runner/request/auth/Auth'; +import LazyRequestHeaders from '../../../query-runner/request/headers/RequestHeaders'; +import LazySpecificPermissions from '../../../query-runner/request/permissions'; +import LazyHistory from '../../../sidebar/history/History'; +import LazyResourceExplorer from '../../../sidebar/resource-explorer/ResourceExplorer'; +import LazyCopyButton from '../../copy-button/CopyButton'; +import LazyCopyButtonV9 from '../../copy-button/CopyButtonV9'; export const Permissions = (props?: IPermissionProps) => { return ( @@ -42,6 +44,12 @@ export const ResponseHeaders = (props?: any) => { ) } +export const ResponseHeadersV9 = (props?: any) => { + return ( + + ) +} + export const Snippets = (props?: any) => { return ( @@ -54,6 +62,12 @@ export const CopyButton = (props?: any) => { ) } +export const CopyButtonV9 = (props?: any) => { + return ( + + ) +} + export const Auth = (props?: any) => { return ( diff --git a/src/app/views/query-response/headers/ResponseHeadersV9.tsx b/src/app/views/query-response/headers/ResponseHeadersV9.tsx new file mode 100644 index 000000000..9895cf966 --- /dev/null +++ b/src/app/views/query-response/headers/ResponseHeadersV9.tsx @@ -0,0 +1,40 @@ + +import { RESPONSE_HEADERS_COPY_BUTTON } from '../../../../telemetry/component-names'; + +import { useAppSelector } from '../../../../store'; +import { MonacoV9 } from '../../common'; +import { trackedGenericCopy } from '../../common/copy'; +import { + convertVhToPx, getResponseEditorHeight, + getResponseHeight +} from '../../common/dimensions/dimensions-adjustment'; +import { CopyButtonV9 } from '../../common/lazy-loader/component-registry'; + +const ResponseHeaders = () => { + const response = useAppSelector(state => state.dimensions.response) + const graphResponse = useAppSelector(state => state.graphResponse) + const responseAreaExpanded = useAppSelector(state => state.responseAreaExpanded) + const sampleQuery = useAppSelector(state => state.sampleQuery) + const { headers } = graphResponse.response; + + const defaultHeight = convertVhToPx(getResponseHeight(response.height, responseAreaExpanded), 220); + const monacoHeight = getResponseEditorHeight(120); + + + const handleCopy = async () => trackedGenericCopy(JSON.stringify(headers), RESPONSE_HEADERS_COPY_BUTTON, sampleQuery) + + if (headers) { + return ( + <> + + + + ); + } + + return ( +
+ ); +}; + +export default ResponseHeaders; diff --git a/src/app/views/query-response/pivot-items/pivot-items.tsx b/src/app/views/query-response/pivot-items/pivot-items.tsx index e57a155b7..fd92ac2a4 100644 --- a/src/app/views/query-response/pivot-items/pivot-items.tsx +++ b/src/app/views/query-response/pivot-items/pivot-items.tsx @@ -9,8 +9,7 @@ import { validateExternalLink } from '../../../utils/external-link-validation'; import { lookupToolkitUrl } from '../../../utils/graph-toolkit-lookup'; import { translateMessage } from '../../../utils/translate-messages'; import { - AdaptiveCards, GraphToolkit, ResponseHeaders, - Snippets + AdaptiveCards, GraphToolkit, ResponseHeadersV9, Snippets } from '../../common/lazy-loader/component-registry'; import { darkThemeHostConfig, lightThemeHostConfig } from '../adaptive-cards/AdaptiveHostConfig'; import { queryResponseStyles } from '../queryResponse.styles'; @@ -87,7 +86,7 @@ export const GetPivotItems = () => { 'aria-controls': 'response-headers-tab' }} > -
+
]; if (mode === Mode.Complete) { From 093d8ea44bb05c7cc8b1c1a18e94f90aa6ef874f Mon Sep 17 00:00:00 2001 From: Musale Martin Date: Tue, 10 Dec 2024 16:47:31 +0300 Subject: [PATCH 10/20] feat: introduce SnippetV9 and CopyButtonV9 components, update related imports and types --- .vscode/settings.json | 3 +- .../autocomplete-action-creators.spec.ts | 5 +- .../permissions-action-creator.spec.ts | 5 +- .../resource-explorer-action-creators.spec.ts | 5 +- src/app/services/slices/snippet.slice.ts | 14 +- src/app/views/common/copy-button/index.ts | 3 +- src/app/views/common/copy.ts | 10 + .../lazy-loader/component-registry/index.tsx | 6 +- .../pivot-items/pivot-items.tsx | 5 +- .../query-response/snippets/Snippets.tsx | 142 +++---- .../query-response/snippets/SnippetsV9.tsx | 187 ++++++++ .../views/query-response/snippets/index.tsx | 6 +- .../snippets/snippets-helper.tsx | 400 +++++++++--------- .../snippets/snippets-helper.v9.tsx | 287 +++++++++++++ src/telemetry/component-names.ts | 2 +- src/types/snippets.ts | 13 +- 16 files changed, 797 insertions(+), 296 deletions(-) create mode 100644 src/app/views/query-response/snippets/SnippetsV9.tsx create mode 100644 src/app/views/query-response/snippets/snippets-helper.v9.tsx diff --git a/.vscode/settings.json b/.vscode/settings.json index 8045f6e7b..85f7bf41c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -23,6 +23,7 @@ "package.json": "package-lock.json, .npmrc" }, "cSpell.words": [ - "fluentui" + "fluentui", + "norefferer" ], } diff --git a/src/app/services/actions/autocomplete-action-creators.spec.ts b/src/app/services/actions/autocomplete-action-creators.spec.ts index 95d89d5fd..8ed3b1ac6 100644 --- a/src/app/services/actions/autocomplete-action-creators.spec.ts +++ b/src/app/services/actions/autocomplete-action-creators.spec.ts @@ -6,6 +6,7 @@ import { ApplicationState, store } from '../../../../src/store/index'; import { fetchAutoCompleteOptions } from '../../../app/services/slices/autocomplete.slice'; import { suggestions } from '../../../modules/suggestions/suggestions'; import { Mode } from '../../../types/enums'; +import { SnippetError } from '../../../types/snippets'; import { AUTOCOMPLETE_FETCH_ERROR, AUTOCOMPLETE_FETCH_PENDING, AUTOCOMPLETE_FETCH_SUCCESS } from '../redux-constants'; import { mockThunkMiddleware } from './mockThunkMiddleware'; @@ -66,8 +67,8 @@ const mockState: ApplicationState = { }, snippets: { pending: false, - data: [], - error: null + data: {}, + error: {} as SnippetError }, responseAreaExpanded: false, dimensions: { diff --git a/src/app/services/actions/permissions-action-creator.spec.ts b/src/app/services/actions/permissions-action-creator.spec.ts index 852aff15a..34b668cfe 100644 --- a/src/app/services/actions/permissions-action-creator.spec.ts +++ b/src/app/services/actions/permissions-action-creator.spec.ts @@ -11,6 +11,7 @@ import { import { authenticationWrapper } from '../../../modules/authentication'; import { ApplicationState, store } from '../../../store/index'; import { Mode } from '../../../types/enums'; +import { SnippetError } from '../../../types/snippets'; import { getPermissionsScopeType } from '../../utils/getPermissionsScopeType'; import { translateMessage } from '../../utils/translate-messages'; import { ACCOUNT_TYPE } from '../graph-constants'; @@ -87,8 +88,8 @@ const mockState: ApplicationState = { }, snippets: { pending: false, - data: [], - error: null + data: {}, + error: {} as SnippetError }, responseAreaExpanded: false, dimensions: { diff --git a/src/app/services/actions/resource-explorer-action-creators.spec.ts b/src/app/services/actions/resource-explorer-action-creators.spec.ts index fdddf5bc6..d3ce233c1 100644 --- a/src/app/services/actions/resource-explorer-action-creators.spec.ts +++ b/src/app/services/actions/resource-explorer-action-creators.spec.ts @@ -6,6 +6,7 @@ import { } from '../../../app/services/redux-constants'; import { ApplicationState } from '../../../store'; import { Mode } from '../../../types/enums'; +import { SnippetError } from '../../../types/snippets'; import { fetchResources } from '../slices/resources.slice'; import { mockThunkMiddleware } from './mockThunkMiddleware'; @@ -67,8 +68,8 @@ const mockState: ApplicationState = { }, snippets: { pending: false, - data: [], - error: null + data: {}, + error: {} as SnippetError }, responseAreaExpanded: false, dimensions: { diff --git a/src/app/services/slices/snippet.slice.ts b/src/app/services/slices/snippet.slice.ts index 2cdf50d07..4a7493fe2 100644 --- a/src/app/services/slices/snippet.slice.ts +++ b/src/app/services/slices/snippet.slice.ts @@ -2,14 +2,14 @@ import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit'; import { ApplicationState } from '../../../store'; import { IRequestOptions } from '../../../types/request'; -import { ISnippet } from '../../../types/snippets'; +import { ISnippet, Snippet, SnippetError } from '../../../types/snippets'; import { parseSampleUrl } from '../../utils/sample-url-generation'; import { constructHeaderString } from '../../utils/snippet.utils'; const initialState: ISnippet = { pending: false, - data: {}, - error: null, + data: {} as Snippet, + error: {} as SnippetError, snippetTab: 'csharp' }; @@ -84,17 +84,17 @@ const snippetSlice = createSlice({ builder .addCase(getSnippet.pending, (state) => { state.pending = true; - state.error = null; + state.error = {} as SnippetError; state.data = {}; }) .addCase(getSnippet.fulfilled, (state, action) => { state.pending = false; - state.data = action.payload; - state.error = null; + state.data = action.payload as Snippet; + state.error = {} as SnippetError; }) .addCase(getSnippet.rejected, (state, action) => { state.pending = false; - state.error = action.payload as object; + state.error = action.payload as SnippetError; state.data = {}; }); } diff --git a/src/app/views/common/copy-button/index.ts b/src/app/views/common/copy-button/index.ts index b7452c10f..9e082d281 100644 --- a/src/app/views/common/copy-button/index.ts +++ b/src/app/views/common/copy-button/index.ts @@ -1,2 +1,3 @@ import CopyButton from './CopyButton'; -export default CopyButton; \ No newline at end of file +import CopyButtonV9 from './CopyButtonV9'; +export { CopyButton, CopyButtonV9 }; diff --git a/src/app/views/common/copy.ts b/src/app/views/common/copy.ts index f56d96b58..621640f03 100644 --- a/src/app/views/common/copy.ts +++ b/src/app/views/common/copy.ts @@ -1,3 +1,4 @@ +import { key } from 'localforage'; import { telemetry } from '../../../telemetry'; import { IQuery } from '../../../types/query-runner'; @@ -35,3 +36,12 @@ export function trackedGenericCopy( genericCopy(text); telemetry.trackCopyButtonClickEvent(componentName, sampleQuery, properties); } + +export function copyAndTrackText( + text: string, + componentName: string, + properties?: { [key: string]: string } +) { + genericCopy(text); + telemetry.trackCopyButtonClickEvent(componentName, undefined, properties); +} diff --git a/src/app/views/common/lazy-loader/component-registry/index.tsx b/src/app/views/common/lazy-loader/component-registry/index.tsx index 79f882383..45063fc03 100644 --- a/src/app/views/common/lazy-loader/component-registry/index.tsx +++ b/src/app/views/common/lazy-loader/component-registry/index.tsx @@ -5,7 +5,7 @@ import LazyAdaptiveCard from '../../../query-response/adaptive-cards/AdaptiveCar import LazyGraphToolkit from '../../../query-response/graph-toolkit/GraphToolkit'; import LazyResponseHeaders from '../../../query-response/headers/ResponseHeaders'; import LazyResponseHeadersV9 from '../../../query-response/headers/ResponseHeadersV9'; -import LazySnippets from '../../../query-response/snippets/Snippets'; +import LazySnippetsV9 from '../../../query-response/snippets/SnippetsV9'; import LazyAuth from '../../../query-runner/request/auth/Auth'; import LazyRequestHeaders from '../../../query-runner/request/headers/RequestHeaders'; import LazySpecificPermissions from '../../../query-runner/request/permissions'; @@ -50,9 +50,9 @@ export const ResponseHeadersV9 = (props?: any) => { ) } -export const Snippets = (props?: any) => { +export const SnippetsV9 = (props?: any) => { return ( - + ) } diff --git a/src/app/views/query-response/pivot-items/pivot-items.tsx b/src/app/views/query-response/pivot-items/pivot-items.tsx index fd92ac2a4..da251acea 100644 --- a/src/app/views/query-response/pivot-items/pivot-items.tsx +++ b/src/app/views/query-response/pivot-items/pivot-items.tsx @@ -9,7 +9,7 @@ import { validateExternalLink } from '../../../utils/external-link-validation'; import { lookupToolkitUrl } from '../../../utils/graph-toolkit-lookup'; import { translateMessage } from '../../../utils/translate-messages'; import { - AdaptiveCards, GraphToolkit, ResponseHeadersV9, Snippets + AdaptiveCards, GraphToolkit, ResponseHeadersV9, SnippetsV9 } from '../../common/lazy-loader/component-registry'; import { darkThemeHostConfig, lightThemeHostConfig } from '../adaptive-cards/AdaptiveHostConfig'; import { queryResponseStyles } from '../queryResponse.styles'; @@ -102,7 +102,8 @@ export const GetPivotItems = () => { 'aria-controls': 'code-snippets-tab' }} > -
+ {/*
*/} +
, state); - const supportedLanguages = { - 'CSharp': { - sdkDownloadLink: 'https://aka.ms/csharpsdk', - sdkDocLink: 'https://aka.ms/sdk-doc' - }, - 'PowerShell': { - sdkDownloadLink: 'https://aka.ms/pshellsdk', - sdkDocLink: 'https://aka.ms/pshellsdkdocs' - }, - 'Go': { - sdkDownloadLink: 'https://aka.ms/graphgosdk', - sdkDocLink: 'https://aka.ms/sdk-doc' - }, - 'Java': { - sdkDownloadLink: 'https://aka.ms/graphjavasdk', - sdkDocLink: 'https://aka.ms/sdk-doc' - }, - 'JavaScript': { - sdkDownloadLink: 'https://aka.ms/graphjssdk', - sdkDocLink: 'https://aka.ms/sdk-doc' - }, - 'PHP': { - sdkDownloadLink: 'https://aka.ms/graphphpsdk', - sdkDocLink: 'https://aka.ms/sdk-doc' - }, - 'Python': { - sdkDownloadLink: 'https://aka.ms/msgraphpythonsdk', - sdkDocLink: 'https://aka.ms/sdk-doc' - }, - 'CLI': { - sdkDownloadLink: 'https://aka.ms/msgraphclisdk', - sdkDocLink: 'https://aka.ms/sdk-doc' - } - }; +// const { snippets, sampleQuery } = useAppSelector((state) => state); +// const supportedLanguages = { +// 'CSharp': { +// sdkDownloadLink: 'https://aka.ms/csharpsdk', +// sdkDocLink: 'https://aka.ms/sdk-doc' +// }, +// 'PowerShell': { +// sdkDownloadLink: 'https://aka.ms/pshellsdk', +// sdkDocLink: 'https://aka.ms/pshellsdkdocs' +// }, +// 'Go': { +// sdkDownloadLink: 'https://aka.ms/graphgosdk', +// sdkDocLink: 'https://aka.ms/sdk-doc' +// }, +// 'Java': { +// sdkDownloadLink: 'https://aka.ms/graphjavasdk', +// sdkDocLink: 'https://aka.ms/sdk-doc' +// }, +// 'JavaScript': { +// sdkDownloadLink: 'https://aka.ms/graphjssdk', +// sdkDocLink: 'https://aka.ms/sdk-doc' +// }, +// 'PHP': { +// sdkDownloadLink: 'https://aka.ms/graphphpsdk', +// sdkDocLink: 'https://aka.ms/sdk-doc' +// }, +// 'Python': { +// sdkDownloadLink: 'https://aka.ms/msgraphpythonsdk', +// sdkDocLink: 'https://aka.ms/sdk-doc' +// }, +// 'CLI': { +// sdkDownloadLink: 'https://aka.ms/msgraphclisdk', +// sdkDocLink: 'https://aka.ms/sdk-doc' +// } +// }; - const handlePivotItemClick = (pivotItem?: PivotItem) => { - if (!pivotItem) { - return; - } - telemetry.trackTabClickEvent(pivotItem.props.itemKey!, sampleQuery); - dispatch(setSnippetTabSuccess(pivotItem.props.itemKey!)); - } +// const handlePivotItemClick = (pivotItem?: PivotItem) => { +// if (!pivotItem) { +// return; +// } +// telemetry.trackTabClickEvent(pivotItem.props.itemKey!, sampleQuery); +// dispatch(setSnippetTabSuccess(pivotItem.props.itemKey!)); +// } - return validation.isValid ? - {renderSnippets(supportedLanguages)} - : -} +// return validation.isValid ? +// {renderSnippets(supportedLanguages)} +// : +// } -const Snippets = telemetry.trackReactComponent( - GetSnippets, - componentNames.CODE_SNIPPETS_TAB -); -export default Snippets; \ No newline at end of file +// const Snippets = telemetry.trackReactComponent( +// GetSnippets, +// componentNames.CODE_SNIPPETS_TAB +// ); +// export default Snippets; \ No newline at end of file diff --git a/src/app/views/query-response/snippets/SnippetsV9.tsx b/src/app/views/query-response/snippets/SnippetsV9.tsx new file mode 100644 index 000000000..ee098bbeb --- /dev/null +++ b/src/app/views/query-response/snippets/SnippetsV9.tsx @@ -0,0 +1,187 @@ +import { + Label, Link, makeStyles, SelectTabData, SelectTabEvent, Spinner, Tab, TabList, TabValue +} from '@fluentui/react-components'; +import { useContext, useEffect, useState } from 'react'; +import { useAppDispatch, useAppSelector } from '../../../../store'; +import { componentNames, telemetry } from '../../../../telemetry'; +import { CODE_SNIPPETS_COPY_BUTTON } from '../../../../telemetry/component-names'; +import { SnippetError } from '../../../../types/snippets'; +import { ValidationContext } from '../../../services/context/validation-context/ValidationContext'; +import { getSnippet } from '../../../services/slices/snippet.slice'; +import { translateMessage } from '../../../utils/translate-messages'; +import { MonacoV9 } from '../../common'; +import { copyAndTrackText } from '../../common/copy'; +import { getResponseEditorHeight } from '../../common/dimensions/dimensions-adjustment'; +import { CopyButtonV9 } from '../../common/lazy-loader/component-registry'; + +interface LanguageSnippet { + [language:string]: { + sdkDocLink: string, + sdkDownloadLink: string, + } +} + +const supportedLanguages: LanguageSnippet= { + 'CSharp': { + sdkDownloadLink: 'https://aka.ms/csharpsdk', + sdkDocLink: 'https://aka.ms/sdk-doc' + }, + 'PowerShell': { + sdkDownloadLink: 'https://aka.ms/pshellsdk', + sdkDocLink: 'https://aka.ms/pshellsdkdocs' + }, + 'Go': { + sdkDownloadLink: 'https://aka.ms/graphgosdk', + sdkDocLink: 'https://aka.ms/sdk-doc' + }, + 'Java': { + sdkDownloadLink: 'https://aka.ms/graphjavasdk', + sdkDocLink: 'https://aka.ms/sdk-doc' + }, + 'JavaScript': { + sdkDownloadLink: 'https://aka.ms/graphjssdk', + sdkDocLink: 'https://aka.ms/sdk-doc' + }, + 'PHP': { + sdkDownloadLink: 'https://aka.ms/graphphpsdk', + sdkDocLink: 'https://aka.ms/sdk-doc' + }, + 'Python': { + sdkDownloadLink: 'https://aka.ms/msgraphpythonsdk', + sdkDocLink: 'https://aka.ms/sdk-doc' + }, + 'CLI': { + sdkDownloadLink: 'https://aka.ms/msgraphclisdk', + sdkDocLink: 'https://aka.ms/sdk-doc' + } +}; + +const useSnippetStyles = makeStyles({ + container: { + margin: '0 40px' + } +}) + +const setCommentSymbol = (language: string): string => { + const lang = language.trim().toLowerCase() + return (lang=== 'powershell' || lang === 'python') ? '#' : '//'; +} + +const getLanguageComponentName = ( + language: string, isDocumentationLink: boolean, + snippetComponentNames: {[language: string]: {sdk: string, doc: string}}) : string => { + const snippetComponentEntries = Object.entries(snippetComponentNames); + const snippetLanguageEntry = snippetComponentEntries.find( + ([key]) => language.toLocaleLowerCase() === key.toLocaleLowerCase() + ); + const componentName = isDocumentationLink ? snippetLanguageEntry?.[1].doc : snippetLanguageEntry?.[1].sdk; + return componentName || '' ; +} + + +const trackLinkClickedEvent = ( + link: string, language: string, e: React.MouseEvent) => { + const isDocumentationLink : boolean = link.includes('doc') + const componentName = getLanguageComponentName(language, isDocumentationLink, componentNames.CODE_SNIPPET_LANGUAGES); + telemetry.trackLinkClickEvent(e.currentTarget.href, componentName); +} + +const addExtraSnippetInformation = (language: string) : JSX.Element => { + const { sdkDownloadLink, sdkDocLink } = supportedLanguages[language]; + const paragraph = sdkDownloadLink + setCommentSymbol(language) + + translateMessage('Leverage libraries') + language + translateMessage('Client library') + const sdkParagraph = setCommentSymbol(language) + translateMessage('SDKs documentation') + return ( +
+

{paragraph}

+ trackLinkClickedEvent(language, sdkDownloadLink, e)} + href={sdkDownloadLink} inline rel='norefferer noopener' target='_blank'>{sdkDownloadLink} +

{sdkParagraph}

+ trackLinkClickedEvent(language, sdkDownloadLink, e)} + href={sdkDocLink} inline rel='norefferer noopener' target='_blank'>{sdkDocLink} +
+ ) +} + +const GetSnippets = ()=> { + const validation = useContext(ValidationContext); + return
+ {validation.isValid && } + {!validation.isValid && } +
+} + +const RenderSnippets = ()=>{ + const styles = useSnippetStyles() + const [selectedLanguage, setSelectedLanguage] = useState('CSharp'); + + const onTabSelect = (_event: SelectTabEvent, data: SelectTabData) => { + setSelectedLanguage(data.value); + }; + return
+ + {Object.keys(supportedLanguages).map((language: string) => ( + + {language === 'CSharp' ? 'C#' : language} + + ))} + +
+
+ +} + +interface SnippetContentProps { + language: string +} + +const SnippetContent = (props: SnippetContentProps)=>{ + const dispatch = useAppDispatch() + const language = props.language.toLowerCase(); + const snippets = useAppSelector(state => state.snippets) + const { data, pending: loadingState, error } = snippets; + const [snippetError, setSnippetError] = useState(error as SnippetError) + const hasSnippetError= (snippetError?.status && snippetError.status === 404) || + (snippetError?.status && snippetError.status === 400) + + const snippet = (!loadingState && data) ? data[language] : ''; + + const monacoHeight = getResponseEditorHeight(235); + + const handleCopy = async () => { + copyAndTrackText(snippet, CODE_SNIPPETS_COPY_BUTTON, {Language: language}) + } + + useEffect(() => { + dispatch(getSnippet(language)); + }, [language]); + + useEffect(()=>{ + setSnippetError(error) + }, [error]) + + return
+ {loadingState && !hasSnippetError && + } + {!loadingState && hasSnippetError && + } +
+ + +
+
+} + +const Snippets = telemetry.trackReactComponent( + GetSnippets, + componentNames.CODE_SNIPPETS_TAB +); +export default Snippets; \ No newline at end of file diff --git a/src/app/views/query-response/snippets/index.tsx b/src/app/views/query-response/snippets/index.tsx index 7829ae60c..5a762c069 100644 --- a/src/app/views/query-response/snippets/index.tsx +++ b/src/app/views/query-response/snippets/index.tsx @@ -1,2 +1,4 @@ -import Snippets from './Snippets'; -export default Snippets; +// import Snippets from './Snippets'; +import SnippetsV9 from './SnippetsV9'; +export { SnippetsV9 }; + diff --git a/src/app/views/query-response/snippets/snippets-helper.tsx b/src/app/views/query-response/snippets/snippets-helper.tsx index a06386000..789bcac15 100644 --- a/src/app/views/query-response/snippets/snippets-helper.tsx +++ b/src/app/views/query-response/snippets/snippets-helper.tsx @@ -1,200 +1,200 @@ -/* eslint-disable max-len */ -import { ITheme, Label, Link, PivotItem, getTheme } from '@fluentui/react'; -import { useEffect, useState } from 'react'; - -import { useAppDispatch, useAppSelector } from '../../../../store'; -import { componentNames, telemetry } from '../../../../telemetry'; -import { CODE_SNIPPETS_COPY_BUTTON } from '../../../../telemetry/component-names'; -import { getSnippet } from '../../../services/slices/snippet.slice'; -import { translateMessage } from '../../../utils/translate-messages'; -import { Monaco } from '../../common'; -import { trackedGenericCopy } from '../../common/copy'; -import { - convertVhToPx, getResponseEditorHeight, - getResponseHeight -} from '../../common/dimensions/dimensions-adjustment'; -import { CopyButton } from '../../common/lazy-loader/component-registry'; -import { getSnippetStyles } from './Snippets.styles'; - -interface ISnippetProps { - language: string; - snippetInfo: ISupportedLanguages; -} - -interface ISupportedLanguages { - [language: string]: { - sdkDownloadLink: string; - sdkDocLink: string; - }; -} - -export function renderSnippets(supportedLanguages: ISupportedLanguages) { - const sortedSupportedLanguages: ISupportedLanguages = {}; - const sortedKeys = Object.keys(supportedLanguages).sort((lang1, lang2) => { - if (lang1 === 'CSharp') { - return -1; - } - if (lang2 === 'CSharp') { - return 1; - } - if (lang1 < lang2) { - return -1; - } else if (lang1 > lang2) { - return 1; - } - return 0; - }); - - sortedKeys.forEach(key => { - sortedSupportedLanguages[key] = supportedLanguages[key]; - }); - - - return Object.keys(sortedSupportedLanguages). - map((language: string) => ( - - - - )); -} - -function Snippet(props: ISnippetProps) { - let { language } = props; - const { sdkDownloadLink, sdkDocLink } = props.snippetInfo[language]; - const unformattedLanguage = language; - - /** - * Converting language lowercase so that we won't have to call toLowerCase() in multiple places. - * - * Ie the monaco component expects a lowercase string for the language prop and the graphexplorerapi expects - * a lowercase string for the param value. - */ - - language = language.toLowerCase(); - - const { dimensions: { response }, snippets, - responseAreaExpanded, sampleQuery } = useAppSelector((state) => state); - const { data, pending: loadingState, error } = snippets; - const snippet = (!loadingState && data) ? data[language] : null; - const responseHeight = getResponseHeight(response.height, responseAreaExpanded); - const defaultHeight = convertVhToPx(responseHeight, 220); - const [snippetError, setSnippetError] = useState(error); - - const monacoHeight = getResponseEditorHeight(235); - - const dispatch = useAppDispatch(); - - useEffect(() => { - setSnippetError(error?.error ? error.error : error); - }, [error]) - - const handleCopy = async () => { - trackedGenericCopy(snippet, CODE_SNIPPETS_COPY_BUTTON, sampleQuery, { Language: language }); - } - - useEffect(() => { - dispatch(getSnippet(language)); - }, [sampleQuery.sampleUrl]); - - const setCommentSymbol = (): string => { - return (language.trim() === 'powershell' || language.trim() === 'python') ? '#' : '//'; - } - - const trackLinkClickedEvent = (link: string, e:any) => { - const isDocumentationLink : boolean = link.includes('doc') - const componentName = getLanguageComponentName(isDocumentationLink, componentNames.CODE_SNIPPET_LANGUAGES); - telemetry.trackLinkClickEvent(e.currentTarget.href, componentName); - } - const getLanguageComponentName = (isDocumentationLink: boolean, snippetComponentNames: object) : string => { - const snippetComponentEntries = Object.entries(snippetComponentNames); - const snippetLanguageEntry = snippetComponentEntries.find( - ([key]) => language.toLocaleLowerCase() === key.toLocaleLowerCase() - ); - const componentName = isDocumentationLink ? snippetLanguageEntry?.[1].doc : snippetLanguageEntry?.[1].sdk; - return componentName || '' ; - } - - const addExtraSnippetInformation = () : JSX.Element => { - const currentTheme: ITheme = getTheme(); - const snippetLinkStyles = getSnippetStyles(currentTheme); - const snippetCommentStyles = getSnippetStyles(currentTheme).snippetComments; - return ( -
- - {setCommentSymbol()} {translateMessage('Leverage libraries')} {unformattedLanguage} {translateMessage('Client library')} - - trackLinkClickedEvent(sdkDownloadLink, e)} target={'_blank'} rel='noreferrer noopener'> - {sdkDownloadLink} - -
- - {setCommentSymbol()} {translateMessage('SDKs documentation')} - - trackLinkClickedEvent(sdkDocLink, e)} target={'_blank'} rel='noreferrer noopener'> - {sdkDocLink} - -
- ); - } - - const displayError = (): JSX.Element | null => { - if((!loadingState && snippet) || (!loadingState && !snippetError)){ - return null; - } - if( - (snippetError?.status && snippetError.status === 404) || - (snippetError?.status && snippetError.status === 400) - ){ - return( - - ) - } - else{ - return ( - <> - {!loadingState && - - } - - ) - } - } - - return ( -
- {loadingState && - - } - {!loadingState && snippet && - <> - - - - } - {displayError()} -
- ); -} \ No newline at end of file +// /* eslint-disable max-len */ +// import { ITheme, Label, Link, PivotItem, getTheme } from '@fluentui/react'; +// import { useEffect, useState } from 'react'; + +// import { useAppDispatch, useAppSelector } from '../../../../store'; +// import { componentNames, telemetry } from '../../../../telemetry'; +// import { CODE_SNIPPETS_COPY_BUTTON } from '../../../../telemetry/component-names'; +// import { getSnippet } from '../../../services/slices/snippet.slice'; +// import { translateMessage } from '../../../utils/translate-messages'; +// import { Monaco } from '../../common'; +// import { trackedGenericCopy } from '../../common/copy'; +// import { +// convertVhToPx, getResponseEditorHeight, +// getResponseHeight +// } from '../../common/dimensions/dimensions-adjustment'; +// import { CopyButton } from '../../common/lazy-loader/component-registry'; +// import { getSnippetStyles } from './Snippets.styles'; + +// interface ISnippetProps { +// language: string; +// snippetInfo: ISupportedLanguages; +// } + +// interface ISupportedLanguages { +// [language: string]: { +// sdkDownloadLink: string; +// sdkDocLink: string; +// }; +// } + +// export function renderSnippets(supportedLanguages: ISupportedLanguages) { +// const sortedSupportedLanguages: ISupportedLanguages = {}; +// const sortedKeys = Object.keys(supportedLanguages).sort((lang1, lang2) => { +// if (lang1 === 'CSharp') { +// return -1; +// } +// if (lang2 === 'CSharp') { +// return 1; +// } +// if (lang1 < lang2) { +// return -1; +// } else if (lang1 > lang2) { +// return 1; +// } +// return 0; +// }); + +// sortedKeys.forEach(key => { +// sortedSupportedLanguages[key] = supportedLanguages[key]; +// }); + + +// return Object.keys(sortedSupportedLanguages). +// map((language: string) => ( +// +// +// +// )); +// } + +// function Snippet(props: ISnippetProps) { +// let { language } = props; +// const { sdkDownloadLink, sdkDocLink } = props.snippetInfo[language]; +// const unformattedLanguage = language; + +// /** +// * Converting language lowercase so that we won't have to call toLowerCase() in multiple places. +// * +// * Ie the monaco component expects a lowercase string for the language prop and the graphexplorerapi expects +// * a lowercase string for the param value. +// */ + +// language = language.toLowerCase(); + +// const { dimensions: { response }, snippets, +// responseAreaExpanded, sampleQuery } = useAppSelector((state) => state); +// const { data, pending: loadingState, error } = snippets; +// const snippet = (!loadingState && data) ? data[language] : null; +// const responseHeight = getResponseHeight(response.height, responseAreaExpanded); +// const defaultHeight = convertVhToPx(responseHeight, 220); +// const [snippetError, setSnippetError] = useState(error); + +// const monacoHeight = getResponseEditorHeight(235); + +// const dispatch = useAppDispatch(); + +// useEffect(() => { +// setSnippetError(error?.error ? error.error : error); +// }, [error]) + +// const handleCopy = async () => { +// trackedGenericCopy(snippet, CODE_SNIPPETS_COPY_BUTTON, sampleQuery, { Language: language }); +// } + +// useEffect(() => { +// dispatch(getSnippet(language)); +// }, [sampleQuery.sampleUrl]); + +// const setCommentSymbol = (): string => { +// return (language.trim() === 'powershell' || language.trim() === 'python') ? '#' : '//'; +// } + +// const trackLinkClickedEvent = (link: string, e:any) => { +// const isDocumentationLink : boolean = link.includes('doc') +// const componentName = getLanguageComponentName(isDocumentationLink, componentNames.CODE_SNIPPET_LANGUAGES); +// telemetry.trackLinkClickEvent(e.currentTarget.href, componentName); +// } +// const getLanguageComponentName = (isDocumentationLink: boolean, snippetComponentNames: object) : string => { +// const snippetComponentEntries = Object.entries(snippetComponentNames); +// const snippetLanguageEntry = snippetComponentEntries.find( +// ([key]) => language.toLocaleLowerCase() === key.toLocaleLowerCase() +// ); +// const componentName = isDocumentationLink ? snippetLanguageEntry?.[1].doc : snippetLanguageEntry?.[1].sdk; +// return componentName || '' ; +// } + +// const addExtraSnippetInformation = () : JSX.Element => { +// const currentTheme: ITheme = getTheme(); +// const snippetLinkStyles = getSnippetStyles(currentTheme); +// const snippetCommentStyles = getSnippetStyles(currentTheme).snippetComments; +// return ( +//
+ +// {setCommentSymbol()} {translateMessage('Leverage libraries')} {unformattedLanguage} {translateMessage('Client library')} + +// trackLinkClickedEvent(sdkDownloadLink, e)} target={'_blank'} rel='noreferrer noopener'> +// {sdkDownloadLink} +// +//
+ +// {setCommentSymbol()} {translateMessage('SDKs documentation')} + +// trackLinkClickedEvent(sdkDocLink, e)} target={'_blank'} rel='noreferrer noopener'> +// {sdkDocLink} +// +//
+// ); +// } + +// const displayError = (): JSX.Element | null => { +// if((!loadingState && snippet) || (!loadingState && !snippetError)){ +// return null; +// } +// if( +// (snippetError?.status && snippetError.status === 404) || +// (snippetError?.status && snippetError.status === 400) +// ){ +// return( +// +// ) +// } +// else{ +// return ( +// <> +// {!loadingState && +// +// } +// +// ) +// } +// } + +// return ( +//
+// {loadingState && +// +// } +// {!loadingState && snippet && +// <> +// +// +// +// } +// {displayError()} +//
+// ); +// } \ No newline at end of file diff --git a/src/app/views/query-response/snippets/snippets-helper.v9.tsx b/src/app/views/query-response/snippets/snippets-helper.v9.tsx new file mode 100644 index 000000000..753d85d27 --- /dev/null +++ b/src/app/views/query-response/snippets/snippets-helper.v9.tsx @@ -0,0 +1,287 @@ +import { getTheme, ITheme, Label, Link, PivotItem } from '@fluentui/react'; +import { useEffect, useState } from 'react'; + +import { makeStyles, SelectTabData, SelectTabEvent, Tab, TabList, TabValue } from '@fluentui/react-components'; +import { useAppDispatch, useAppSelector } from '../../../../store'; +import { componentNames, telemetry } from '../../../../telemetry'; +import { CODE_SNIPPETS_COPY_BUTTON } from '../../../../telemetry/component-names'; +import { SnippetError } from '../../../../types/snippets'; +import { getSnippet } from '../../../services/slices/snippet.slice'; +import { translateMessage } from '../../../utils/translate-messages'; +import { Monaco, MonacoV9 } from '../../common'; +import { copyAndTrackText, trackedGenericCopy } from '../../common/copy'; +import { + convertVhToPx, getResponseEditorHeight, getResponseHeight +} from '../../common/dimensions/dimensions-adjustment'; +import { CopyButton, CopyButtonV9 } from '../../common/lazy-loader/component-registry'; +import { getSnippetStyles } from './Snippets.styles'; + +interface ISnippetProps { + language: string; + snippetInfo: ISupportedLanguages; +} + +interface ISupportedLanguages { + [language: string]: { + sdkDownloadLink: string; + sdkDocLink: string; + }; +} + +export function renderSnippets(supportedLanguages: ISupportedLanguages) { + const sortedSupportedLanguages: ISupportedLanguages = {}; + const sortedKeys = Object.keys(supportedLanguages).sort((lang1, lang2) => { + if (lang1 === 'CSharp') { + return -1; + } + if (lang2 === 'CSharp') { + return 1; + } + if (lang1 < lang2) { + return -1; + } else if (lang1 > lang2) { + return 1; + } + return 0; + }); + + sortedKeys.forEach(key => { + sortedSupportedLanguages[key] = supportedLanguages[key]; + }); + + + return Object.keys(sortedSupportedLanguages). + map((language: string) => ( + + + + )); +} + +function Snippet(props: ISnippetProps) { + + /** + * Converting language lowercase so that we won't have to call toLowerCase() in multiple places. + * + * Ie the monaco component expects a lowercase string for the language prop and the graphexplorerapi expects + * a lowercase string for the param value. + */ + let { language } = props; + const { sdkDownloadLink, sdkDocLink } = props.snippetInfo[language]; + + const unformattedLanguage = language; + language = language.toLowerCase(); + + const response = useAppSelector(state => state.dimensions.response) + const responseAreaExpanded = useAppSelector(state => state.responseAreaExpanded) + const snippets = useAppSelector(state => state.snippets) + const sampleQuery = useAppSelector(state => state.sampleQuery) + + + const { data, pending: loadingState, error } = snippets; + const snippet = (!loadingState && data) ? data[language] : ''; + const responseHeight = getResponseHeight(response.height, responseAreaExpanded); + const defaultHeight = convertVhToPx(responseHeight, 220); + const [snippetError, setSnippetError] = useState(error); + + const monacoHeight = getResponseEditorHeight(235); + + const dispatch = useAppDispatch(); + + useEffect(() => { + setSnippetError(error?.error ? { ...error, error: error.error } : error); + }, [error]) + + const handleCopy = async () => { + trackedGenericCopy(snippet, CODE_SNIPPETS_COPY_BUTTON, sampleQuery, { Language: language }); + } + + useEffect(() => { + dispatch(getSnippet(language)); + }, [sampleQuery.sampleUrl]); + + const setCommentSymbol = (): string => { + return (language.trim() === 'powershell' || language.trim() === 'python') ? '#' : '//'; + } + + const trackLinkClickedEvent = (link: string, e: any) => { + const isDocumentationLink : boolean = link.includes('doc') + const componentName = getLanguageComponentName(isDocumentationLink, componentNames.CODE_SNIPPET_LANGUAGES); + telemetry.trackLinkClickEvent(e.currentTarget.href, componentName); + } + const getLanguageComponentName = (isDocumentationLink: boolean, snippetComponentNames: object) : string => { + const snippetComponentEntries = Object.entries(snippetComponentNames); + const snippetLanguageEntry = snippetComponentEntries.find( + ([key]) => language.toLocaleLowerCase() === key.toLocaleLowerCase() + ); + const componentName = isDocumentationLink ? snippetLanguageEntry?.[1].doc : snippetLanguageEntry?.[1].sdk; + return componentName || '' ; + } + + const addExtraSnippetInformation = () : JSX.Element => { + const currentTheme: ITheme = getTheme(); + const snippetLinkStyles = getSnippetStyles(currentTheme); + const snippetCommentStyles = getSnippetStyles(currentTheme).snippetComments; + return ( +
+ + {setCommentSymbol()} {translateMessage('Leverage libraries')} {unformattedLanguage} {translateMessage('Client library')} + + trackLinkClickedEvent(sdkDownloadLink, e)} target={'_blank'} rel='noreferrer noopener'> + {sdkDownloadLink} + +
+ + {setCommentSymbol()} {translateMessage('SDKs documentation')} + + trackLinkClickedEvent(sdkDocLink, e)} target={'_blank'} rel='noreferrer noopener'> + {sdkDocLink} + +
+ ); + } + + const displayError = (): JSX.Element | null => { + if((!loadingState && snippet) || (!loadingState && !snippetError)){ + return null; + } + if( + (snippetError?.status && snippetError.status === 404) || + (snippetError?.status && snippetError.status === 400) + ){ + return( + + ) + } + else{ + return ( + <> + {!loadingState && + + } + + ) + } + } + + return ( +
+ {loadingState && + + } + {!loadingState && snippet && + <> + + + + } + {displayError()} +
+ ); +} + +export const SnippetV9 = (props: ISnippetProps) => { + const dispatch = useAppDispatch(); + let { language } = props; + // const { sdkDownloadLink, sdkDocLink } = props.snippetInfo[language]; + + // const unformattedLanguage = language; + language = language.toLowerCase(); + + // const response = useAppSelector(state => state.dimensions.response) + // const responseAreaExpanded = useAppSelector(state => state.responseAreaExpanded) + // const sampleQuery = useAppSelector(state => state.sampleQuery) + // console.log('sample query') + // console.log(sampleQuery) + const snippets = useAppSelector(state => state.snippets) + const { data, pending: loadingState, error } = snippets; + const [snippetsError, setSnippetError] = useState(error as SnippetError) + + + const snippet = (!loadingState && data) ? data[language] : ''; + + const monacoHeight = getResponseEditorHeight(235); + + const handleCopy = async () => { + copyAndTrackText(snippet, CODE_SNIPPETS_COPY_BUTTON, {Language: language}) + } + + useEffect(() => { + dispatch(getSnippet(language)); + }, [language]); + + useEffect(()=>{ + setSnippetError(error) + }, [error]) + + // Handle the snippetsError + + return <> + + + +} + +const useSnippetStyles = makeStyles({ + container: { + alignItems: 'flex-start', + display: 'flex', + flexDirection: 'column', + justifyContent: 'flex-start', + rowGap: '20px' + } +}) + +export function renderSnippetsV9(languages: ISupportedLanguages) { + const styles = useSnippetStyles() + const [selectedValue, setSelectedValue] = useState('CSharp'); + + const onTabSelect = (_event: SelectTabEvent, data: SelectTabData) => { + const value = data.value as string; + setSelectedValue(value); + }; + return ( +
+ + {Object.keys(languages).map((language: string) => ( + + {language === 'CSharp' ? 'C#' : language} + + ))} + +
+ {} +
+
+ ); +} \ No newline at end of file diff --git a/src/telemetry/component-names.ts b/src/telemetry/component-names.ts index 371d2ea02..a798b8e6f 100644 --- a/src/telemetry/component-names.ts +++ b/src/telemetry/component-names.ts @@ -77,7 +77,7 @@ export const MICROSOFT_APIS_TERMS_OF_USE_LINK = 'Microsoft APIs terms of use lin export const MICROSOFT_PRIVACY_STATEMENT_LINK = 'Microsoft privacy statement link'; export const MICROSOFT_GRAPH_API_REFERENCE_DOCS_LINK = 'Microsoft graph API reference docs link'; export const GRAPH_EXPLORER_TUTORIAL_LINK = 'Graph Explorer Tutorial Link'; -export const CODE_SNIPPET_LANGUAGES = { +export const CODE_SNIPPET_LANGUAGES: {[language: string]: {sdk: string, doc: string}} = { CSharp: { sdk: 'C# SDK link', doc: 'C# snippet docs link' }, diff --git a/src/types/snippets.ts b/src/types/snippets.ts index e8b5f6b07..ce4ac0739 100644 --- a/src/types/snippets.ts +++ b/src/types/snippets.ts @@ -2,7 +2,16 @@ import { IApiResponse } from './action'; export interface ISnippet extends IApiResponse { pending: boolean; - data: any; - error: any | null; + data: Snippet; + error: SnippetError; snippetTab?: string; } + +export interface Snippet { + [language: string]: string +} + +export interface SnippetError { + status: number, + error: string +} From ada6582893a553f48994a381087c2df9d0e19d17 Mon Sep 17 00:00:00 2001 From: Musale Martin Date: Tue, 10 Dec 2024 18:06:09 +0300 Subject: [PATCH 11/20] feat: enhance MonacoV9 and SnippetsV9 components with extra information display and improved styling --- src/app/views/common/monaco/MonacoV9.tsx | 18 ++-- .../query-response/snippets/SnippetsV9.tsx | 96 +++++++++++-------- src/messages/GE.json | 2 +- 3 files changed, 65 insertions(+), 51 deletions(-) diff --git a/src/app/views/common/monaco/MonacoV9.tsx b/src/app/views/common/monaco/MonacoV9.tsx index 4b060d7b4..d76ad4d2f 100644 --- a/src/app/views/common/monaco/MonacoV9.tsx +++ b/src/app/views/common/monaco/MonacoV9.tsx @@ -36,14 +36,16 @@ const MonacoV9 = (props: MonacoProps)=>{ } - return
-
+ return
+ {props.extraInfoElement} + +
} export { MonacoV9 }; diff --git a/src/app/views/query-response/snippets/SnippetsV9.tsx b/src/app/views/query-response/snippets/SnippetsV9.tsx index ee098bbeb..47b421b69 100644 --- a/src/app/views/query-response/snippets/SnippetsV9.tsx +++ b/src/app/views/query-response/snippets/SnippetsV9.tsx @@ -1,5 +1,6 @@ import { - Label, Link, makeStyles, SelectTabData, SelectTabEvent, Spinner, Tab, TabList, TabValue + Label, Link, makeStyles, SelectTabData, SelectTabEvent, Spinner, Tab, TabList, TabValue, + Text } from '@fluentui/react-components'; import { useContext, useEffect, useState } from 'react'; import { useAppDispatch, useAppSelector } from '../../../../store'; @@ -9,7 +10,7 @@ import { SnippetError } from '../../../../types/snippets'; import { ValidationContext } from '../../../services/context/validation-context/ValidationContext'; import { getSnippet } from '../../../services/slices/snippet.slice'; import { translateMessage } from '../../../utils/translate-messages'; -import { MonacoV9 } from '../../common'; +import { Monaco, MonacoV9 } from '../../common'; import { copyAndTrackText } from '../../common/copy'; import { getResponseEditorHeight } from '../../common/dimensions/dimensions-adjustment'; import { CopyButtonV9 } from '../../common/lazy-loader/component-registry'; @@ -59,12 +60,17 @@ const supportedLanguages: LanguageSnippet= { const useSnippetStyles = makeStyles({ container: { margin: '0 40px' + }, + extraInformation: { + color: 'rgb(0, 128, 0)', + marginLeft: '28px', + lineHeight: '1.5' } }) const setCommentSymbol = (language: string): string => { const lang = language.trim().toLowerCase() - return (lang=== 'powershell' || lang === 'python') ? '#' : '//'; + return (lang=== 'powershell' || lang === 'python') ? '# ' : '// '; } const getLanguageComponentName = ( @@ -87,20 +93,27 @@ const trackLinkClickedEvent = ( } const addExtraSnippetInformation = (language: string) : JSX.Element => { + const styles = useSnippetStyles() const { sdkDownloadLink, sdkDocLink } = supportedLanguages[language]; - const paragraph = sdkDownloadLink + setCommentSymbol(language) + - translateMessage('Leverage libraries') + language + translateMessage('Client library') - const sdkParagraph = setCommentSymbol(language) + translateMessage('SDKs documentation') + const libParagraph = setCommentSymbol(language) + translateMessage('Leverage libraries') + + translateMessage('Client library') + ' ' + const sdkParagraph = setCommentSymbol(language) + translateMessage('SDKs documentation') + ' ' return ( -
-

{paragraph}

- trackLinkClickedEvent(language, sdkDownloadLink, e)} - href={sdkDownloadLink} inline rel='norefferer noopener' target='_blank'>{sdkDownloadLink} -

{sdkParagraph}

- trackLinkClickedEvent(language, sdkDownloadLink, e)} - href={sdkDocLink} inline rel='norefferer noopener' target='_blank'>{sdkDocLink} +
+ + {libParagraph} + trackLinkClickedEvent(language, sdkDownloadLink, e)} + href={sdkDownloadLink} inline rel='norefferer noopener' target='_blank'> + {sdkDownloadLink} + +
+ + {sdkParagraph} + trackLinkClickedEvent(language, sdkDocLink, e)} + href={sdkDocLink} inline rel='norefferer noopener' target='_blank'>{sdkDocLink} +
) } @@ -137,48 +150,47 @@ interface SnippetContentProps { language: string } -const SnippetContent = (props: SnippetContentProps)=>{ - const dispatch = useAppDispatch() +const SnippetContent: React.FC = (props: SnippetContentProps) => { + const dispatch = useAppDispatch(); const language = props.language.toLowerCase(); - const snippets = useAppSelector(state => state.snippets) + const snippets = useAppSelector(state => state.snippets); const { data, pending: loadingState, error } = snippets; - const [snippetError, setSnippetError] = useState(error as SnippetError) - const hasSnippetError= (snippetError?.status && snippetError.status === 404) || - (snippetError?.status && snippetError.status === 400) + const hasSnippetError = (error?.status && error.status === 404) || + (error?.status && error.status === 400); const snippet = (!loadingState && data) ? data[language] : ''; const monacoHeight = getResponseEditorHeight(235); const handleCopy = async () => { - copyAndTrackText(snippet, CODE_SNIPPETS_COPY_BUTTON, {Language: language}) - } + copyAndTrackText(snippet, CODE_SNIPPETS_COPY_BUTTON, { Language: language }); + }; useEffect(() => { dispatch(getSnippet(language)); - }, [language]); + }, [language, dispatch]); - useEffect(()=>{ - setSnippetError(error) - }, [error]) + const showSpinner = loadingState && !hasSnippetError; + const notAvailable = !loadingState && hasSnippetError; - return
- {loadingState && !hasSnippetError && - } - {!loadingState && hasSnippetError && - } + return (
- - + {showSpinner && } + {notAvailable && } + <> + + +
-
-} + ); +}; + const Snippets = telemetry.trackReactComponent( GetSnippets, diff --git a/src/messages/GE.json b/src/messages/GE.json index b8cab4904..5b899c70c 100644 --- a/src/messages/GE.json +++ b/src/messages/GE.json @@ -434,7 +434,7 @@ "Failed to get profile information": "Failed to get profile information", "Do you want to remove all the items you have added to the collection?": "Do you want to remove all the items you have added to the collection?", "Delete collection": "Delete collection", - "Leverage libraries": "Leverage the power of our client libraries. Download the", + "Leverage libraries": "Leverage the power of our client libraries. Download the ", "Client library": "client library here ", "SDKs documentation": "To read more about our SDKs, go to the documentation page at ", "Add to collection": "Add to collection", From 719a95ea88051821a166a3d0dddcbec19877ed9b Mon Sep 17 00:00:00 2001 From: Musale Martin Date: Tue, 10 Dec 2024 19:14:21 +0300 Subject: [PATCH 12/20] feat: add GraphToolkitV9 component and integrate it into pivot items --- .../graph-toolkit/GraphToolkitV9.tsx | 54 +++++++++++++++++++ .../pivot-items/pivot-items.tsx | 4 +- 2 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 src/app/views/query-response/graph-toolkit/GraphToolkitV9.tsx diff --git a/src/app/views/query-response/graph-toolkit/GraphToolkitV9.tsx b/src/app/views/query-response/graph-toolkit/GraphToolkitV9.tsx new file mode 100644 index 000000000..683b7dca1 --- /dev/null +++ b/src/app/views/query-response/graph-toolkit/GraphToolkitV9.tsx @@ -0,0 +1,54 @@ +import { Label, Link, makeStyles, MessageBar, MessageBarBody, MessageBarTitle } from '@fluentui/react-components'; +import { useAppSelector } from '../../../../store'; +import { componentNames, telemetry } from '../../../../telemetry'; +import { lookupToolkitUrl } from '../../../utils/graph-toolkit-lookup'; +import { translateMessage } from '../../../utils/translate-messages'; +import { + convertVhToPx, getResponseEditorHeight, + getResponseHeight +} from '../../common/dimensions/dimensions-adjustment'; + +const useMGTStyles = makeStyles({ + container: { + display: 'flex', + flexDirection: 'column' + } +}) +const GraphToolkitV9 = () => { + const styles = useMGTStyles() + const { sampleQuery, dimensions: { response }, responseAreaExpanded } = useAppSelector((state) => state); + const { toolkitUrl, exampleUrl } = lookupToolkitUrl(sampleQuery); + + const defaultHeight = convertVhToPx(getResponseHeight(response.height, responseAreaExpanded), 220); + const monacoHeight = getResponseEditorHeight(115); + + if (toolkitUrl && exampleUrl) { + return
+ + + Microsoft Graph Toolkit + {translateMessage('Open this example in')}{' '} + + telemetry.trackLinkClickEvent((e.currentTarget as HTMLAnchorElement).href, + componentNames.GRAPH_TOOLKIT_PLAYGROUND_LINK)}> + {translateMessage('graph toolkit playground')} + + +