diff --git a/package-lock.json b/package-lock.json index 69f9f6236b..6fe3ad419c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@azure/msal-browser": "3.26.1", "@babel/core": "7.26.0", "@babel/runtime": "7.26.0", + "@fluentui-contrib/react-resize-handle": "^0.6.1", "@fluentui/react": "8.122.4", "@fluentui/react-components": "9.56.8", "@fluentui/react-icons": "2.0.270", @@ -3004,6 +3005,22 @@ "integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==", "license": "MIT" }, + "node_modules/@fluentui-contrib/react-resize-handle": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@fluentui-contrib/react-resize-handle/-/react-resize-handle-0.6.1.tgz", + "integrity": "sha512-xiF9spM4d4fTRe9ucpK74vpZNWCyyBrY7D7IHX5yBJ4BEhZQbeu8Il1wwU6vqKO3spOuw/w75Gtr8VTYz/HVkw==", + "dependencies": { + "@fluentui/react-utilities": ">=9.16.0 < 10.0.0", + "@swc/helpers": "~0.5.1" + }, + "peerDependencies": { + "@fluentui/react-components": ">=9.35.1 <10.0.0", + "@types/react": ">=16.8.0 <19.0.0", + "@types/react-dom": ">=16.8.0 <19.0.0", + "react": ">=16.8.0 <19.0.0", + "react-dom": ">=16.8.0 <19.0.0" + } + }, "node_modules/@fluentui/date-time-utilities": { "version": "8.6.9", "license": "MIT", diff --git a/package.json b/package.json index be83934d6e..b747e903a2 100644 --- a/package.json +++ b/package.json @@ -8,9 +8,10 @@ "@azure/msal-browser": "3.26.1", "@babel/core": "7.26.0", "@babel/runtime": "7.26.0", + "@fluentui-contrib/react-resize-handle": "^0.6.1", + "@fluentui/react": "8.122.4", "@fluentui/react-components": "9.56.8", "@fluentui/react-icons": "2.0.270", - "@fluentui/react": "8.122.4", "@fluentui/react-icons-mdl2": "1.3.80", "@microsoft/applicationinsights-react-js": "17.3.4", "@microsoft/applicationinsights-web": "3.3.4", @@ -131,4 +132,4 @@ "reportFile": "test-report.xml", "indent": 4 } -} \ No newline at end of file +} diff --git a/public/index.html b/public/index.html index 1959ee8a77..d5e7a58d0b 100644 --- a/public/index.html +++ b/public/index.html @@ -63,9 +63,10 @@ } body { + height: 100vh; margin: 0; - padding: 0; - border: 0; + display: flex; + flex-direction: column; } @keyframes lds-ring { diff --git a/src/app/services/context/resize-context/ResizeContext.tsx b/src/app/services/context/resize-context/ResizeContext.tsx new file mode 100644 index 0000000000..6ab64175aa --- /dev/null +++ b/src/app/services/context/resize-context/ResizeContext.tsx @@ -0,0 +1,11 @@ +import { createContext } from 'react'; + +interface ResizeContext { + sidebarSize: number; + responseAreaSize: number; + dragValue: number; + parentHeight: number; + initiator: string; +} + +export const ResizeContext = createContext({} as ResizeContext); diff --git a/src/app/services/context/resize-context/ResizeProvider.tsx b/src/app/services/context/resize-context/ResizeProvider.tsx new file mode 100644 index 0000000000..b7bada03a9 --- /dev/null +++ b/src/app/services/context/resize-context/ResizeProvider.tsx @@ -0,0 +1,53 @@ +import { ReactNode, useEffect, useMemo, useState } from 'react'; +import { ResizeContext } from './ResizeContext'; + +interface ResizeProviderProps { + children: ReactNode; +} +export const ResizeProvider = (props: ResizeProviderProps) => { + const { children } = props; + const [sidebarSize, setSidebarSize] = useState(0); + const [responseSize, setResponseSize] = useState(0); + const [parentHeight, setParentHeight] = useState(0); + const [initiatorValue, setInitiatorValue] = useState(''); + const [dragValue, setDragValue] = useState(-1); + + useEffect(() => { + const handleResize = (e: Event) => { + const layout = document.getElementById('layout'); + if (layout) { + const parentOffset = layout.parentElement?.offsetHeight ?? 0; + setParentHeight(parentOffset); + const customEvent = e as CustomEvent; + const { initiator, value } = customEvent.detail as { + initiator: string; + value: number; + }; + if (initiator === 'responseSize') { + setResponseSize(value); + } else if (initiator === 'sidebar') { + setSidebarSize(value); + } + setInitiatorValue(initiator); + setDragValue(value); + } + }; + window.addEventListener('onResizeDragEnd', handleResize); + return () => window.removeEventListener('resize', handleResize); + }, []); + + const contextValue = useMemo(() => { + return { + sidebarSize, + responseAreaSize: responseSize, + parentHeight, + initiator: initiatorValue, + dragValue + }; + }, [sidebarSize, responseSize, parentHeight, initiatorValue, dragValue]); + return ( + + {children} + + ); +}; diff --git a/src/app/views/App.styles.ts b/src/app/views/App.styles.ts index bb5e61fd6a..a9eb3e7e91 100644 --- a/src/app/views/App.styles.ts +++ b/src/app/views/App.styles.ts @@ -1,93 +1,94 @@ import { FontSizes, ITheme } from '@fluentui/react'; export const appStyles = (theme: ITheme) => { - return { - app: { - background: theme.semanticColors.bodyBackground, - color: theme.semanticColors.bodyText, - paddingTop: theme.spacing.s1, - height: '100%', - paddingRight: '15px', - paddingLeft: '4px', - paddingBottom: '5px', - marginLeft: 'auto', - marginRight: 'auto' - }, - appRow: { - display: 'flex', - flexWrap: 'no-wrap', - alignItems: 'stretch' - }, - tryItMessage: { - marginBottom: theme.spacing.s1 - }, - sidebar: { - background: theme.palette.neutralLighter, - paddingRight: 10, - paddingLeft: 10, - marginRight: 10, - marginBottom: 9 - }, - sidebarMini: { - background: theme.palette.neutralLighter, - maxWidth: '65px', - minWidth: '55px', - padding: 10, - marginBottom: 9 - }, - layoutExtra: { - minWidth: '95%', - maxWidth: '96%' - }, - separator: { - borderBottom: '1px solid ' + theme.palette.neutralLight - }, - sidebarToggle: { - marginBottom: theme.spacing.s1, - marginTop: theme.spacing.s1 - }, - previewButton: { - marginTop: theme.spacing.s1 - }, - graphExplorerLabel: { - fontSize: FontSizes.xLarge, - fontWeight: 600 - }, - graphExplorerLabelContainer: { - display: 'flex', - alignItems: 'center', - justifyContent: 'space-between', - width: 500 - }, - versionLabel: { - color: theme.palette.neutralSecondary, - fontSize: '10px', - paddingLeft: 3, - paddingTop: 5 - }, - statusAreaMobileScreen: { - marginTop: 5 - }, - statusAreaFullScreen: { - marginTop: 6, - position: 'relative', - bottom: 0 - }, - vResizeHandle: { - zIndex: 1, - borderRight: '1px solid silver', - '&:hover': { - borderRight: '3px solid silver' - } - }, - feedbackButtonFullScreenDisplay: { - position: 'relative', - top: '5px' - }, - feedbackButtonMobileDisplay: { - position: 'absolute', - top: '13px', - right: '10px' - } - }; + // return { + // app: { + // background: theme.semanticColors.bodyBackground, + // color: theme.semanticColors.bodyText, + // paddingTop: theme.spacing.s1, + // height: '100%', + // paddingRight: '15px', + // paddingLeft: '4px', + // paddingBottom: '5px', + // marginLeft: 'auto', + // marginRight: 'auto' + // }, + // appRow: { + // display: 'flex', + // flexWrap: 'no-wrap', + // alignItems: 'stretch' + // }, + // tryItMessage: { + // marginBottom: theme.spacing.s1 + // }, + // sidebar: { + // background: theme.palette.neutralLighter, + // paddingRight: 10, + // paddingLeft: 10, + // marginRight: 10, + // marginBottom: 9 + // }, + // sidebarMini: { + // background: theme.palette.neutralLighter, + // maxWidth: '65px', + // minWidth: '55px', + // padding: 10, + // marginBottom: 9 + // }, + // layoutExtra: { + // minWidth: '95%', + // maxWidth: '96%' + // }, + // separator: { + // borderBottom: '1px solid ' + theme.palette.neutralLight + // }, + // sidebarToggle: { + // marginBottom: theme.spacing.s1, + // marginTop: theme.spacing.s1 + // }, + // previewButton: { + // marginTop: theme.spacing.s1 + // }, + // graphExplorerLabel: { + // fontSize: FontSizes.xLarge, + // fontWeight: 600 + // }, + // graphExplorerLabelContainer: { + // display: 'flex', + // alignItems: 'center', + // justifyContent: 'space-between', + // width: 500 + // }, + // versionLabel: { + // color: theme.palette.neutralSecondary, + // fontSize: '10px', + // paddingLeft: 3, + // paddingTop: 5 + // }, + // statusAreaMobileScreen: { + // marginTop: 5 + // }, + // statusAreaFullScreen: { + // marginTop: 6, + // position: 'relative', + // bottom: 0 + // }, + // vResizeHandle: { + // zIndex: 1, + // borderRight: '1px solid silver', + // '&:hover': { + // borderRight: '3px solid silver' + // } + // }, + // feedbackButtonFullScreenDisplay: { + // position: 'relative', + // top: '5px' + // }, + // feedbackButtonMobileDisplay: { + // position: 'absolute', + // top: '13px', + // right: '10px' + // } + // }; + return {} }; diff --git a/src/app/views/App.tsx b/src/app/views/App.tsx index 6721078946..7a9872a274 100644 --- a/src/app/views/App.tsx +++ b/src/app/views/App.tsx @@ -1,5 +1,11 @@ import { getTheme, ITheme, styled } from '@fluentui/react'; -import { FluentProvider, teamsHighContrastTheme, Theme, webDarkTheme, webLightTheme } from '@fluentui/react-components'; +import { + FluentProvider, + teamsHighContrastTheme, + Theme, + webDarkTheme, + webLightTheme +} from '@fluentui/react-components'; import { bindActionCreators, Dispatch } from '@reduxjs/toolkit'; import { Resizable } from 're-resizable'; import { Component } from 'react'; @@ -12,7 +18,11 @@ import { componentNames, eventTypes, telemetry } from '../../telemetry'; import { loadGETheme } from '../../themes'; import { ThemeContext } from '../../themes/theme-context'; import { Mode } from '../../types/enums'; -import { IInitMessage, IQuery, IThemeChangedMessage } from '../../types/query-runner'; +import { + IInitMessage, + IQuery, + IThemeChangedMessage +} from '../../types/query-runner'; import { ISharedQueryParams } from '../../types/share-query'; import { ISidebarProps } from '../../types/sidebar'; import CollectionPermissionsProvider from '../services/context/collection-permissions/CollectionPermissionsProvider'; @@ -27,9 +37,9 @@ import { toggleSidebar } from '../services/slices/sidebar-properties.slice'; import { changeTheme } from '../services/slices/theme.slice'; import { parseSampleUrl } from '../utils/sample-url-generation'; import { substituteTokens } from '../utils/token-helpers'; -import { headerMessagingV9 } from './app-sections/HeaderMessagingV9'; import { translateMessage } from '../utils/translate-messages'; import { StatusMessagesV9, TermsOfUseMessageV9 } from './app-sections'; +import { headerMessagingV9 } from './app-sections/HeaderMessagingV9'; import { appStyles } from './App.styles'; import { classNames } from './classnames'; import Notification from './common/banners/Notification'; @@ -42,6 +52,7 @@ import { QueryResponse } from './query-response'; import { QueryRunner } from './query-runner'; import { parse } from './query-runner/util/iframe-message-parser'; // import { Sidebar } from './sidebar/Sidebar'; +import { Layout } from './layout/Layout'; import { SidebarV9 } from './sidebar/SidebarV9'; export interface IAppProps { theme?: ITheme; @@ -71,7 +82,10 @@ interface IAppState { } const getSystemTheme = (): string => { - if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) { + if ( + window.matchMedia && + window.matchMedia('(prefers-color-scheme: dark)').matches + ) { return 'dark'; } return 'light'; @@ -80,8 +94,8 @@ const getSystemTheme = (): string => { class App extends Component { private mediaQueryList = window.matchMedia('(max-width: 992px)'); private currentTheme: ITheme = getTheme(); - private statusAreaMobileStyle = appStyles(this.currentTheme).statusAreaMobileScreen; - private statusAreaFullScreenStyle = appStyles(this.currentTheme).statusAreaFullScreen; + // private statusAreaMobileStyle = appStyles(this.currentTheme).statusAreaMobileScreen; + // private statusAreaFullScreenStyle = appStyles(this.currentTheme).statusAreaFullScreen; constructor(props: IAppProps) { super(props); @@ -97,7 +111,7 @@ class App extends Component { this.setState({ sidebarTabSelection: selectedTab }); - } + }; public componentDidMount = async () => { removeSpinners(); @@ -223,14 +237,14 @@ class App extends Component { const msgEvent: IThemeChangedMessage | IInitMessage = event.data; switch (msgEvent.type) { - case 'init': - this.handleInitMsg(msgEvent); - break; - case 'theme-changed': - this.handleThemeChangeMsg(msgEvent); - break; - default: - return; + case 'init': + this.handleInitMsg(msgEvent); + break; + case 'theme-changed': + this.handleThemeChangeMsg(msgEvent); + break; + default: + return; } }; @@ -285,11 +299,9 @@ class App extends Component { public toggleSidebar = (): void => { const shouldShowSidebar = this.setSidebarProperties(); this.changeDimensions(shouldShowSidebar ? '28%' : '4%'); - telemetry.trackEvent( - eventTypes.BUTTON_CLICK_EVENT, - { - ComponentName: componentNames.SIDEBAR_HAMBURGER_BUTTON - }); + telemetry.trackEvent(eventTypes.BUTTON_CLICK_EVENT, { + ComponentName: componentNames.SIDEBAR_HAMBURGER_BUTTON + }); }; public displayToggleButton = (mediaQueryList: any) => { @@ -309,7 +321,6 @@ class App extends Component { // @ts-ignore this.props.actions!.toggleSidebar(properties); - }; private setSidebarProperties() { @@ -326,7 +337,10 @@ class App extends Component { const width = this.changeDimensions(sidebarWidth); const { sidebarProperties } = this.props; const minimised = !sidebarProperties.showSidebar; - if ((width <= breakPoint && !minimised) || (width > breakPoint && minimised)) { + if ( + (width <= breakPoint && !minimised) || + (width > breakPoint && minimised) + ) { this.setSidebarProperties(); } } @@ -355,7 +369,11 @@ class App extends Component { private shouldDisplayContent(parameters: any) { const { graphExplorerMode, mobileScreen, showSidebar } = parameters; - return !(graphExplorerMode === Mode.Complete && mobileScreen && showSidebar); + return !( + graphExplorerMode === Mode.Complete && + mobileScreen && + showSidebar + ); } private removeFlexBasisProperty() { @@ -391,8 +409,14 @@ class App extends Component { public render() { const classes = classNames(this.props); - const { authenticated, graphExplorerMode, minimised, sampleQuery, - sidebarProperties, dimensions }: any = this.props; + const { + authenticated, + graphExplorerMode, + minimised, + sampleQuery, + sidebarProperties, + dimensions + }: any = this.props; const { sidebar, content } = dimensions; let sidebarWidth = classes.sidebar; @@ -406,7 +430,8 @@ class App extends Component { const displayContent = this.shouldDisplayContent({ graphExplorerMode, - mobileScreen, showSidebar + mobileScreen, + showSidebar }); if (mobileScreen) { @@ -423,113 +448,105 @@ class App extends Component { this.removeSidebarHeightProperty(); const fluentV9Themes: Record = { - 'light': webLightTheme, - 'dark': webDarkTheme, + light: webLightTheme, + dark: webDarkTheme, 'high-contrast': teamsHighContrastTheme - } + }; return ( // @ts-ignore // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -
- {/* - */} - + { + console.log('Selected verb:', verb); + this.handleSelectVerb(verb); + }} + onDragEnd={( + value: number, + eventType: string, + initiator: string + ) => { + const event = new CustomEvent('onResizeDragEnd', { + detail: { value, eventType, initiator } + }); + window.dispatchEvent(event); + }} + /> + {/*
- {graphExplorerMode === Mode.Complete && ( - { - if (ref?.style?.width) { - this.resizeSideBar(ref.style.width); - } - }} - className={`ms-Grid-col ms-sm12 ms-md4 ms-lg4 ${sidebarWidth} resizable-sidebar`} - minWidth={'71'} - maxWidth={maxWidth} - enable={{ - right: true - }} - handleClasses={{ - right: classes.vResizeHandle - }} - bounds={'parent'} - size={{ - width: sideWidth, - height: '' - }} - > - {/* */} - - - )} - {graphExplorerMode === Mode.TryIt && - headerMessagingV9(query)} - - {displayContent && ( - -
- -
- -
- -
-
-
- -
- -
-
-
- )} + {graphExplorerMode === Mode.Complete && ( + { + if (ref?.style?.width) { + this.resizeSideBar(ref.style.width); + } + }} + className={`ms-Grid-col ms-sm12 ms-md4 ms-lg4 ${sidebarWidth} resizable-sidebar`} + minWidth={'71'} + maxWidth={maxWidth} + enable={{ + right: true + }} + handleClasses={{ + right: classes.vResizeHandle + }} + bounds={'parent'} + size={{ + width: sideWidth, + height: '' + }} + > + + + )} + {graphExplorerMode === Mode.TryIt && + headerMessagingV9(query)} + + {displayContent && ( + +
+ +
+
+
+ +
+ +
+ +
+ )}
- +
-
- +
*/} + {/* - + */}
@@ -537,8 +554,14 @@ class App extends Component { } } -const mapStateToProps = ({ sidebarProperties, theme, dimensions, - profile, sampleQuery, auth: { authToken }, graphExplorerMode +const mapStateToProps = ({ + sidebarProperties, + theme, + dimensions, + profile, + sampleQuery, + auth: { authToken }, + graphExplorerMode }: ApplicationState) => { const mobileScreen = !!sidebarProperties.mobileScreen; const showSidebar = !!sidebarProperties.showSidebar; @@ -576,4 +599,4 @@ const mapDispatchToProps = (dispatch: Dispatch) => { const StyledApp = styled(App, appStyles as any); //@ts-ignore -export default connect(mapStateToProps, mapDispatchToProps)(StyledApp); \ No newline at end of file +export default connect(mapStateToProps, mapDispatchToProps)(StyledApp); diff --git a/src/app/views/common/banners/Notification.styles.ts b/src/app/views/common/banners/Notification.styles.ts index 08709d734c..87acd3f483 100644 --- a/src/app/views/common/banners/Notification.styles.ts +++ b/src/app/views/common/banners/Notification.styles.ts @@ -4,7 +4,6 @@ import polygons from './bgPolygons.svg'; export const useNotificationStyles = makeStyles({ container: { padding: '8px', - marginBottom: '8px', backgroundImage: `url(${polygons})`, backgroundRepeat: 'no-repeat', backgroundSize: 'contain', @@ -20,6 +19,9 @@ export const useNotificationStyles = makeStyles({ '&.highContrast': { backgroundColor: '#0C3B5E', color: '#ffffff' + }, + '&:empty': { + display: 'none' } }, body: { diff --git a/src/app/views/common/copy-button/CopyButtonV9.tsx b/src/app/views/common/copy-button/CopyButtonV9.tsx index f42e1f43c3..b56e445be3 100644 --- a/src/app/views/common/copy-button/CopyButtonV9.tsx +++ b/src/app/views/common/copy-button/CopyButtonV9.tsx @@ -1,11 +1,11 @@ -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'; +import { useRef, useState } from 'react'; +import { translateMessage } from '../../../utils/translate-messages'; interface ICopyButtonProps { style?: React.CSSProperties; - handleOnClick: Function; + handleOnClick: (props: ICopyButtonProps) => void; className?: string; isIconButton: boolean; } @@ -23,7 +23,9 @@ export default function CopyButton(props: ICopyButtonProps) { const copyRef = useRef(null); const styles = useStyles(); - const copyLabel: string = !copied ? translateMessage('Copy') : translateMessage('Copied'); + const copyLabel: string = !copied + ? translateMessage('Copy') + : translateMessage('Copied'); const handleCopyClick = async () => { props.handleOnClick(props); @@ -40,9 +42,9 @@ export default function CopyButton(props: ICopyButtonProps) { return ( <> {props.isIconButton ? ( - + )} ); -} \ No newline at end of file +} diff --git a/src/app/views/common/dimensions/dimensions-adjustment.ts b/src/app/views/common/dimensions/dimensions-adjustment.ts index a8d8ef8947..6bdf1a5056 100644 --- a/src/app/views/common/dimensions/dimensions-adjustment.ts +++ b/src/app/views/common/dimensions/dimensions-adjustment.ts @@ -3,6 +3,11 @@ export function convertVhToPx(height: string, adjustment: number) { * document.documentElement.clientHeight; return Math.floor(newHeight - adjustment) + 'px'; } +export function convertPercentToPx(percent: string, adjustment: number) { + const newHeight = parseFloat(percent.replace('%', '')) / 100 + * document.documentElement.clientHeight; + return Math.floor(newHeight - adjustment) + 'px'; +} export function convertPxToVh(px: number){ const innerHeight = window.innerHeight; diff --git a/src/app/views/common/monaco/Monaco.tsx b/src/app/views/common/monaco/Monaco.tsx index 34f6333c4f..8313b03423 100644 --- a/src/app/views/common/monaco/Monaco.tsx +++ b/src/app/views/common/monaco/Monaco.tsx @@ -1,12 +1,9 @@ -import { FocusZone } from '@fluentui/react'; -import * as monaco from 'monaco-editor/esm/vs/editor/editor.api'; import Editor, { OnChange, loader } from '@monaco-editor/react'; -import { useEffect } from 'react'; - +import * as monaco from 'monaco-editor/esm/vs/editor/editor.api'; +import { useContext, useEffect, useRef, useState } from 'react'; import { ThemeContext } from '../../../../themes/theme-context'; -import './monaco.scss'; +import { ResizeContext } from '../../../services/context/resize-context/ResizeContext'; import { formatJsonStringForAllBrowsers } from './util/format-json'; - interface IMonaco { body: object | string | undefined; onChange?: OnChange; @@ -19,12 +16,12 @@ interface IMonaco { export function Monaco(props: IMonaco) { let { body } = props; - const { onChange, language, readOnly, height } = props; + const { onChange, language, readOnly } = props; + const { parentHeight, dragValue, initiator } = useContext(ResizeContext); if (body && typeof body !== 'string') { body = formatJsonStringForAllBrowsers(body); } - const itemHeight = height ? height : '300px'; loader.config({ monaco }); @@ -40,15 +37,30 @@ export function Monaco(props: IMonaco) { } }, [monaco]); + const editorDidMount = (editor: monaco.editor.IStandaloneCodeEditor) => { + editorRef.current = editor; + }; + + const editorRef = useRef(null); + const [editorHeight, setEditorHeight] = useState(300); + useEffect(() => { + if (initiator === 'responseSize') { + if (editorRef.current && dragValue > 0) { + const newHeight = parentHeight - dragValue - 124; + setEditorHeight(Math.max(300, newHeight)); + } + } + }, [parentHeight, dragValue, initiator]); + return ( - -
- {props.extraInfoElement} - - {(theme) => ( + {props.extraInfoElement} + + {/* TODO: handle this theme differently? */} + {(theme) => ( + )} - -
-
+ onMount={editorDidMount} + /> + )} + + ); -} \ No newline at end of file +} diff --git a/src/app/views/common/monaco/monaco.scss b/src/app/views/common/monaco/monaco.scss deleted file mode 100644 index 168d05a5d9..0000000000 --- a/src/app/views/common/monaco/monaco.scss +++ /dev/null @@ -1,18 +0,0 @@ -@import "../../../../styles/variables"; - -.monaco-editor { - margin-left: 1px; - margin-top: 5px; - padding-bottom: $gutter; - width: inherit !important; - overflow: hidden; - - .react-monaco-editor-container { - width: inherit !important; - } - - .overflow-guard { - width: inherit !important; - } -} - diff --git a/src/app/views/layout/Layout.tsx b/src/app/views/layout/Layout.tsx new file mode 100644 index 0000000000..ab1b3b9983 --- /dev/null +++ b/src/app/views/layout/Layout.tsx @@ -0,0 +1,258 @@ +import { useResizeHandle } from '@fluentui-contrib/react-resize-handle'; +import { + makeResetStyles, + makeStyles, + mergeClasses, + tokens +} from '@fluentui/react-components'; +import { useEffect, useState } from 'react'; +import { useAppDispatch, useAppSelector } from '../../../store'; +import CollectionPermissionsProvider from '../../services/context/collection-permissions/CollectionPermissionsProvider'; +import { ResizeProvider } from '../../services/context/resize-context/ResizeProvider'; +import { ValidationProvider } from '../../services/context/validation-context/ValidationProvider'; +import { BANNER_IS_VISIBLE } from '../../services/graph-constants'; +import { setSampleQuery } from '../../services/slices/sample-query.slice'; +import { translateMessage } from '../../utils/translate-messages'; +import { StatusMessagesV9, TermsOfUseMessageV9 } from '../app-sections'; +import Notification from '../common/banners/Notification'; +import PopupsWrapper from '../common/popups/PopupsWrapper'; +import { MainHeaderV9 } from '../main-header/MainHeaderV9'; +import { QueryResponse } from '../query-response'; +import { QueryRunner } from '../query-runner'; +import { styles } from '../query-runner/query-input/auto-complete/suffix/suffix.styles'; +import RequestV9 from '../query-runner/request/RequestV9'; +import { SidebarV9 } from '../sidebar/SidebarV9'; +import { LayoutResizeHandler } from './LayoutResizeHandler'; + +const RESPONSE_AREA_SIZE_CSS_VAR = '--response-area-size'; +const RESPONSE_AREA_SIZE_VAR = 66; +const SIDEBAR_SIZE_CSS_VAR = '--sidebar-size'; + +interface LayoutProps { + handleSelectVerb: (verb: string) => void; + onDragEnd: (value: number, eventType: string, initiator: string) => void; +} + +const storageBanner = localStorage.getItem(BANNER_IS_VISIBLE); +const bannerIsVisible = storageBanner === null || storageBanner === 'true'; + +const usePageStyles = makeResetStyles({ + height: '100vh', + backgroundColor: tokens.colorNeutralBackground1, + padding: tokens.spacingHorizontalXS +}); + +const useMainWrapperStyles = makeResetStyles({ + [SIDEBAR_SIZE_CSS_VAR]: '460px', + [RESPONSE_AREA_SIZE_CSS_VAR]: `${RESPONSE_AREA_SIZE_VAR}%`, + display: 'grid', + width: '100%', + height: '100%', + gap: tokens.spacingVerticalS, + gridTemplateAreas: ` + "header header" + "sidebar mainArea" + "footer footer" + `, + gridTemplateRows: '48px 1fr auto', + gridTemplateColumns: `clamp(60px, var(${SIDEBAR_SIZE_CSS_VAR}), 40%) 1fr` +}); +const bannerRow = `${bannerIsVisible ? 'auto' : ''}`; +const useMainAreaWrapperStyles = makeResetStyles({ + display: 'grid', + gridArea: 'mainArea', + gridTemplate: ` + ${bannerIsVisible ? '"notificationArea"' : ''} + "queryArea" + "requestArea" + "responseArea" + `, + gridTemplateRows: `${bannerRow} 36px minmax(30%, 80%) clamp(16%, var(${RESPONSE_AREA_SIZE_CSS_VAR}), 80%)`, + gridTemplateColumns: '1fr', + position: 'relative', + gap: tokens.spacingHorizontalS +}); + +const useStyles = makeStyles({ + areaHeader: { + gridArea: 'header' + }, + areaSidebar: { + gridArea: 'sidebar', + padding: `0 ${tokens.spacingHorizontalS}`, + backgroundColor: tokens.colorNeutralBackground6, + borderRightStyle: 'solid', + borderRightColor: tokens.colorNeutralStroke1, + borderRightWidth: tokens.strokeWidthThin + }, + notificationArea: { + gridArea: 'notificationArea', + alignContent: 'center' + }, + queryArea: { + gridArea: 'queryArea' + }, + requestArea: { + gridArea: 'requestArea', + display: 'flex', + flexDirection: 'column', + gap: tokens.spacingHorizontalS, + padding: tokens.spacingHorizontalXS, + border: `solid ${tokens.colorNeutralStroke1} ${tokens.strokeWidthThin}`, + borderRadius: tokens.borderRadiusMedium + }, + responseArea: { + gridArea: 'responseArea', + padding: tokens.spacingHorizontalXS, + border: `solid ${tokens.colorNeutralStroke1} ${tokens.strokeWidthThin}`, + borderRadius: tokens.borderRadiusMedium + }, + + areaFooter: { + gridArea: 'footer', + backgroundColor: 'yellow' + } +}); + +const useMainBoxStyles = makeResetStyles({ + position: 'relative' +}); + +const Layout = (props: LayoutProps) => { + const pageStyles = usePageStyles(); + const boxStyles = useMainBoxStyles(); + const styles = useStyles(); + const mainAreaStyles = useMainAreaWrapperStyles(); + const wrapperStyles = useMainWrapperStyles(); + + const { + handleRef: sidebarHandleRef, + wrapperRef: sidebarWrapperRef, + elementRef: sidebarElementRef, + setValue: setSidebarColumnSize + } = useResizeHandle({ + variableName: SIDEBAR_SIZE_CSS_VAR, + growDirection: 'end', + // relative: true, + onDragEnd: (_, { value, type }) => { + console.log('sidebar'); + props.onDragEnd(value, String(type), 'sidebar'); + } + }); + + const { + handleRef: responseAreaHandleRef, + wrapperRef: responseAreaWrapperRef, + elementRef: responseAreaElementRef, + setValue: setResponseAreaRowSize + } = useResizeHandle({ + variableName: RESPONSE_AREA_SIZE_CSS_VAR, + growDirection: 'up', + onDragEnd: (_, { value, type }) => { + props.onDragEnd(value, String(type), 'responseSize'); + } + }); + + const resetSidebarArea = () => { + setSidebarColumnSize(460); + props.onDragEnd(460, '', 'sidebar'); + }; + + const resetResponseArea = () => { + const layout = document.getElementById('layout'); + if (layout) { + const parentHeight = layout.parentElement?.offsetHeight ?? 790; + const resize = Math.floor((RESPONSE_AREA_SIZE_VAR / 100) * parentHeight); + setResponseAreaRowSize(resize); + props.onDragEnd(resize, '', 'responseSize'); + } + }; + + const dispatch = useAppDispatch(); + const sampleQuery = useAppSelector((state) => state.sampleQuery); + const [sampleBody, setSampleBody] = useState(''); + + useEffect(() => { + if (sampleQuery.selectedVerb !== 'GET') { + const query = { ...sampleQuery }; + query.sampleBody = sampleBody; + dispatch(setSampleQuery(query)); + } + }, [sampleBody]); + + const handleOnEditorChange = (value?: string) => { + setSampleBody(value!); + }; + + return ( +
+
+
+ +
+ +
+ {/* TODO: use the onDrag event to resize this sidebar */} + + +
+
+ {bannerIsVisible && ( +
+ +
+ )} + +
+ +
+
+ + +
+
+ + +
+
+ + + +
+
+ +
+ +
+
+
+ ); +}; + +export { Layout }; diff --git a/src/app/views/layout/LayoutResizeHandler.tsx b/src/app/views/layout/LayoutResizeHandler.tsx new file mode 100644 index 0000000000..a2932db2f5 --- /dev/null +++ b/src/app/views/layout/LayoutResizeHandler.tsx @@ -0,0 +1,70 @@ +import { makeResetStyles, tokens, useFluent } from '@fluentui/react-components'; +import * as React from 'react'; + +interface HandleProps { + position: 'start' | 'end' | 'top' | 'bottom'; + onDoubleClick?: () => void; +} + +const useHoverStyles = makeResetStyles({ + ':hover': { + backgroundColor: tokens.colorBrandBackgroundHover + } +}) +export const LayoutResizeHandler = React.forwardRef( + (props, ref) => { + const { position, ...rest } = props; + const { dir } = useFluent(); + const hoverStyles = useHoverStyles() + + const handleClick = (event: React.MouseEvent) => { + if (event.detail === 2) { + props.onDoubleClick?.(); + } + }; + + const positioningAttr = + dir === 'ltr' + ? position === 'start' + ? 'left' + : 'right' + : position === 'start' + ? 'right' + : 'left'; + + const positioningProps = + position === 'start' || position === 'end' + ? { + [positioningAttr]: '-7px', + top: '50%', + transform: 'translateY(-50%)', + width: '3px', + height: '100%', + cursor: 'col-resize' + } + : { + ...(position === 'top' ? { top: '-6.5px' } : { bottom: '-6.5px' }), + left: '50%', + transform: 'translateX(-50%)', + width: '100%', + height: '3px', + cursor: 'row-resize' + }; + + return ( +
+ ); + } +); +LayoutResizeHandler.displayName = 'LayoutResizeHandler'; \ No newline at end of file diff --git a/src/app/views/main-header/MainHeaderV9.tsx b/src/app/views/main-header/MainHeaderV9.tsx index 390de1e61d..6ff7475034 100644 --- a/src/app/views/main-header/MainHeaderV9.tsx +++ b/src/app/views/main-header/MainHeaderV9.tsx @@ -11,8 +11,7 @@ const useStyles = makeStyles({ justifyContent: 'space-between', padding: '0 0 0 24px', height: '48px', - background: tokens.colorNeutralBackground4, - marginBottom: '8px' // TODO: remove when sidebar and query areas are updated + background: tokens.colorNeutralBackground4 }, headerIcons: { display: 'flex', diff --git a/src/app/views/query-runner/QueryRunner.tsx b/src/app/views/query-runner/QueryRunner.tsx index 8df285b27f..3b33e697c5 100644 --- a/src/app/views/query-runner/QueryRunner.tsx +++ b/src/app/views/query-runner/QueryRunner.tsx @@ -12,10 +12,12 @@ import { sanitizeQueryUrl } from '../../utils/query-url-sanitization'; 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/RequestV9'; -const QueryRunner = (props: any) => { +interface IQueryRunnerProps { + onSelectVerb: (verb: string) => void; +} + +const QueryRunner = (props: IQueryRunnerProps) => { const dispatch = useAppDispatch(); const sampleQuery = useAppSelector((state) => state.sampleQuery); @@ -40,10 +42,6 @@ const QueryRunner = (props: any) => { } }; - const handleOnEditorChange = (value?: string) => { - setSampleBody(value!); - }; - const handleOnRunQuery = (query?: IQuery) => { let sample = { ...sampleQuery }; if (sampleBody && sample.selectedVerb !== 'GET') { @@ -51,7 +49,7 @@ const QueryRunner = (props: any) => { const contentType = headers.find((k: { name: string; }) => k.name.toLowerCase() === 'content-type'); if (!contentType || (contentType.value === ContentType.Json)) { try { - sample.sampleBody = JSON.parse(sampleBody); + sample.sampleBody = JSON.parse(sampleBody) as string; } catch (error) { dispatch(setQueryResponseStatus({ ok: false, @@ -112,27 +110,11 @@ const QueryRunner = (props: any) => { }; return ( - <> -
-
- -
-
-
-
- { - - } -
-
- + ); } diff --git a/src/app/views/query-runner/request/RequestV9.tsx b/src/app/views/query-runner/request/RequestV9.tsx index f5f40f8d41..c6cf96e8ea 100644 --- a/src/app/views/query-runner/request/RequestV9.tsx +++ b/src/app/views/query-runner/request/RequestV9.tsx @@ -1,23 +1,29 @@ import { Resizable } from 're-resizable'; import { CSSProperties, useEffect, useState } from 'react'; +import { makeStyles, Tab, TabList, TabValue } from '@fluentui/react-components'; import { useAppDispatch, useAppSelector } from '../../../../store'; import { telemetry } from '../../../../telemetry'; import { Mode } from '../../../../types/enums'; +import { IQuery } from '../../../../types/query-runner'; 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 { + 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 - } +interface IRequestProps { + handleOnEditorChange: () => void; + sampleQuery: IQuery; +} const useStyles = makeStyles({ resizable: { @@ -34,25 +40,15 @@ const useStyles = makeStyles({ } }); -const Request = (props: IRequestProps) => { +const RequestV9 = (props: IRequestProps) => { const styles = useStyles(); const dispatch = useAppDispatch(); const [selectedTab, setSelectedTab] = useState('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) { @@ -80,7 +76,10 @@ const Request = (props: IRequestProps) => { }, response: { ...dimensions.response, - height: `${maxDeviceVerticalHeight - parseFloat(requestHeightInVh.replace('vh', ''))}vh` + height: `${ + maxDeviceVerticalHeight - + parseFloat(requestHeightInVh.replace('vh', '')) + }vh` } }; @@ -91,62 +90,65 @@ const Request = (props: IRequestProps) => { const resizable = document.getElementsByClassName('request-resizable'); if (resizable && resizable.length > 0) { const resizableElement = resizable[0] as HTMLElement; - if (resizableElement && resizableElement.style && resizableElement.style.height) { + if ( + resizableElement && + resizableElement.style && + resizableElement.style.height + ) { resizableElement.style.height = ''; } } }; return ( - { - 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 - }} - > -
- handleTabSelect(data.value)} - className={styles.tabList} +
+ handleTabSelect(data.value)} + className={styles.tabList} + > + - - {translateMessage('Request Body')} - - - {translateMessage('Request Headers')} - - - {translateMessage('Modify Permissions')} + {translateMessage('Request Body')} + + + {translateMessage('Request Headers')} + + + {translateMessage('Modify Permissions')} + + {mode === Mode.Complete && ( + + {translateMessage('Access Token')} - {mode === Mode.Complete && ( - - {translateMessage('Access Token')} - - )} - + )} + -
- {selectedTab === 'request-body' && } - {selectedTab === 'request-headers' && } - {selectedTab === 'modify-permissions' && } - {selectedTab === 'access-token' && mode === Mode.Complete && } -
+
+ {selectedTab === 'request-body' && ( + + )} + {selectedTab === 'request-headers' && } + {selectedTab === 'modify-permissions' && } + {selectedTab === 'access-token' && mode === Mode.Complete && }
- +
); }; -export default Request; \ No newline at end of file +export default RequestV9; diff --git a/src/app/views/query-runner/request/auth/AuthV9.tsx b/src/app/views/query-runner/request/auth/AuthV9.tsx index b4512491fe..ff2cf32795 100644 --- a/src/app/views/query-runner/request/auth/AuthV9.tsx +++ b/src/app/views/query-runner/request/auth/AuthV9.tsx @@ -1,41 +1,41 @@ -import { useEffect, useState } from 'react'; +import { AuthenticationResult } from '@azure/msal-browser'; import { Button, - Text, - Tooltip, makeStyles, - MessageBar + MessageBar, + Text, + tokens, + Tooltip } from '@fluentui/react-components'; -import { AuthenticationResult } from '@azure/msal-browser'; +import { useEffect, useState } from 'react'; import { authenticationWrapper } from '../../../../../modules/authentication'; import { useAppSelector } from '../../../../../store'; +import { BracesRegular } from '@fluentui/react-icons'; 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'; +import { CopyButton } from '../../../common/lazy-loader/component-registry'; const useStyles = makeStyles({ auth: { - padding: '5px', + padding: tokens.spacingVerticalSNudge, overflowY: 'auto' }, accessTokenContainer: { - width: '160px', display: 'flex', flexDirection: 'row', - justifyContent: 'space-between', + justifyContent: 'start', alignItems: 'center', - paddingBottom: '10px' + gap: tokens.spacingVerticalS, + paddingBottom: tokens.spacingVerticalMNudge }, accessToken: { wordWrap: 'break-word', - fontFamily: 'monospace', - fontSize: '12px', - width: '100%', + width: 'calc(100vw - var(--sidebar-size) - 4rem)', + padding: tokens.spacingHorizontalXS, height: '100%', border: 'none', resize: 'none' @@ -55,17 +55,22 @@ const useStyles = makeStyles({ export function Auth() { const profile = useAppSelector((state) => state.profile); - const height: string = useAppSelector((state) => state.dimensions.request.height); + 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 requestHeight = convertVhToPx(height, 5); const [accessToken, setAccessToken] = useState(null); const [loading, setLoading] = useState(false); const styles = useStyles(); const handleCopy = async () => { - trackedGenericCopy(accessToken || '', componentNames.ACCESS_TOKEN_COPY_BUTTON); + trackedGenericCopy( + accessToken || '', + componentNames.ACCESS_TOKEN_COPY_BUTTON + ); }; useEffect(() => { @@ -96,25 +101,36 @@ export function Auth() { {!loading ? (
- {translateMessage('Access Token')} - - + + {translateMessage('Access Token')} + + +
- - {accessToken} - +
+ + {accessToken} + +
) : ( @@ -132,4 +148,7 @@ export function Auth() { } } -export default telemetry.trackReactComponent(Auth, componentNames.ACCESS_TOKEN_TAB); \ No newline at end of file +export default telemetry.trackReactComponent( + Auth, + componentNames.ACCESS_TOKEN_TAB +); diff --git a/src/app/views/query-runner/request/body/RequestBody.tsx b/src/app/views/query-runner/request/body/RequestBody.tsx index 3d7f1d2fbc..46d7d227b7 100644 --- a/src/app/views/query-runner/request/body/RequestBody.tsx +++ b/src/app/views/query-runner/request/body/RequestBody.tsx @@ -5,21 +5,19 @@ import { Monaco } from '../../../common'; import { convertVhToPx } from '../../../common/dimensions/dimensions-adjustment'; interface IRequestBodyProps { - handleOnEditorChange: (v: string | undefined)=> void; + handleOnEditorChange: (v: string | undefined) => void; } const RequestBody = ({ handleOnEditorChange }: IRequestBodyProps) => { - const height = useAppSelector((state)=> state.dimensions.request.height); - const sampleBody = useAppSelector((state)=> state.sampleQuery.sampleBody); + const sampleBody = useAppSelector((state) => state.sampleQuery.sampleBody); return ( - +
handleOnEditorChange(value)} /> - - + onChange={(value) => handleOnEditorChange(value)} + /> +
); }; diff --git a/src/app/views/query-runner/request/headers/Headers.styles.ts b/src/app/views/query-runner/request/headers/Headers.styles.ts deleted file mode 100644 index e7968a1369..0000000000 --- a/src/app/views/query-runner/request/headers/Headers.styles.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { FontSizes } from '@fluentui/react'; - -export const headerStyles = () => { - return { - container: { - textAlign: 'center', - padding: 10, - overflowY: 'auto', - overflowX: 'hidden' - }, - itemContent: { - marginTop: '2.5%' - }, - rowContainer: { - fontSize: FontSizes.medium, - position: 'relative' - }, - detailsRow: { - display: 'flex', - justifyContent: 'center', - alignItems: 'center' - }, - headersList: { marginBottom: '120px' } - }; -}; diff --git a/src/app/views/query-runner/request/headers/HeadersList.tsx b/src/app/views/query-runner/request/headers/HeadersList.tsx index d1bc588db1..e11252dd30 100644 --- a/src/app/views/query-runner/request/headers/HeadersList.tsx +++ b/src/app/views/query-runner/request/headers/HeadersList.tsx @@ -1,82 +1,74 @@ -import { DetailsList, DetailsRow, IColumn, IconButton, SelectionMode } from '@fluentui/react'; +import { + Button, + Table, + TableBody, + TableCell, + TableCellActions, + TableHeader, + TableHeaderCell, + TableRow, + Text +} from '@fluentui/react-components'; +import { DeleteRegular, EditRegular } from '@fluentui/react-icons'; import { IHeadersListControl } from '../../../../../types/request'; import { translateMessage } from '../../../../utils/translate-messages'; -import { headerStyles } from './Headers.styles'; +const columns = [ + { + key: 'key', + name: translateMessage('Key') + }, + { + key: 'value', + name: translateMessage('Value') + } +]; const HeadersList = ({ handleOnHeaderDelete, headers, handleOnHeaderEdit }: IHeadersListControl) => { - - const renderItemColumn = (item: any, index: number | undefined, column: IColumn | undefined) => { - const itemContent: any = headerStyles().itemContent; - if (column) { - const fieldContent = item[column.fieldName as keyof any] as string; - if(column.key === 'button') { - return
- handleOnHeaderDelete(item)} - /> - | - handleOnHeaderEdit(item)} - /> -
; - } - else{ - return
{fieldContent}
; - } - } - }; - - const renderRow = (props: any): any => { - const { detailsRow, rowContainer }: any = headerStyles(); - - if (props) { - return ( -
- -
- ); - } - }; - - const columns = [ - { - key: 'key', name: translateMessage('Key'), fieldName: 'name', minWidth: 300, - maxWidth: 400, ariaLabel: translateMessage('Key') - }, - { - key: 'value', name: translateMessage('Value'), fieldName: 'value', minWidth: 300, - maxWidth: 400, ariaLabel: translateMessage('Value') - }, - { - key: 'button', name: translateMessage('actions'), fieldName: 'button', minWidth: 200, - maxWidth: 300, ariaLabel: translateMessage('actions') - } - ]; - - const headerItems = (headers) ? headers.filter((header) => { - return header.value !== ''; - }) : []; + const headerItems = headers + ? headers.filter((header) => header.value !== '') + : []; return ( - + + + + {columns.map((column) => ( + + {column.name} + + ))} + + + + {headerItems.map((item, index) => ( + + {item.name} + + {item.value} + + + + + + + ))} + +
); }; diff --git a/src/app/views/query-runner/request/headers/RequestHeaders.tsx b/src/app/views/query-runner/request/headers/RequestHeaders.tsx deleted file mode 100644 index 841472c6b7..0000000000 --- a/src/app/views/query-runner/request/headers/RequestHeaders.tsx +++ /dev/null @@ -1,137 +0,0 @@ -import { Announced, ITextField, PrimaryButton, styled, TextField } from '@fluentui/react'; -import { createRef, useState } from 'react'; - -import { useAppDispatch, useAppSelector } from '../../../../../store'; -import { setSampleQuery } from '../../../../services/slices/sample-query.slice'; -import { translateMessage } from '../../../../utils/translate-messages'; -import { classNames } from '../../../classnames'; -import { convertVhToPx } from '../../../common/dimensions/dimensions-adjustment'; -import { headerStyles } from './Headers.styles'; -import HeadersList from './HeadersList'; - -interface IHeader { - name: string; - value: string; -} - -const RequestHeaders = (props: any) => { - const { sampleQuery, dimensions: { request: { height } } } = useAppSelector((state) => state); - const [announcedMessage, setAnnouncedMessage] = useState(''); - const [isHoverOverHeadersList, setIsHoverOverHeadersList] = useState(false); - const [isUpdatingHeader, setIsUpdatingHeader] = useState(false); - - const emptyHeader = { name: '', value: '' }; - const [header, setHeader] = useState(emptyHeader); - - const sampleQueryHeaders = sampleQuery.sampleHeaders; - - const dispatch = useAppDispatch(); - const classes = classNames(props); - - const textfieldRef = createRef(); - const onSetFocus = () => textfieldRef.current!.focus(); - - const changeHeaderProperties = - (event: React.FormEvent) => { - setHeader({ ...header, [event.currentTarget.name]: event.currentTarget.value }); - }; - - const handleOnHeaderDelete = (headerToDelete: IHeader) => { - let headers = [...sampleQuery.sampleHeaders]; - headers = headers.filter(head => head.name !== headerToDelete.name); - - const query = { ...sampleQuery }; - query.sampleHeaders = headers; - - dispatch(setSampleQuery(query)); - setAnnouncedMessage(translateMessage('Request Header deleted')); - onSetFocus(); //set focus to textfield after an item is deleted - }; - - const handleOnHeaderAdd = () => { - if (header.name && header.value) { - let { sampleHeaders } = sampleQuery; - - if (!sampleHeaders) { - sampleHeaders = [{ - name: '', - value: '' - }]; - } - - const newHeaders = [header, ...sampleHeaders]; - setHeader(emptyHeader); - setAnnouncedMessage(translateMessage('Request Header added')); - setIsUpdatingHeader(false); - - const query = { ...sampleQuery }; - query.sampleHeaders = newHeaders; - dispatch(setSampleQuery(query)); - } - }; - - const handleOnHeaderEdit = (headerToEdit: IHeader) => { - if (header.name !== '') { - return; - } - removeHeaderFromSampleQuery(headerToEdit); - setIsUpdatingHeader(true); - setHeader({ ...headerToEdit }); - onSetFocus(); - } - - const removeHeaderFromSampleQuery = (headerToRemove: IHeader) => { - let headers = [...sampleQuery.sampleHeaders]; - headers = headers.filter(head => head.name !== headerToRemove.name); - const query = { ...sampleQuery }; - query.sampleHeaders = headers; - dispatch(setSampleQuery(query)); - } - - return ( -
setIsHoverOverHeadersList(true)} - onMouseLeave={() => setIsHoverOverHeadersList(false)} - className={classes.container} - style={isHoverOverHeadersList ? { height: convertVhToPx(height, 60) } : - { height: convertVhToPx(height, 60), overflow: 'hidden' }}> - -
-
- -
-
- -
-
- - {translateMessage(isUpdatingHeader ? 'Update' : 'Add')} - -
-
-
- handleOnHeaderDelete(headerToDelete)} - headers={sampleQueryHeaders} - handleOnHeaderEdit={(headerToEdit: IHeader) => handleOnHeaderEdit(headerToEdit)} - /> -
- ); -}; -// @ts-ignore -const styledRequestHeaders = styled(RequestHeaders, headerStyles); -export default styledRequestHeaders; diff --git a/src/app/views/query-runner/request/headers/RequestHeadersV9.tsx b/src/app/views/query-runner/request/headers/RequestHeadersV9.tsx index de690f06d7..7cba38ec88 100644 --- a/src/app/views/query-runner/request/headers/RequestHeadersV9.tsx +++ b/src/app/views/query-runner/request/headers/RequestHeadersV9.tsx @@ -1,11 +1,9 @@ -import { Button, Input, makeStyles, shorthands } from '@fluentui/react-components'; -import { useRef, useState } from 'react'; - +import { Button, Input, makeStyles, tokens } from '@fluentui/react-components'; +import { useState } from 'react'; import { useAppDispatch, useAppSelector } from '../../../../../store'; import { setSampleQuery } from '../../../../services/slices/sample-query.slice'; import { translateMessage } from '../../../../utils/translate-messages'; import HeadersList from './HeadersList'; -import { convertVhToPx } from '../../../common/dimensions/dimensions-adjustment'; interface IHeader { name: string; @@ -14,15 +12,14 @@ interface IHeader { const useStyles = makeStyles({ container: { - textAlign: 'center', - padding: '10px', - overflowY: 'auto', - overflowX: 'hidden' + padding: tokens.spacingHorizontalXS, + display: 'flex', + flexDirection: 'column', + gap: tokens.spacingVerticalS }, - row: { + headerInputs: { display: 'flex', - gap: '16px', - marginBottom: '16px', + gap: tokens.spacingHorizontalM, alignItems: 'center' }, input: { @@ -31,19 +28,13 @@ const useStyles = makeStyles({ button: { flexShrink: 0, minWidth: '80px' - }, - listContainer: { - flex: 1, - overflow: 'auto' } }); const RequestHeaders = () => { const sampleQuery = useAppSelector((state) => state.sampleQuery); - const height = useAppSelector((state) => state.dimensions.request.height); const [header, setHeader] = useState({ name: '', value: '' }); const [isUpdatingHeader, setIsUpdatingHeader] = useState(false); - const [isHoverOverHeadersList, setIsHoverOverHeadersList] = useState(false); const dispatch = useAppDispatch(); const styles = useStyles(); @@ -55,67 +46,62 @@ const RequestHeaders = () => { const handleAddHeader = () => { if (header.name.trim() && header.value.trim()) { const updatedHeaders = [header, ...(sampleQuery.sampleHeaders || [])]; - dispatch(setSampleQuery({ ...sampleQuery, sampleHeaders: updatedHeaders })); + dispatch( + setSampleQuery({ ...sampleQuery, sampleHeaders: updatedHeaders }) + ); setHeader({ name: '', value: '' }); setIsUpdatingHeader(false); } }; const handleDeleteHeader = (headerToDelete: IHeader) => { - const updatedHeaders = sampleQuery.sampleHeaders.filter((h) => h.name !== headerToDelete.name); + const updatedHeaders = sampleQuery.sampleHeaders.filter( + (h) => h.name !== headerToDelete.name + ); dispatch(setSampleQuery({ ...sampleQuery, sampleHeaders: updatedHeaders })); }; const handleEditHeader = (headerToEdit: IHeader) => { setHeader(headerToEdit); setIsUpdatingHeader(true); - const updatedHeaders = sampleQuery.sampleHeaders.filter((h) => h.name !== headerToEdit.name); + const updatedHeaders = sampleQuery.sampleHeaders.filter( + (h) => h.name !== headerToEdit.name + ); dispatch(setSampleQuery({ ...sampleQuery, sampleHeaders: updatedHeaders })); }; return ( -
setIsHoverOverHeadersList(true)} - onMouseLeave={() => setIsHoverOverHeadersList(false)} - style={ - isHoverOverHeadersList - ? { height: convertVhToPx(height, 60) } - : { height: convertVhToPx(height, 60), overflow: 'hidden' } - } - > -
+
+
-
- -
+
); }; -export default RequestHeaders; \ No newline at end of file +export default RequestHeaders; diff --git a/src/app/views/query-runner/request/permissions/Permission.stylesV9.ts b/src/app/views/query-runner/request/permissions/Permission.stylesV9.ts index 24b4ac0f99..d1f30e0e33 100644 --- a/src/app/views/query-runner/request/permissions/Permission.stylesV9.ts +++ b/src/app/views/query-runner/request/permissions/Permission.stylesV9.ts @@ -33,7 +33,7 @@ const permissionStyles = makeStyles({ }, icon: { position: 'relative', - top: '4px', + top: '2px', cursor: 'pointer' }, iconButton: { @@ -48,6 +48,10 @@ const permissionStyles = makeStyles({ }, headerText: { marginLeft: '8px' + }, + value: { + display: 'flex', + gap: '2px' } }); diff --git a/src/app/views/query-runner/request/permissions/PermissionItemV9.tsx b/src/app/views/query-runner/request/permissions/PermissionItemV9.tsx index 76889f7f60..4429eed1a5 100644 --- a/src/app/views/query-runner/request/permissions/PermissionItemV9.tsx +++ b/src/app/views/query-runner/request/permissions/PermissionItemV9.tsx @@ -1,8 +1,9 @@ import { Button, - Tooltip, - Label + Label, + Tooltip } from '@fluentui/react-components'; +import { InfoRegular } from '@fluentui/react-icons'; import { useAppDispatch, useAppSelector } from '../../../../../store'; import { IPermission, IPermissionGrant } from '../../../../../types/permissions'; import { revokeScopes } from '../../../../services/actions/revoke-scopes.action'; @@ -11,7 +12,6 @@ import { consentToScopes } from '../../../../services/slices/auth.slice'; import { getAllPrincipalGrant, getSinglePrincipalGrant } from '../../../../services/slices/permission-grants.slice'; import { translateMessage } from '../../../../utils/translate-messages'; import { PermissionConsentType } from './ConsentType'; -import { InfoRegular } from '@fluentui/react-icons'; import permissionStyles from './Permission.stylesV9'; interface PermissionItemProps { @@ -89,7 +89,10 @@ const PermissionItem = (props: PermissionItemProps): JSX.Element | null => { ); } return ( - + @@ -108,10 +111,10 @@ const PermissionItem = (props: PermissionItemProps): JSX.Element | null => { switch (column.key) { case 'value': return ( -
+
{content} {props.index === 0 && ( - + )} @@ -130,7 +133,7 @@ const PermissionItem = (props: PermissionItemProps): JSX.Element | null => { case 'consentDescription': return ( - + {item.consentDescription} ); @@ -140,7 +143,7 @@ const PermissionItem = (props: PermissionItemProps): JSX.Element | null => { default: return ( - + {content} ); diff --git a/src/app/views/query-runner/request/permissions/Permissions.QueryV9.tsx b/src/app/views/query-runner/request/permissions/Permissions.QueryV9.tsx index 87cad45265..f2c720fb5a 100644 --- a/src/app/views/query-runner/request/permissions/Permissions.QueryV9.tsx +++ b/src/app/views/query-runner/request/permissions/Permissions.QueryV9.tsx @@ -1,10 +1,16 @@ import { - Table, TableHeader, TableRow, TableCell, TableBody, - Link, Text + Link, + Table, + TableBody, + TableCell, + TableHeader, + TableRow, + Text } from '@fluentui/react-components'; import { useContext, useEffect, useState } from 'react'; import { useAppDispatch, useAppSelector } from '../../../../../store'; import { IPermission } from '../../../../../types/permissions'; +import { ResizeContext } from '../../../../services/context/resize-context/ResizeContext'; import { ValidationContext } from '../../../../services/context/validation-context/ValidationContext'; import { usePopups } from '../../../../services/hooks'; import { fetchAllPrincipalGrants } from '../../../../services/slices/permission-grants.slice'; @@ -13,13 +19,13 @@ import { ScopesError } from '../../../../utils/error-utils/ScopesError'; import { translateMessage } from '../../../../utils/translate-messages'; import { convertVhToPx } from '../../../common/dimensions/dimensions-adjustment'; import { getColumns } from './columnsV9'; +import permissionStyles from './Permission.stylesV9'; import { setConsentedStatus, sortPermissionsWithPrivilege } from './util'; -import permissionStyles from './Permission.stylesV9'; - export const Permissions = (): JSX.Element => { const dispatch = useAppDispatch(); const validation = useContext(ValidationContext); + const resizeValues = useContext(ResizeContext); const sampleQuery = useAppSelector((state) => state.sampleQuery); const scopes = useAppSelector((state) => state.scopes); const authToken = useAppSelector((state) => state.auth.authToken); @@ -29,8 +35,12 @@ export const Permissions = (): JSX.Element => { const tokenPresent = !!authToken.token; const { pending: loading, error } = scopes; - const [permissions, setPermissions] = useState<{ item: IPermission; index: number }[]>([]); - const [permissionsError, setPermissionsError] = useState(error); + const [permissions, setPermissions] = useState< + { item: IPermission; index: number }[] + >([]); + const [permissionsError, setPermissionsError] = useState( + error + ); const styles = permissionStyles(); const tabHeight = convertVhToPx(dimensions.request.height, 110); @@ -76,18 +86,30 @@ export const Permissions = (): JSX.Element => { useEffect(() => { let updatedPermissions = scopes.data.specificPermissions || []; updatedPermissions = sortPermissionsWithPrivilege(updatedPermissions); - updatedPermissions = setConsentedStatus(tokenPresent, updatedPermissions, consentedScopes); + updatedPermissions = setConsentedStatus( + tokenPresent, + updatedPermissions, + consentedScopes + ); setPermissions(updatedPermissions.map((item, index) => ({ item, index }))); }, [scopes.data.specificPermissions, tokenPresent, consentedScopes]); const columns = getColumns({ source: 'tab', tokenPresent }); if (loading.isSpecificPermissions) { - return
{translateMessage('Fetching permissions')}...
; + return ( +
+ {translateMessage('Fetching permissions')}... +
+ ); } if (!validation.isValid) { - return
{translateMessage('Invalid URL')}!
; + return ( +
+ {translateMessage('Invalid URL')}! +
+ ); } const displayNoPermissionsFoundMessage = (): JSX.Element => ( @@ -104,12 +126,16 @@ export const Permissions = (): JSX.Element => { const displayNotSignedInMessage = (): JSX.Element => (
- {translateMessage('sign in to view a list of all permissions')} + + {translateMessage('sign in to view a list of all permissions')} +
); const displayErrorFetchingPermissionsMessage = (): JSX.Element => ( -
{translateMessage('Fetching permissions failing')}
+
+ {translateMessage('Fetching permissions failing')} +
); if (!tokenPresent && permissions.length === 0) { @@ -117,24 +143,38 @@ export const Permissions = (): JSX.Element => { } if (permissions.length === 0) { - return permissionsError?.status && (permissionsError?.status === 404 || permissionsError?.status === 400) + return permissionsError?.status && + (permissionsError?.status === 404 || permissionsError?.status === 400) ? displayNoPermissionsFoundMessage() : displayErrorFetchingPermissionsMessage(); } + const calcTableHeight = + resizeValues.parentHeight - resizeValues.dragValue - 124; + const tableHeight = Math.max( + 0, + Math.max(calcTableHeight, parseInt(tabHeight, 10)) + ); + return (
- {translateMessage(tokenPresent ? 'permissions required to run the query':'sign in to consent to permissions')} + {translateMessage( + tokenPresent + ? 'permissions required to run the query' + : 'sign in to consent to permissions' + )}
-
- +
+
{columns.map((column) => ( - {column.renderHeaderCell()} + + {column.renderHeaderCell()} + ))} @@ -142,7 +182,9 @@ export const Permissions = (): JSX.Element => { {permissions.map(({ item, index }) => ( {columns.map((column) => ( - {column.renderCell({ item, index })} + + {column.renderCell({ item, index })} + ))} ))} @@ -151,4 +193,4 @@ export const Permissions = (): JSX.Element => { ); -}; \ No newline at end of file +}; diff --git a/src/app/views/query-runner/request/permissions/columnsV9.tsx b/src/app/views/query-runner/request/permissions/columnsV9.tsx index e4c867aad3..69d467cef4 100644 --- a/src/app/views/query-runner/request/permissions/columnsV9.tsx +++ b/src/app/views/query-runner/request/permissions/columnsV9.tsx @@ -45,6 +45,7 @@ const createRenderColumnHeader = (styles: ReturnType) = return (