Skip to content

Commit

Permalink
Fix: Autocomplete validation (#2811)
Browse files Browse the repository at this point in the history
  • Loading branch information
thewahome authored Nov 2, 2023
1 parent 06b5b32 commit 656e10a
Show file tree
Hide file tree
Showing 21 changed files with 479 additions and 190 deletions.
12 changes: 12 additions & 0 deletions src/app/services/context/validation-context/ValidationContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { createContext } from 'react';

interface ValidationContext {
isValid: boolean;
validate: (queryUrl: string) => void;
query: string;
error: string;
}

export const ValidationContext = createContext<ValidationContext>(
{} as ValidationContext
);
66 changes: 66 additions & 0 deletions src/app/services/context/validation-context/ValidationProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { ReactNode, useEffect, useMemo, useState } from 'react';

import { ValidationService } from '../../../../modules/validation/validation-service';
import { useAppSelector } from '../../../../store';
import { IResource } from '../../../../types/resources';
import { ValidationError } from '../../../utils/error-utils/ValidationError';
import { getResourcesSupportedByVersion } from '../../../utils/resources/resources-filter';
import { parseSampleUrl } from '../../../utils/sample-url-generation';
import { GRAPH_API_VERSIONS } from '../../graph-constants';
import { ValidationContext } from './ValidationContext';

interface ValidationProviderProps {
children: ReactNode;
}

export const ValidationProvider = ({ children }: ValidationProviderProps) => {
const { resources } = useAppSelector((state) => state);
const base = getResourcesSupportedByVersion(resources.data.children, GRAPH_API_VERSIONS[0]);

const [isValid, setIsValid] = useState<boolean>(false);
const [query, setQuery] = useState<string>('');
const [validationError, setValidationError] = useState<string>('');

const [versionedResources, setVersionedResources] =
useState<IResource[]>(resources.data.children.length > 0 ? base : []);
const [version, setVersion] = useState<string>(GRAPH_API_VERSIONS[0]);

const { queryVersion } = parseSampleUrl(query);

useEffect(() => {
if (resources.data.children.length > 0) {
setVersionedResources(getResourcesSupportedByVersion(resources.data.children, GRAPH_API_VERSIONS[0]));
}
}, [resources])

useEffect(() => {
if (version !== queryVersion && GRAPH_API_VERSIONS.includes(queryVersion) && resources.data.children.length > 0) {
setVersionedResources(getResourcesSupportedByVersion(resources.data.children, queryVersion));
setVersion(queryVersion);
}
}, [query]);

const validate = (queryToValidate: string) => {
setQuery(queryToValidate);
try {
ValidationService.validate(queryToValidate, versionedResources);
setIsValid(true);
setValidationError('');
} catch (error: unknown) {
const theError = error as ValidationError;
setValidationError(theError.message);
setIsValid(theError.type === 'warning');
}
};

const contextValue = useMemo(() => {
return { isValid, validate, query, error: validationError };
}, [isValid, validate, query, validationError]);

return (
<ValidationContext.Provider value={contextValue}>
{children}
</ValidationContext.Provider>
);

};
14 changes: 14 additions & 0 deletions src/app/utils/error-utils/ValidationError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
type ErrorType = 'warning' | 'error';

class ValidationError extends Error {
type: ErrorType;

constructor(message: string, type: ErrorType, name: string = 'ValidationError') {
super(message);
this.name = name;
this.type = type;
this.message = message;
}
}

export { ValidationError };
5 changes: 5 additions & 0 deletions src/app/utils/sample-url-generation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,8 @@ export function hasPlaceHolders(url: string): boolean {
return placeHolderChars.length > 1 && placeHolderChars.every((char) => url.includes(char));
}

export function isValidHostname(hostname: string): boolean {
const regex = /^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)?graph\.microsoft\.com$/;
return regex.test(hostname);
}

24 changes: 13 additions & 11 deletions src/app/views/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,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 { ValidationProvider } from '../services/context/validation-context/ValidationProvider';
export interface IAppProps {
theme?: ITheme;
styles?: object;
Expand Down Expand Up @@ -465,18 +466,19 @@ class App extends Component<IAppProps, IAppState> {
display: 'flex', flexDirection: 'column', alignItems: 'stretch', flex: 1
}}
>
<div style={{ marginBottom: 2 }} >
<QueryRunner onSelectVerb={this.handleSelectVerb} />
</div>

