Skip to content

Commit

Permalink
Merge pull request #3441 from microsoftgraph/feat/fluent-v9-request-body
Browse files Browse the repository at this point in the history
feat: update request body area
  • Loading branch information
Mnickii authored Dec 11, 2024
2 parents e5e2ba4 + f2f521c commit f11865e
Show file tree
Hide file tree
Showing 8 changed files with 479 additions and 6 deletions.
65 changes: 65 additions & 0 deletions src/app/views/common/copy-button/CopyButtonV9.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { useState, useRef } from 'react';
import { Button, Tooltip, makeStyles } from '@fluentui/react-components';
import { translateMessage } from '../../../utils/translate-messages';
import { CheckmarkRegular, CopyRegular } from '@fluentui/react-icons';

interface ICopyButtonProps {
style?: React.CSSProperties;
handleOnClick: Function;
className?: string;
isIconButton: boolean;
}

const useStyles = makeStyles({
button: {
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center'
}
});

export default function CopyButton(props: ICopyButtonProps) {
const [copied, setCopied] = useState(false);
const copyRef = useRef<HTMLButtonElement>(null);
const styles = useStyles();

const copyLabel: string = !copied ? translateMessage('Copy') : translateMessage('Copied');

const handleCopyClick = async () => {
props.handleOnClick(props);
setCopied(true);
handleTimeout();
copyRef.current?.focus(); // Set focus back to the button
};

const handleTimeout = () => {
const timer = setTimeout(() => setCopied(false), 3000); // Reset copied state after 3 seconds
return () => clearTimeout(timer);
};

return (
<>
{props.isIconButton ? (
<Tooltip content={copyLabel} relationship='label'>
<Button
appearance="subtle"
icon={copied ? <CheckmarkRegular /> : <CopyRegular />}
aria-label={copyLabel}
onClick={handleCopyClick}
style={props.style}
className={`${props.className} ${styles.button}`}
ref={copyRef}
/>
</Tooltip>
) : (
<Button
appearance="primary"
onClick={handleCopyClick}
ref={copyRef}
>
{copyLabel}
</Button>
)}
</>
);
}
6 changes: 3 additions & 3 deletions src/app/views/common/lazy-loader/component-registry/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ import LazyResponseHeaders from '../../../query-response/headers/ResponseHeader
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 LazyCopyButton from '../../copy-button/CopyButtonV9';
import LazyAuth from '../../../query-runner/request/auth/AuthV9';
import LazyRequestHeaders from '../../../query-runner/request/headers/RequestHeadersV9';
import LazyHistory from '../../../sidebar/history/History';
import LazyResourceExplorer from '../../../sidebar/resource-explorer/ResourceExplorer';

Expand Down
2 changes: 1 addition & 1 deletion src/app/views/query-runner/QueryRunner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { parseSampleUrl } from '../../utils/sample-url-generation';
import { translateMessage } from '../../utils/translate-messages';
import { QueryInput } from './query-input';
import './query-runner.scss';
import Request from './request/Request';
import Request from './request/RequestV9';

