Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix: Autocomplete validation #2811

Merged
merged 24 commits into from
Nov 2, 2023
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
845625e
strongly type autocomplete components
thewahome Sep 21, 2023
e9b4eab
create validator utility
thewahome Sep 21, 2023
1a6474e
create validation service
thewahome Sep 27, 2023
1c6aaa3
create validation service
thewahome Oct 2, 2023
4e25baa
use validation validation context
thewahome Oct 2, 2023
da80bdb
add types to validation error definition
thewahome Oct 2, 2023
e94cd6c
use context to prevent running queries when errors exist
thewahome Oct 2, 2023
f680ae9
Fix: use resources to validate paths (#2812)
thewahome Oct 6, 2023
fc0aa45
Merge branch 'dev' into fix/autocomplete-validation
thewahome Oct 6, 2023
bfb0c5d
reduce code smells
thewahome Oct 6, 2023
086457e
useMemo to remove code smell
thewahome Oct 6, 2023
99f1cd3
use url constant
thewahome Oct 9, 2023
7dfa02b
Change query-input control width
thewahome Oct 9, 2023
ed0f2b6
Merge branch 'dev' into fix/autocomplete-validation
thewahome Oct 17, 2023
d26c4a5
properly validate against the protocol and hostname
thewahome Oct 19, 2023
acb303b
check the hostname against allowed parameters
thewahome Oct 19, 2023
b8e895b
add validation for missing versions
thewahome Oct 19, 2023
6eafbca
Merge branch 'dev' into fix/autocomplete-validation
thewahome Oct 31, 2023
d5b3843
validate hostname to be graph / subdomain
thewahome Oct 31, 2023
f206168
Merge branch 'fix/autocomplete-validation' of https://github.com/micr…
thewahome Oct 31, 2023
213300c
Merge branch 'dev' into fix/autocomplete-validation
thewahome Nov 1, 2023
40c911d
Merge branch 'dev' into fix/autocomplete-validation
thewahome Nov 1, 2023
659c115
Merge branch 'dev' into fix/autocomplete-validation
thewahome Nov 2, 2023
ae09762
remove duplicate imports
thewahome Nov 2, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
);
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