<div style={{
display: 'flex', flexDirection: 'column', alignItems: 'stretch', flex: 1
}}>
<div style={mobileScreen ? this.statusAreaMobileStyle : this.statusAreaFullScreenStyle}>
<StatusMessages />
<ValidationProvider>
<div style={{ marginBottom: 2 }} >
<QueryRunner onSelectVerb={this.handleSelectVerb} />
</div>
<div style={{
display: 'flex', flexDirection: 'column', alignItems: 'stretch', flex: 1
}}>
<div style={mobileScreen ? this.statusAreaMobileStyle : this.statusAreaFullScreenStyle}>
<StatusMessages />
</div>
<QueryResponse verb={this.state.selectedVerb} />
</div>
<QueryResponse verb={this.state.selectedVerb} />
</div>
</ValidationProvider>
</Resizable>
)}
</div>
Expand Down
19 changes: 14 additions & 5 deletions src/app/views/query-response/snippets/Snippets.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import { FontSizes, Pivot, PivotItem } from '@fluentui/react';
import { FontSizes, Label, Pivot, PivotItem } from '@fluentui/react';
import { useDispatch } from 'react-redux';
import { useContext } from 'react';
import { FormattedMessage } from 'react-intl';

import { AppDispatch, useAppSelector } from '../../../../store';
import { componentNames, telemetry } from '../../../../telemetry';
import { setSnippetTabSuccess } from '../../../services/actions/snippet-action-creator';
import { renderSnippets } from './snippets-helper';
import { ValidationContext } from '../../../services/context/validation-context/ValidationContext';
import { translateMessage } from '../../../utils/translate-messages';
import { renderSnippets } from './snippets-helper';

