From b728fc4c09cf580fc0b419131746ff7199dabef7 Mon Sep 17 00:00:00 2001 From: Elinor Date: Wed, 18 May 2022 13:21:23 +0300 Subject: [PATCH] Fix: Refactor sample queries (#1677) --- .../sample-queries/SampleQueries.spec.tsx | 3 - .../sidebar/sample-queries/SampleQueries.tsx | 472 +++++++----------- .../sample-queries/sample-query-utils.ts | 73 +++ src/types/query-runner.ts | 3 - 4 files changed, 249 insertions(+), 302 deletions(-) diff --git a/src/app/views/sidebar/sample-queries/SampleQueries.spec.tsx b/src/app/views/sidebar/sample-queries/SampleQueries.spec.tsx index 4db44a313..d1ea486d9 100644 --- a/src/app/views/sidebar/sample-queries/SampleQueries.spec.tsx +++ b/src/app/views/sidebar/sample-queries/SampleQueries.spec.tsx @@ -37,9 +37,6 @@ const renderSampleQueries = () => { error: { message: '' } - }, - intl: { - message: messages } } diff --git a/src/app/views/sidebar/sample-queries/SampleQueries.tsx b/src/app/views/sidebar/sample-queries/SampleQueries.tsx index 587e7c18d..a8a4306e3 100644 --- a/src/app/views/sidebar/sample-queries/SampleQueries.tsx +++ b/src/app/views/sidebar/sample-queries/SampleQueries.tsx @@ -4,105 +4,134 @@ import { GroupHeader, IColumn, Icon, IDetailsRowStyles, MessageBar, MessageBarType, SearchBox, SelectionMode, Spinner, SpinnerSize, styled, TooltipHost } from '@fluentui/react'; -import React, { Component } from 'react'; -import { FormattedMessage, injectIntl } from 'react-intl'; -import { connect } from 'react-redux'; -import { bindActionCreators, Dispatch } from 'redux'; +import React, { useEffect, useState } from 'react'; +import { FormattedMessage } from 'react-intl'; +import { useDispatch, useSelector } from 'react-redux'; import { geLocale } from '../../../../appLocale'; -import { componentNames, eventTypes, telemetry } from '../../../../telemetry'; +import { componentNames, telemetry } from '../../../../telemetry'; import { IQuery, ISampleQueriesProps, ISampleQuery } from '../../../../types/query-runner'; import { IRootState } from '../../../../types/root'; -import * as queryActionCreators from '../../../services/actions/query-action-creators'; -import * as queryInputActionCreators from '../../../services/actions/query-input-action-creators'; -import * as queryStatusActionCreators from '../../../services/actions/query-status-action-creator'; -import * as samplesActionCreators from '../../../services/actions/samples-action-creators'; import { GRAPH_URL } from '../../../services/graph-constants'; import { getStyleFor } from '../../../utils/http-methods.utils'; -import { validateExternalLink } from '../../../utils/external-link-validation'; import { generateGroupsFromList } from '../../../utils/generate-groups'; -import { sanitizeQueryUrl } from '../../../utils/query-url-sanitization'; import { substituteTokens } from '../../../utils/token-helpers'; import { classNames } from '../../classnames'; +import { + columns, isJsonString, performSearch, trackDocumentLinkClickedEvent, + trackSampleQueryClickEvent +} from './sample-query-utils'; import { sidebarStyles } from '../Sidebar.styles'; -import { isJsonString } from './sample-query-utils'; import { searchBoxStyles } from '../../../utils/searchbox.styles'; +import { fetchSamples } from '../../../services/actions/samples-action-creators'; +import { setQueryResponseStatus } from '../../../services/actions/query-status-action-creator'; +import { runQuery } from '../../../services/actions/query-action-creators'; +import { setSampleQuery } from '../../../services/actions/query-input-action-creators'; +import { translateMessage } from '../../../utils/translate-messages'; + +const unstyledSampleQueries = (sampleProps?: ISampleQueriesProps): JSX.Element => { + + const [selectedQuery, setSelectedQuery] = useState(null) + const { authToken, profile, samples } = + useSelector((state: IRootState) => state); + const tokenPresent = authToken.token; + const [sampleQueries, setSampleQueries] = useState(samples.queries); + const dispatch = useDispatch(); + const currentTheme = getTheme(); + + const { error, pending } = samples; + const groups = generateGroupsFromList(sampleQueries, 'category'); + + const classProps = { + styles: sampleProps!.styles, + theme: sampleProps!.theme + }; + const classes = classNames(classProps); -export class SampleQueries extends Component { - constructor(props: ISampleQueriesProps) { - super(props); - this.state = { - sampleQueries: [], - selectedQuery: null - }; - } - - public componentDidMount = () => { - const { queries } = this.props.samples; - if (queries && queries.length > 0) { - this.setState({ sampleQueries: queries }); + useEffect(() => { + if (samples.queries.length === 0) { + dispatch(fetchSamples()); } else { - this.props.actions!.fetchSamples(); + setSampleQueries(samples.queries) } + }, [samples.queries, tokenPresent]) + + const searchValueChanged = (_event: any, value?: string): void => { + const { queries } = samples; + const filteredQueries = value ? performSearch(queries, value) : queries; + setSampleQueries(filteredQueries); }; - public componentDidUpdate = (prevProps: ISampleQueriesProps) => { - if (prevProps.samples.queries !== this.props.samples.queries) { - this.setState({ sampleQueries: this.props.samples.queries }); - } + const onDocumentationLinkClicked = (item: ISampleQuery) => { + window.open(item.docLink, '_blank'); + trackDocumentLinkClickedEvent(item); }; - public searchValueChanged = (event: any, value?: string): void => { - const { queries } = this.props.samples; - let sampleQueries = queries; - if (value) { - const keyword = value.toLowerCase(); - sampleQueries = queries.filter((sample: any) => { - const name = sample.humanName.toLowerCase(); - const category = sample.category.toLowerCase(); - return name.includes(keyword) || category.includes(keyword); - }); + const querySelected = (query: ISampleQuery) => { + const queryVersion = query.requestUrl.substring(1, 5); + const sampleQuery: IQuery = { + sampleUrl: GRAPH_URL + query.requestUrl, + selectedVerb: query.method, + sampleBody: query.postBody, + sampleHeaders: query.headers || [], + selectedVersion: queryVersion + }; + substituteTokens(sampleQuery, profile!); + sampleQuery.sampleBody = getSampleBody(sampleQuery); + + if (query.tip) { + displayTipMessage(query); } - this.setState({ sampleQueries }); - }; - public onDocumentationLinkClicked = (item: ISampleQuery) => { - window.open(item.docLink, '_blank'); - this.trackDocumentLinkClickedEvent(item); + if (shouldRunQuery(query)) { + dispatch(runQuery(sampleQuery)); + } + + trackSampleQueryClickEvent(query); + dispatch(setSampleQuery(sampleQuery)); }; - private async trackDocumentLinkClickedEvent(item: ISampleQuery): Promise { - const properties: { [key: string]: any } = { - ComponentName: componentNames.DOCUMENTATION_LINK, - SampleId: item.id, - SampleName: item.humanName, - SampleCategory: item.category, - Link: item.docLink - }; - telemetry.trackEvent(eventTypes.LINK_CLICK_EVENT, properties); + const getSampleBody = (query: IQuery) => { + return query.sampleBody ? parseSampleBody() : undefined; + + function parseSampleBody() { + return isJsonString(query.sampleBody!) + ? JSON.parse(query.sampleBody!) + : query.sampleBody; + } + } + + const displayTipMessage = (query: ISampleQuery) => { + dispatch(setQueryResponseStatus({ + messageType: MessageBarType.warning, + statusText: 'Tip', + status: query.tip + })); + } - // Check if link throws error - validateExternalLink(item.docLink || '', componentNames.DOCUMENTATION_LINK, item.id); + const shouldRunQuery = (query: ISampleQuery) => { + if (query.tip && tokenPresent) { + return false; + } + if (!tokenPresent || query.method === 'GET') { + return true; + } + return false; } - public renderItemColumn = ( + + const renderItemColumn = ( item: ISampleQuery, index: number | undefined, column: IColumn | undefined ) => { - const classes = classNames(this.props); - const { - tokenPresent, - intl: { messages } - }: any = this.props; - if (column) { const queryContent = item[column.fieldName as keyof ISampleQuery] as string; - const signInText = messages['Sign In to try this sample']; + const signInText = translateMessage('Sign In to try this sample'); switch (column.key) { case 'authRequiredIcon': @@ -154,7 +183,7 @@ export class SampleQueries extends Component { > this.onDocumentationLinkClicked(item)} + onClick={() => onDocumentationLinkClicked(item)} className={classes.docLink} style={{ marginRight: '45%', @@ -208,15 +237,12 @@ export class SampleQueries extends Component { ); } } - }; + } - public renderRow = (props: any): any => { - const currentTheme = getTheme(); - const { tokenPresent } = this.props; - const classes = classNames(this.props); + const renderRow = (props: any): any => { let selectionDisabled = false; const customStyles: Partial = {}; - if (this.state.selectedQuery?.id === props.item.id) { + if (selectedQuery?.id === props.item.id) { customStyles.root = { backgroundColor: currentTheme.palette.neutralLight }; } @@ -231,9 +257,10 @@ export class SampleQueries extends Component { styles={customStyles} onClick={() => { if (!selectionDisabled) { - this.querySelected(props.item); + const query: ISampleQuery = props.item!; + querySelected(query); } - this.setState({ selectedQuery: props.item }) + setSelectedQuery(props.item) }} className={ classes.queryRow + @@ -248,68 +275,7 @@ export class SampleQueries extends Component { } }; - private querySelected = (query: any) => { - const { actions, tokenPresent, profile } = this.props; - const selectedQuery = query; - if (!selectedQuery) { - return; - } - - const queryVersion = selectedQuery.requestUrl.substring(1, 5); - const sampleQuery: IQuery = { - sampleUrl: GRAPH_URL + selectedQuery.requestUrl, - selectedVerb: selectedQuery.method, - sampleBody: selectedQuery.postBody, - sampleHeaders: selectedQuery.headers || [], - selectedVersion: queryVersion - }; - - substituteTokens(sampleQuery, profile); - - if (actions) { - if (sampleQuery.selectedVerb === 'GET') { - sampleQuery.sampleBody = JSON.parse('{}'); - if (tokenPresent) { - if (selectedQuery.tip) { - displayTipMessage(actions, selectedQuery); - } else { - actions.runQuery(sampleQuery); - } - } else { - actions.runQuery(sampleQuery); - } - this.trackSampleQueryClickEvent(selectedQuery); - } else { - if (sampleQuery.sampleBody) { - sampleQuery.sampleBody = isJsonString(sampleQuery.sampleBody) - ? JSON.parse(sampleQuery.sampleBody) - : sampleQuery.sampleBody; - } else { - sampleQuery.sampleBody = undefined; - } - - if (selectedQuery.tip) { - displayTipMessage(actions, selectedQuery); - } - } - actions.setSampleQuery(sampleQuery); - } - }; - - private trackSampleQueryClickEvent(selectedQuery: ISampleQuery) { - const sanitizedUrl = sanitizeQueryUrl(GRAPH_URL + selectedQuery.requestUrl); - telemetry.trackEvent( - eventTypes.LISTITEM_CLICK_EVENT, - { - ComponentName: componentNames.SAMPLE_QUERY_LIST_ITEM, - SampleId: selectedQuery.id, - SampleName: selectedQuery.humanName, - SampleCategory: selectedQuery.category, - QuerySignature: `${selectedQuery.method} ${sanitizedUrl}` - }); - } - - public renderGroupHeader = (props: any): any => { + const renderGroupHeader = (props: any): any => { const onToggleSelectGroup = () => { props.onToggleCollapse(props.group); }; @@ -333,180 +299,94 @@ export class SampleQueries extends Component { ); }; - private renderDetailsHeader() { + const renderDetailsHeader = () => { return
; } - public render() { - const { error, pending } = this.props.samples; - const { - intl: { messages } - }: any = this.props; - - const { sampleQueries } = this.state; - const classes = classNames(this.props); - const groups = generateGroupsFromList(sampleQueries, 'category'); - if (this.state.selectedQuery) { - const index = groups.findIndex(k => k.key === this.state.selectedQuery.category); - if (index !== -1) { - groups[index].isCollapsed = false; - } - } - - if (pending) { - return ( - - ); - } - - let maxWidthOfHumanName = 180; - if (window.innerWidth > 1280) { - maxWidthOfHumanName = 200; + if (selectedQuery) { + const index = groups.findIndex(k => k.key === selectedQuery.category); + if (index !== -1) { + groups[index].isCollapsed = false; } + } - window.onresize = () => { - if (window.innerWidth > 1280) { - maxWidthOfHumanName = 200; - } - }; - - const columns: IColumn[] = [ - { - key: 'button', - name: '', - fieldName: 'button', - minWidth: 15, - maxWidth: 25 - }, - { - key: 'authRequiredIcon', - name: '', - fieldName: 'authRequiredIcon', - minWidth: 20, - maxWidth: 20 - }, - { - key: 'method', - name: '', - fieldName: 'method', - minWidth: 20, - maxWidth: 50 - }, - { - key: 'humanName', - name: '', - fieldName: 'humanName', - minWidth: 100, - maxWidth: maxWidthOfHumanName - } - ]; - + if (pending) { return ( -
- -
- {error && ( - - - - )} + + ); + } + + return ( + + ); } -function displayTipMessage(actions: any, selectedQuery: ISampleQuery) { - actions.setQueryResponseStatus({ - messageType: MessageBarType.warning, - statusText: 'Tip', - status: selectedQuery.tip - }); -} - -function mapStateToProps({ authToken, profile, samples, theme }: IRootState) { - return { - tokenPresent: !!authToken.token, - profile, - samples, - appTheme: theme - }; -} - -function mapDispatchToProps(dispatch: Dispatch): object { - return { - actions: bindActionCreators( - { - ...queryActionCreators, - ...queryInputActionCreators, - ...samplesActionCreators, - ...queryStatusActionCreators - }, - dispatch - ) - }; -} - -// @ts-ignore -const styledSampleQueries = styled(SampleQueries, sidebarStyles); -// @ts-ignore -const IntlSampleQueries = injectIntl(styledSampleQueries); // @ts-ignore -export default connect(mapStateToProps, mapDispatchToProps)(IntlSampleQueries); +const SampleQueries = styled(unstyledSampleQueries, sidebarStyles); +export default SampleQueries; diff --git a/src/app/views/sidebar/sample-queries/sample-query-utils.ts b/src/app/views/sidebar/sample-queries/sample-query-utils.ts index cf5c515c4..65669fd14 100644 --- a/src/app/views/sidebar/sample-queries/sample-query-utils.ts +++ b/src/app/views/sidebar/sample-queries/sample-query-utils.ts @@ -1,3 +1,40 @@ +import { IColumn } from '@fluentui/react'; +import { telemetry, eventTypes, componentNames } from '../../../../telemetry'; +import { ISampleQuery } from '../../../../types/query-runner'; +import { GRAPH_URL } from '../../../services/graph-constants'; +import { validateExternalLink } from '../../../utils/external-link-validation'; +import { sanitizeQueryUrl } from '../../../utils/query-url-sanitization'; + +export const columns: IColumn[] = [ + { + key: 'button', + name: '', + fieldName: 'button', + minWidth: 15, + maxWidth: 25 + }, + { + key: 'authRequiredIcon', + name: '', + fieldName: 'authRequiredIcon', + minWidth: 20, + maxWidth: 20 + }, + { + key: 'method', + name: '', + fieldName: 'method', + minWidth: 20, + maxWidth: 50 + }, + { + key: 'humanName', + name: '', + fieldName: 'humanName', + minWidth: 100, + maxWidth: 200 + } +]; export function isJsonString(str: string): boolean { try { JSON.parse(str); @@ -6,3 +43,39 @@ export function isJsonString(str: string): boolean { return false; } } + +export function performSearch(queries: ISampleQuery[], value: string): ISampleQuery[] { + const keyword = value.toLowerCase(); + return queries.filter((sample: any) => { + const name = sample.humanName.toLowerCase(); + const category = sample.category.toLowerCase(); + return name.includes(keyword) || category.includes(keyword); + }); +} + +export const trackSampleQueryClickEvent = (query: ISampleQuery) => { + const sanitizedUrl = sanitizeQueryUrl(GRAPH_URL + query.requestUrl); + telemetry.trackEvent( + eventTypes.LISTITEM_CLICK_EVENT, + { + ComponentName: componentNames.SAMPLE_QUERY_LIST_ITEM, + SampleId: query.id, + SampleName: query.humanName, + SampleCategory: query.category, + QuerySignature: `${query.method} ${sanitizedUrl}` + }); +} + +export const trackDocumentLinkClickedEvent = async (item: ISampleQuery): Promise => { + const properties: { [key: string]: any } = { + ComponentName: componentNames.DOCUMENTATION_LINK, + SampleId: item.id, + SampleName: item.humanName, + SampleCategory: item.category, + Link: item.docLink + }; + telemetry.trackEvent(eventTypes.LINK_CLICK_EVENT, properties); + + // Check if link throws error + validateExternalLink(item.docLink || '', componentNames.DOCUMENTATION_LINK, item.id); +} diff --git a/src/types/query-runner.ts b/src/types/query-runner.ts index 08a7291b3..4c3304586 100644 --- a/src/types/query-runner.ts +++ b/src/types/query-runner.ts @@ -100,9 +100,6 @@ export interface ISampleQueriesProps { fetchSamples: Function; setQueryResponseStatus: Function; }; - intl: { - message: object; - }; } export const httpMethods: IDropdownOption[] = [