const QueryRunner = (props: any) => {
const dispatch = useAppDispatch();
Expand Down
152 changes: 152 additions & 0 deletions src/app/views/query-runner/request/RequestV9.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import { Resizable } from 're-resizable';
import { CSSProperties, useEffect, useState } from 'react';

import { useAppDispatch, useAppSelector } from '../../../../store';
import { telemetry } from '../../../../telemetry';
import { Mode } from '../../../../types/enums';
import { setDimensions } from '../../../services/slices/dimensions.slice';
import { translateMessage } from '../../../utils/translate-messages';
import { convertPxToVh, convertVhToPx } from '../../common/dimensions/dimensions-adjustment';
import { Auth, Permissions, RequestHeaders } from '../../common/lazy-loader/component-registry';
import { RequestBody } from './body';
import './request.scss';
import { IQuery } from '../../../../types/query-runner';
import { makeStyles, Tab, TabList, TabValue } from '@fluentui/react-components';


interface IRequestProps {
handleOnEditorChange: () => void
sampleQuery: IQuery
}

const useStyles = makeStyles({
resizable: {
width: '100%'
},
tabList: {
paddingBottom: '4px',
marginBottom: '8px'
},
tab: {
fontWeight: 'bold',
padding: '8px 16px',
borderBottom: '2px solid transparent'
}
});

const Request = (props: IRequestProps) => {
const styles = useStyles();
const dispatch = useAppDispatch();
const [selectedTab, setSelectedTab] = useState<TabValue>('request-body');
const mode = useAppSelector((state) => state.graphExplorerMode);
const dimensions = useAppSelector((state) => state.dimensions);
const sidebarProperties = useAppSelector((state) => state.sidebarProperties);
const minHeight = 60;
const maxHeight = 800;

const { handleOnEditorChange, sampleQuery }: IRequestProps = props;
const newHeight = convertVhToPx(dimensions.request.height, 55);
const containerStyle: CSSProperties = {
height: newHeight,
overflow: 'hidden',
borderRadius: '4px',
border: '1px solid #ddd',
padding: '8px'
}

useEffect(() => {
if (sidebarProperties && sidebarProperties.mobileScreen) {
window.addEventListener('resize', resizeHandler);
} else {
window.removeEventListener('resize', resizeHandler);
}
}, [sidebarProperties.mobileScreen]);

const handleTabSelect = (tab: TabValue) => {
setSelectedTab(tab);
telemetry.trackTabClickEvent(tab as string, sampleQuery);
};

const setRequestAndResponseHeights = (requestHeight: string) => {
const heightInPx = requestHeight.replace('px', '').trim();
const requestHeightInVh = convertPxToVh(parseFloat(heightInPx)).toString();
const maxDeviceVerticalHeight = 90;

const dimensionsToUpdate = {
...dimensions,
request: {
...dimensions.request,
height: requestHeightInVh
},
response: {
...dimensions.response,
height: `${maxDeviceVerticalHeight - parseFloat(requestHeightInVh.replace('vh', ''))}vh`
}
};

dispatch(setDimensions(dimensionsToUpdate));
};

const resizeHandler = () => {
const resizable = document.getElementsByClassName('request-resizable');
if (resizable && resizable.length > 0) {
const resizableElement = resizable[0] as HTMLElement;
if (resizableElement && resizableElement.style && resizableElement.style.height) {
resizableElement.style.height = '';
}
}
};

return (
<Resizable
className={styles.resizable}
onResize={(e: any, direction: any, ref: any) => {
if (ref && ref.style && ref.style.height) {
setRequestAndResponseHeights(ref.style.height);
}
}}
maxHeight={maxHeight}
minHeight={minHeight}
bounds={'window'}
size={{
height: 'inherit',
width: '100%'
}}
enable={{
bottom: true
}}
>
<div className="query-request">
<TabList
selectedValue={selectedTab}
onTabSelect={(_, data) => handleTabSelect(data.value)}
className={styles.tabList}
>
<Tab value="request-body" className={styles.tab} aria-label={translateMessage('request body')}>
{translateMessage('Request Body')}
</Tab>
<Tab value="request-headers" className={styles.tab} aria-label={translateMessage('request header')}>
{translateMessage('Request Headers')}
</Tab>
<Tab value="modify-permissions" className={styles.tab} aria-label={translateMessage('modify permissions')}>
{translateMessage('Modify Permissions')}
</Tab>
{mode === Mode.Complete && (
<Tab value="access-token" className={styles.tab} aria-label={translateMessage('Access Token')}>
{translateMessage('Access Token')}
</Tab>
)}
</TabList>

<div style={containerStyle}>
{selectedTab === 'request-body' && <RequestBody handleOnEditorChange={handleOnEditorChange} />}
{selectedTab === 'request-headers' && <RequestHeaders />}
{selectedTab === 'modify-permissions' && <Permissions />}
{selectedTab === 'access-token' && mode === Mode.Complete && <Auth />}
</div>
</div>
</Resizable>
);
};

export default Request;
135 changes: 135 additions & 0 deletions src/app/views/query-runner/request/auth/AuthV9.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import { useEffect, useState } from 'react';
import {
Button,
Text,
Tooltip,
makeStyles,
MessageBar
} from '@fluentui/react-components';
import { AuthenticationResult } from '@azure/msal-browser';
import { authenticationWrapper } from '../../../../../modules/authentication';
import { useAppSelector } from '../../../../../store';

import { componentNames, telemetry } from '../../../../../telemetry';
import { ACCOUNT_TYPE } from '../../../../services/graph-constants';
import { translateMessage } from '../../../../utils/translate-messages';
import { trackedGenericCopy } from '../../../common/copy';
import { CopyButton } from '../../../common/lazy-loader/component-registry';
import { convertVhToPx } from '../../../common/dimensions/dimensions-adjustment';
import { BracesRegular } from '@fluentui/react-icons';

const useStyles = makeStyles({
auth: {
padding: '5px',
overflowY: 'auto'
},
accessTokenContainer: {
width: '160px',
display: 'flex',
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingBottom: '10px'
},
accessToken: {
wordWrap: 'break-word',
fontFamily: 'monospace',
fontSize: '12px',
width: '100%',
height: '100%',
border: 'none',
resize: 'none'
},
accessTokenLabel: {
fontWeight: 'bold',
marginBottom: '5px'
},
emptyStateLabel: {
display: 'flex',
width: '100%',
minHeight: '100%',
justifyContent: 'center',
alignItems: 'center'
}
});

export function Auth() {
const profile = useAppSelector((state) => state.profile);
const height: string = useAppSelector((state) => state.dimensions.request.height);
const authToken = useAppSelector((state) => state.auth.authToken);
const { user } = profile;
const requestHeight = convertVhToPx(height, 60);
const [accessToken, setAccessToken] = useState<string | null>(null);
const [loading, setLoading] = useState(false);

const styles = useStyles();

const handleCopy = async () => {
trackedGenericCopy(accessToken || '', componentNames.ACCESS_TOKEN_COPY_BUTTON);
};

useEffect(() => {
setLoading(true);
authenticationWrapper
.getToken()
.then((response: AuthenticationResult) => {
setAccessToken(response.accessToken);
setLoading(false);
})
.catch(() => {
setLoading(false);
});
}, [authToken]);

const tokenDetailsDisabled = user?.profileType === ACCOUNT_TYPE.MSA;

if (!authToken.token) {
return (
<MessageBar intent='error'>
{translateMessage('Sign In to see your access token.')}
</MessageBar>
);
}

return (
<div className={styles.auth} style={{ height: requestHeight }}>
{!loading ? (
<div>
<div className={styles.accessTokenContainer}>
<Text className={styles.accessTokenLabel}>{translateMessage('Access Token')}</Text>
<CopyButton
isIconButton={true}
handleOnClick={handleCopy}
/>
<Tooltip content={translateMessage(showMessage())} relationship='label'>
<Button
as='a'
href={`https://jwt.ms#access_token=${accessToken}`}
target='_blank'
appearance='subtle'
disabled={tokenDetailsDisabled}
icon={<BracesRegular/>}
/>
</Tooltip>
</div>
<Text className={styles.accessToken} id="access-tokens-tab" tabIndex={0}>
{accessToken}
</Text>
</div>
) : (
<MessageBar intent='info'>
{translateMessage('Getting your access token')} ...
</MessageBar>
)}
</div>
);

function showMessage(): string {
if (tokenDetailsDisabled) {
return 'This token is not a JWT token and cannot be decoded by jwt.ms';
}
return 'Get token details (Powered by jwt.ms)';
}
}

export default telemetry.trackReactComponent(Auth, componentNames.ACCESS_TOKEN_TAB);
2 changes: 1 addition & 1 deletion src/app/views/query-runner/request/auth/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
import { Auth } from './Auth';
import { Auth } from './AuthV9';

export default Auth;
Loading

0 comments on commit f11865e

Please sign in to comment.