function GetSnippets() {
const dispatch: AppDispatch = useDispatch();
const validation = useContext(ValidationContext);

const { snippets, sampleQuery } = useAppSelector((state) => state);
const supportedLanguages = {
'CSharp': {
Expand Down Expand Up @@ -52,7 +58,7 @@ function GetSnippets() {
dispatch(setSnippetTabSuccess(pivotItem.props.itemKey!));
}

return <Pivot
return validation.isValid ? <Pivot
className={'unstyled-pivot'}
selectedKey={snippets.snippetTab}
onLinkClick={handlePivotItemClick}
Expand All @@ -61,8 +67,11 @@ function GetSnippets() {
overflowAriaLabel={translateMessage('More items')}
>
{renderSnippets(supportedLanguages)}
</Pivot>;
}
</Pivot> : <Label style={{ marginLeft: '12px' }}>
<FormattedMessage id={'Invalid URL'} />!
</Label>
};

const Snippets = telemetry.trackReactComponent(
GetSnippets,
componentNames.CODE_SNIPPETS_TAB
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ITheme } from '@fluentui/react';

export const queryInputStyles = (theme: ITheme) => {
const controlWidth = '96.5%';
const controlWidth = '94%';
return {
autoComplete: {
input: {
Expand Down
7 changes: 5 additions & 2 deletions src/app/views/query-runner/query-input/QueryInput.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { Dropdown, IDropdownOption, IStackTokens, Stack } from '@fluentui/react';
import { useContext } from 'react';
import { injectIntl } from 'react-intl';
import { useDispatch } from 'react-redux';

import { AppDispatch, useAppSelector } from '../../../../store';
import { IQuery, IQueryInputProps, httpMethods } from '../../../../types/query-runner';
import { setSampleQuery } from '../../../services/actions/query-input-action-creators';
import { ValidationContext } from '../../../services/context/validation-context/ValidationContext';
import { GRAPH_API_VERSIONS } from '../../../services/graph-constants';
import { getStyleFor } from '../../../utils/http-methods.utils';
import { parseSampleUrl } from '../../../utils/sample-url-generation';
Expand All @@ -23,6 +25,7 @@ const QueryInput = (props: IQueryInputProps) => {
} = props;

const dispatch: AppDispatch = useDispatch();
const validation = useContext(ValidationContext);

const urlVersions: IDropdownOption[] = [];
GRAPH_API_VERSIONS.forEach(version => {
Expand Down Expand Up @@ -69,7 +72,7 @@ const QueryInput = (props: IQueryInputProps) => {
if (queryUrl) {
query = getChangedQueryContent(queryUrl);
}
if (!query.sampleUrl || query.sampleUrl.indexOf('graph.microsoft.com') === -1) {
if (!validation.isValid) {
return;
}
handleOnRunQuery(query);
Expand Down Expand Up @@ -111,7 +114,7 @@ const QueryInput = (props: IQueryInputProps) => {
<SubmitButton
className='run-query-button'
text={translateMessage('Run Query')}
disabled={showError || !sampleQuery.sampleUrl}
disabled={showError || !sampleQuery.sampleUrl || !validation.isValid}
role='button'
handleOnClick={() => runQuery()}
submitting={submitting}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,70 @@
import { ITheme } from '@fluentui/react';
import { IStyle, ITheme } from '@fluentui/react';

export const autoCompleteStyles = (theme: ITheme) => {
const controlWidth = '95.5%';

const suggestions: IStyle = {
maxHeight: '250px',
overflow: 'auto',
paddingLeft: 0,
position: 'absolute',
backgroundColor: theme.palette.neutralLighter,
minWidth: '40%',
maxWidth: '50%',
zIndex: 1,
cursor: 'pointer',
color: theme.palette.black
};
const suggestionOption: IStyle = {
display: 'block',
selectors: {
':hover': {
background: theme.palette.neutralLight
}
},
cursor: 'pointer',
backgroundColor: theme.palette.white,
boxShadow: 'none',
margin: '0px 0px 0px 0px',
padding: '10px 32px 12px 10px',
boxSizing: 'border-box',
height: '32px',
lineHeight: '30px',
position: 'relative',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
borderWidth: '1px',
borderStyle: 'solid',
borderColor: theme.palette.neutralLight,
overflow: 'hidden'
};
const suggestionActive: IStyle = {
display: 'block',
cursor: 'pointer',
boxShadow: 'none',
margin: '0px 0px 0px 0px',
padding: '10px 32px 12px 10px',
boxSizing: 'border-box',
height: '32px',
lineHeight: '30px',
position: 'relative',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
borderWidth: '1px',
borderStyle: 'solid',
overflow: 'hidden',
wordWrap: 'normal',
backgroundColor: theme.palette.neutralLight
};
const suggestionTitle: IStyle = {
display: 'flex',
height: '100%',
flexWrap: 'nowrap',
justifyContent: 'flex-start',
alignItems: 'center',
fontWeight: 400
};

return {
input: {
minWidth: controlWidth,
Expand All @@ -12,66 +74,9 @@ export const autoCompleteStyles = (theme: ITheme) => {
color: theme.palette.black,
padding: 10
},
suggestions: {
maxHeight: '250px',
overflow: 'auto',
paddingLeft: 0,
position: 'absolute',
backgroundColor: theme.palette.neutralLighter,
minWidth: '40%',
maxWidth: '50%',
zIndex: 1,
cursor: 'pointer',
color: theme.palette.black
},
suggestionOption: {
display: 'block',
selectors: {
':hover': {
background: theme.palette.neutralLight
}
},
cursor: 'pointer',
backgroundColor: theme.palette.white,
boxShadow: 'none',
margin: '0px 0px 0px 0px',
padding: '10px 32px 12px 10px',
boxSizing: 'border-box',
height: '32px',
lineHeight: '30px',
position: 'relative',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
borderWidth: '1px',
borderStyle: 'solid',
borderColor: theme.palette.neutralLight,
overflow: 'hidden'
},
suggestionActive: {
display: 'block',
cursor: 'pointer',
boxShadow: 'none',
margin: '0px 0px 0px 0px',
padding: '10px 32px 12px 10px',
boxSizing: 'border-box',
height: '32px',
lineHeight: '30px',
position: 'relative',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
borderWidth: '1px',
borderStyle: 'solid',
overflow: 'hidden',
wordWrap: 'normal',
backgroundColor: theme.palette.neutralLight
},
suggestionTitle: {
display: 'flex',
height: '100%',
flexWrap: 'nowrap',
justifyContent: 'flex-start',
alignItems: 'center',
fontWeight: 400
}
suggestions,
suggestionOption,
suggestionActive,
suggestionTitle
};
};
Loading

0 comments on commit 656e10a

Please sign in to comment.