From a5f36bfd8e5641a9ca2f8a17ac5ea75370008c1c Mon Sep 17 00:00:00 2001 From: EvansA Date: Wed, 1 Dec 2021 11:53:12 +0300 Subject: [PATCH 1/4] Task: add status message for anonymous queries (#1278) * add status message based on status code * rename status text map variable * rename responseStatusTextMap to httpStatusMessage --- .../services/actions/query-action-creators.ts | 3 +- src/app/utils/status-message.ts | 49 +++++++++++++++++++ src/tests/utils/url-to-html.spec.ts | 8 ++- 3 files changed, 58 insertions(+), 2 deletions(-) diff --git a/src/app/services/actions/query-action-creators.ts b/src/app/services/actions/query-action-creators.ts index a52a8ee9a4..b73b724dca 100644 --- a/src/app/services/actions/query-action-creators.ts +++ b/src/app/services/actions/query-action-creators.ts @@ -4,6 +4,7 @@ import { ContentType } from '../../../types/enums'; import { IHistoryItem } from '../../../types/history'; import { IQuery } from '../../../types/query-runner'; import { IStatus } from '../../../types/status'; +import { setStatusMessage } from '../../utils/status-message'; import { writeHistoryData } from '../../views/sidebar/history/history-utils'; import { anonymousRequest, @@ -95,7 +96,7 @@ export function runQuery(query: IQuery): Function { if (response) { status.status = response.status; - status.statusText = response.statusText; + status.statusText = response.statusText === '' ? setStatusMessage(response.status) : response.statusText; } if (response && response.ok) { diff --git a/src/app/utils/status-message.ts b/src/app/utils/status-message.ts index 094d5380a1..77611d40fb 100644 --- a/src/app/utils/status-message.ts +++ b/src/app/utils/status-message.ts @@ -37,3 +37,52 @@ export function getMatchesAndParts(message: string) { const parts: string[] = message.split(numberPattern); return { matches, parts }; } + +const httpStatusMessage = new Map([ + [100, 'Continue'], + [101, 'Switching Protocols'], + [102, 'Processing'], + [200, 'OK'], + [201, 'Created'], + [202, 'Accepted'], + [203, 'Non Authoritative'], + [204, 'No Content'], + [205, 'Reset Content'], + [206, 'Partial Content'], + [300, 'Multiple Choices'], + [301, 'Moved Permanently'], + [302, 'Found'], + [303, 'See Other'], + [304, 'Not Modified'], + [305, 'Use Proxy'], + [307, 'Temporary Redirect'], + [400, 'Bad Request'], + [401, 'Unauthorized'], + [403, 'Forbidden'], + [404, 'Not Found'], + [405, 'Method Not Allowed'], + [406, 'Not Acceptable'], + [407, 'Proxy Authentication Required'], + [408, 'Request Timeout'], + [409, 'Conflict'], + [410, 'Gone'], + [411, 'Length Required'], + [412, 'Precondition Failed'], + [413, 'Request Entity Too Large'], + [414, 'Request-URI Too Long'], + [415, 'Unsupported Media Type'], + [416, 'Requested Range Not Satisfiable'], + [417, 'Expectation Failed'], + [422, 'Unprocessable Entity'], + [500, 'Internal Server Error'], + [501, 'Not Implemented'], + [502, 'Bad Gateway'], + [503, 'Service Unavailable'], + [504, 'Gateway Timeout'], + [505, 'HTTP Version Not Supported'] +]); + +export function setStatusMessage(status: number): string { + const statusMessage = httpStatusMessage.get(status); + return statusMessage ? statusMessage : ''; +} \ No newline at end of file diff --git a/src/tests/utils/url-to-html.spec.ts b/src/tests/utils/url-to-html.spec.ts index 219f794e35..b889589df8 100644 --- a/src/tests/utils/url-to-html.spec.ts +++ b/src/tests/utils/url-to-html.spec.ts @@ -1,5 +1,5 @@ /* eslint-disable max-len */ -import { extractUrl, replaceLinks, convertArrayToObject, getMatchesAndParts } from '../../app/utils/status-message'; +import { extractUrl, replaceLinks, convertArrayToObject, getMatchesAndParts, setStatusMessage } from '../../app/utils/status-message'; describe('status message should', () => { @@ -40,4 +40,10 @@ describe('status message should', () => { const { matches } = getMatchesAndParts(replaceLinks(message)); expect(matches?.includes('$0')).toBe(true); }); + + it('should return a status message given a status code', () => { + const statusCode: number = 200; + const statusMessage = setStatusMessage(statusCode); + expect(statusMessage).toBe('OK'); + }) }) From 5e62bd29b10adfb9890b223b4f11d76fdbc620f2 Mon Sep 17 00:00:00 2001 From: EvansA Date: Thu, 2 Dec 2021 11:17:35 +0300 Subject: [PATCH 2/4] fix failing coverage information collection (#1279) --- tsconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tsconfig.json b/tsconfig.json index a6111e3262..723e5f14d3 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -16,7 +16,7 @@ "moduleResolution": "node", "resolveJsonModule": true, "noEmit": true, - "jsx": "preserve" + "jsx": "react-jsx" }, "include": [ From 2ddf9c4a4d467d5de12d153d7d7e7e1addb4c057 Mon Sep 17 00:00:00 2001 From: Charles Wahome Date: Thu, 2 Dec 2021 15:15:55 +0300 Subject: [PATCH 3/4] Task: Export selected resources to postman collection (#1277) --- .../resource-explorer-action-creators.ts | 19 ++- .../services/reducers/resources-reducer.ts | 41 ++++- src/app/services/redux-constants.ts | 2 + src/app/utils/download.ts | 10 ++ src/app/views/sidebar/history/har-utils.ts | 10 +- .../resource-explorer/CommandOptions.tsx | 46 ++++++ .../resource-explorer/QueryParameters.tsx | 119 -------------- .../resource-explorer/ResourceExplorer.tsx | 48 ++++-- .../resource-explorer/ResourceLink.tsx | 22 ++- .../resource-explorer/panels/Paths.tsx | 81 ++++++++++ .../resource-explorer/panels/PathsReview.tsx | 120 ++++++++++++++ .../panels/QueryParameters.tsx | 151 ++++++++++++++++++ .../resource-explorer/panels/postman.util.ts | 49 ++++++ .../resource-explorer.utils.ts | 47 +++++- src/messages/GE.json | 6 +- .../resource-explorer-action-creators.spec.ts | 61 ++++++- .../reducers/resources-reducer.spec.ts | 58 ++++++- src/tests/utils/postman-util.spec.ts | 16 ++ .../utils/resource-payload-filter.spec.ts | 14 +- src/types/postman-collection.ts | 35 ++++ src/types/resources.ts | 27 +++- 21 files changed, 811 insertions(+), 171 deletions(-) create mode 100644 src/app/utils/download.ts create mode 100644 src/app/views/sidebar/resource-explorer/CommandOptions.tsx delete mode 100644 src/app/views/sidebar/resource-explorer/QueryParameters.tsx create mode 100644 src/app/views/sidebar/resource-explorer/panels/Paths.tsx create mode 100644 src/app/views/sidebar/resource-explorer/panels/PathsReview.tsx create mode 100644 src/app/views/sidebar/resource-explorer/panels/QueryParameters.tsx create mode 100644 src/app/views/sidebar/resource-explorer/panels/postman.util.ts create mode 100644 src/tests/utils/postman-util.spec.ts create mode 100644 src/types/postman-collection.ts diff --git a/src/app/services/actions/resource-explorer-action-creators.ts b/src/app/services/actions/resource-explorer-action-creators.ts index b0db446d54..0b24ebeeee 100644 --- a/src/app/services/actions/resource-explorer-action-creators.ts +++ b/src/app/services/actions/resource-explorer-action-creators.ts @@ -1,5 +1,8 @@ import { IAction } from '../../../types/action'; -import { FETCH_RESOURCES_SUCCESS, FETCH_RESOURCES_PENDING, FETCH_RESOURCES_ERROR } from '../redux-constants'; +import { + FETCH_RESOURCES_SUCCESS, FETCH_RESOURCES_PENDING, + FETCH_RESOURCES_ERROR, RESOURCEPATHS_ADD_SUCCESS, RESOURCEPATHS_DELETE_SUCCESS +} from '../redux-constants'; import { IResource } from '../../../types/resources'; import { IRootState } from '../../../types/root'; import { IRequestOptions } from '../../../types/request'; @@ -24,6 +27,20 @@ export function fetchResourcesError(response: object): IAction { }; } +export function addResourcePaths(response: object): IAction { + return { + type: RESOURCEPATHS_ADD_SUCCESS, + response + }; +} + +export function removeResourcePaths(response: object): IAction { + return { + type: RESOURCEPATHS_DELETE_SUCCESS, + response + }; +} + export function fetchResources(): Function { return async (dispatch: Function, getState: Function) => { try { diff --git a/src/app/services/reducers/resources-reducer.ts b/src/app/services/reducers/resources-reducer.ts index 00c5229938..33c7595eeb 100644 --- a/src/app/services/reducers/resources-reducer.ts +++ b/src/app/services/reducers/resources-reducer.ts @@ -1,7 +1,10 @@ import { IAction } from '../../../types/action'; -import { IResource, IResources } from '../../../types/resources'; +import { IResource, IResourceLink, IResources } from '../../../types/resources'; import content from '../../utils/resources/resources.json'; -import { FETCH_RESOURCES_ERROR, FETCH_RESOURCES_PENDING, FETCH_RESOURCES_SUCCESS } from '../redux-constants'; +import { + FETCH_RESOURCES_ERROR, FETCH_RESOURCES_PENDING, + FETCH_RESOURCES_SUCCESS, RESOURCEPATHS_ADD_SUCCESS, RESOURCEPATHS_DELETE_SUCCESS +} from '../redux-constants'; const res = JSON.parse(JSON.stringify(content)) as IResource; const initialState: IResources = { @@ -11,7 +14,8 @@ const initialState: IResources = { labels: [], segment: '' }, - error: null + error: null, + paths: [] }; export function resources(state: IResources = initialState, action: IAction): IResources { @@ -20,19 +24,44 @@ export function resources(state: IResources = initialState, action: IAction): IR return { pending: false, data: action.response, - error: null + error: null, + paths: [] }; case FETCH_RESOURCES_ERROR: return { pending: false, error: action.response, - data: res + data: res, + paths: [] }; case FETCH_RESOURCES_PENDING: return { pending: true, data: initialState.data, - error: null + error: null, + paths: [] + }; + case RESOURCEPATHS_ADD_SUCCESS: + const paths: IResourceLink[] = [...state.paths]; + action.response.forEach((element: any) => { + const exists = !!paths.find(k => k.key === element.key); + if (!exists) { + paths.push(element); + } + }); + return { + ...state, + paths + }; + case RESOURCEPATHS_DELETE_SUCCESS: + const list: IResourceLink[] = [...state.paths]; + action.response.forEach((path: IResourceLink) => { + const index = list.findIndex(k => k.key === path.key); + list.splice(index, 1); + }); + return { + ...state, + paths: list }; default: return state; diff --git a/src/app/services/redux-constants.ts b/src/app/services/redux-constants.ts index c9c2c1d69b..ab01f7e867 100644 --- a/src/app/services/redux-constants.ts +++ b/src/app/services/redux-constants.ts @@ -48,3 +48,5 @@ export const FETCH_RESOURCES_PENDING = 'FETCH_RESOURCES_PENDING'; export const GET_POLICY_SUCCESS = 'GET_POLICY_SUCCESS'; export const GET_POLICY_ERROR = 'GET_POLICY_ERROR'; export const GET_POLICY_PENDING = 'GET_POLICY_PENDING'; +export const RESOURCEPATHS_ADD_SUCCESS = 'RESOURCEPATHS_ADD_SUCCESS'; +export const RESOURCEPATHS_DELETE_SUCCESS = 'RESOURCEPATHS_DELETE_SUCCESS'; \ No newline at end of file diff --git a/src/app/utils/download.ts b/src/app/utils/download.ts new file mode 100644 index 0000000000..10cbd566fa --- /dev/null +++ b/src/app/utils/download.ts @@ -0,0 +1,10 @@ +export function downloadToLocal(content: any, filename: string) { + const blob = new Blob([JSON.stringify(content, null, 4)], { type: 'text/json' }); + + const elem = window.document.createElement('a'); + elem.href = window.URL.createObjectURL(blob); + elem.download = filename; + document.body.appendChild(elem); + elem.click(); + document.body.removeChild(elem); +} \ No newline at end of file diff --git a/src/app/views/sidebar/history/har-utils.ts b/src/app/views/sidebar/history/har-utils.ts index 927073b516..8146bfdfae 100644 --- a/src/app/views/sidebar/history/har-utils.ts +++ b/src/app/views/sidebar/history/har-utils.ts @@ -1,5 +1,6 @@ import { IHarFormat, IHarHeaders, IHarPayload } from '../../../../types/har'; import { IHistoryItem } from '../../../../types/history'; +import { downloadToLocal } from '../../../utils/download'; export function createHarPayload(query: IHistoryItem): IHarPayload { const queryResult = JSON.stringify(query.result); @@ -108,18 +109,11 @@ function createEntries(payloads: IHarPayload[]) { } export function exportQuery(content: IHarFormat, requestUrl: string) { - const blob = new Blob([JSON.stringify(content)], { type: 'text/json' }); - const url = requestUrl.substr(8).split('/'); url.pop(); const filename = `${url.join('_')}.har`; - const elem = window.document.createElement('a'); - elem.href = window.URL.createObjectURL(blob); - elem.download = filename; - document.body.appendChild(elem); - elem.click(); - document.body.removeChild(elem); + downloadToLocal(content, filename); } diff --git a/src/app/views/sidebar/resource-explorer/CommandOptions.tsx b/src/app/views/sidebar/resource-explorer/CommandOptions.tsx new file mode 100644 index 0000000000..a06cf83888 --- /dev/null +++ b/src/app/views/sidebar/resource-explorer/CommandOptions.tsx @@ -0,0 +1,46 @@ +import { CommandBar, ICommandBarItemProps } from '@fluentui/react'; +import React, { useState } from 'react'; + +import { translateMessage } from '../../../utils/translate-messages'; +import PathsReview from './panels/PathsReview'; + +interface ICommandOptions { + version: string; +} + +const CommandOptions = (props: ICommandOptions) => { + const [isOpen, setIsOpen] = useState(false); + const { version } = props; + const options: ICommandBarItemProps[] = [ + { + key: 'preview', + text: translateMessage('Preview collection'), + iconProps: { iconName: 'View' }, + onClick: () => toggleSelectedResourcesPreview() + } + ]; + + const toggleSelectedResourcesPreview = () => { + let open = isOpen; + open = !open; + setIsOpen(open); + } + + return ( +
+ + +
+ ) +} + +export default CommandOptions; diff --git a/src/app/views/sidebar/resource-explorer/QueryParameters.tsx b/src/app/views/sidebar/resource-explorer/QueryParameters.tsx deleted file mode 100644 index 17b5f2c440..0000000000 --- a/src/app/views/sidebar/resource-explorer/QueryParameters.tsx +++ /dev/null @@ -1,119 +0,0 @@ -import { - DetailsRow, GroupedList, IGroup, - INavLink, Label, Spinner, SpinnerSize -} from '@fluentui/react'; -import React, { useEffect } from 'react'; -import { FormattedMessage } from 'react-intl'; -import { useDispatch, useSelector } from 'react-redux'; - -import { IRootState } from '../../../../types/root'; -import { fetchAutoCompleteOptions } from '../../../services/actions/autocomplete-action-creators'; -import { translateMessage } from '../../../utils/translate-messages'; -import { getUrlFromLink } from './resource-explorer.utils'; - -interface IQueryParametersProps { - version: string; - context: INavLink; -} - -const QueryParameters = (props: IQueryParametersProps) => { - - const { autoComplete } = useSelector( - (state: IRootState) => state - ); - const { pending, data } = autoComplete; - const { context, version } = props; - - const dispatch = useDispatch(); - - const requestUrl = getUrlFromLink(context); - - useEffect(() => { - if (!data && !pending || (data?.url !== requestUrl)) { - dispatch(fetchAutoCompleteOptions(requestUrl, version)) - } - }, [requestUrl]); - - if (pending) { - return - } - - if (!data) { - return (
); - } - - const items: any[] = []; let groups: IGroup[] = []; - const columns = [{ - key: 'name', - name: 'name', - fieldName: 'name', - minWidth: 300 - }]; - - const flattenList = (parameter: any, name: any, parent: string) => { - parameter.items.forEach((item: string) => { - items.push({ - category: name, - key: `${parent}-${name}-${item}`, - name: item, - parent - }); - }); - } - - const generateChildren = (values: any[], parent: string) => { - const listItems: IGroup[] = []; - values.forEach((parameter: any, index: number) => { - const { name } = parameter; - listItems.push({ - key: name, - name, - startIndex: index, - count: parameter.items.length, - isCollapsed: true, - level: 0 - }); - flattenList(parameter, name, parent); - }); - return listItems; - } - - if (data) { - const { parameters } = data; - const odataParameters = parameters.find(k => k.verb.toLowerCase() === 'get'); - groups = generateChildren(odataParameters!.values, odataParameters!.verb); - } - - const onRenderCell = (depth?: number, item?: any, index?: number, group?: IGroup): React.ReactNode => { - return item && typeof index === 'number' && index > -1 ? ( - - ) : null; - }; - - return ( -
- - -
- ); -} - -export default QueryParameters; diff --git a/src/app/views/sidebar/resource-explorer/ResourceExplorer.tsx b/src/app/views/sidebar/resource-explorer/ResourceExplorer.tsx index f563d9cb03..6e42465e70 100644 --- a/src/app/views/sidebar/resource-explorer/ResourceExplorer.tsx +++ b/src/app/views/sidebar/resource-explorer/ResourceExplorer.tsx @@ -5,26 +5,30 @@ import { } from '@fluentui/react'; import React, { useEffect, useState } from 'react'; import { FormattedMessage } from 'react-intl'; -import { useSelector } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; -import { IResource } from '../../../../types/resources'; +import { IResource, IResourceLink, ResourceOptions } from '../../../../types/resources'; import { IRootState } from '../../../../types/root'; +import { addResourcePaths } from '../../../services/actions/resource-explorer-action-creators'; import { translateMessage } from '../../../utils/translate-messages'; import { classNames } from '../../classnames'; import { sidebarStyles } from '../Sidebar.styles'; -import QueryParameters from './QueryParameters'; +import CommandOptions from './CommandOptions'; +import QueryParameters from './panels/QueryParameters'; import { createList, getCurrentTree, + getResourcePaths, getResourcesSupportedByVersion, getUrlFromLink, removeCounter } from './resource-explorer.utils'; import ResourceLink from './ResourceLink'; const unstyledResourceExplorer = (props: any) => { + const dispatch = useDispatch(); const { resources } = useSelector( (state: IRootState) => state ); const classes = classNames(props); - const { data, pending } = resources; + const { data, pending, paths: selectedLinks } = resources; const versions: any[] = [ { key: 'v1.0', text: 'v1.0', iconProps: { iconName: 'CloudWeather' } }, @@ -71,6 +75,10 @@ const unstyledResourceExplorer = (props: any) => { return []; } + const addToCollection = (item: IResourceLink) => { + dispatch(addResourcePaths(getResourcePaths(item, version))); + } + const changeVersion = (ev: React.FormEvent | undefined, option: IChoiceGroupOption | undefined): void => { const selectedVersion = option!.key; @@ -145,8 +153,14 @@ const unstyledResourceExplorer = (props: any) => { setPanelContext(null); } - const openPanel = (activity: string, context: any) => { - if (activity) { + const clickLink = (ev?: React.MouseEvent) => { + ev!.preventDefault(); + } + + const resourceOptionSelected = (activity: string, context: any) => { + if (activity === ResourceOptions.ADD_TO_COLLECTION) { + addToCollection(context); + } else { const requestUrl = getUrlFromLink(context); setPanelIsOpen(true); setPanelContext({ @@ -157,7 +171,7 @@ const unstyledResourceExplorer = (props: any) => { } } - const breadCrumbs = (!!isolated) ? generateBreadCrumbs() : []; + const breadCrumbs = generateBreadCrumbs(); if (pending) { return ( @@ -191,7 +205,14 @@ const unstyledResourceExplorer = (props: any) => { } - {isolated && breadCrumbs.length > 0 && + {selectedLinks && selectedLinks.length > 0 && <> + + + + } + + { + isolated && breadCrumbs.length > 0 && <> { ariaLabel={translateMessage('Path display')} overflowAriaLabel={translateMessage('More path links')} /> - } + + }