From 2aef7c61a423b4dfee2566cd374a20728db142e3 Mon Sep 17 00:00:00 2001 From: Anoop M D Date: Mon, 7 Nov 2022 02:56:58 +0530 Subject: [PATCH] feat: graphql support (#65) --- .../src/components/CodeEditor/index.js | 2 - .../GraphQLRequestPane/StyledWrapper.js | 48 ++----- .../RequestPane/GraphQLRequestPane/index.js | 121 +++++++++++++++--- .../GraphQLRequestPane/useGraphqlSchema.js | 70 ++++++++++ .../RequestPane/QueryEditor/StyledWrapper.js | 20 ++- .../RequestPane/QueryEditor/index.js | 43 ++++--- .../components/RequestPane/QueryUrl/index.js | 2 +- .../src/components/RequestTabPanel/index.js | 14 +- .../Collection/CollectionItem/index.js | 4 +- .../components/Sidebar/NewRequest/index.js | 20 +-- .../src/components/Sidebar/StyledWrapper.js | 2 +- packages/bruno-app/src/globalStyles.js | 4 + .../src/hooks/useGraphqlSchema/index.js | 44 ------- .../src/providers/ReduxStore/slices/app.js | 4 +- .../ReduxStore/slices/collections/actions.js | 9 +- .../ReduxStore/slices/collections/index.js | 17 +++ .../src/providers/ReduxStore/slices/tabs.js | 2 +- packages/bruno-app/src/themes/dark.js | 1 + packages/bruno-app/src/themes/light.js | 1 + .../bruno-app/src/utils/collections/index.js | 13 +- packages/bruno-app/src/utils/network/index.js | 28 ++-- .../bruno-schema/src/collections/index.js | 8 +- 22 files changed, 306 insertions(+), 171 deletions(-) create mode 100644 packages/bruno-app/src/components/RequestPane/GraphQLRequestPane/useGraphqlSchema.js delete mode 100644 packages/bruno-app/src/hooks/useGraphqlSchema/index.js diff --git a/packages/bruno-app/src/components/CodeEditor/index.js b/packages/bruno-app/src/components/CodeEditor/index.js index c0d92817..5e55d176 100644 --- a/packages/bruno-app/src/components/CodeEditor/index.js +++ b/packages/bruno-app/src/components/CodeEditor/index.js @@ -92,8 +92,6 @@ export default class QueryEditor extends React.Component { } if (this.props.theme !== prevProps.theme && this.editor) { - this.cachedValue = this.props.value; - this.editor.setValue(this.props.value); this.editor.setOption('theme', this.props.theme === 'dark' ? 'monokai' : 'default'); } this.ignoreChangeEvent = false; diff --git a/packages/bruno-app/src/components/RequestPane/GraphQLRequestPane/StyledWrapper.js b/packages/bruno-app/src/components/RequestPane/GraphQLRequestPane/StyledWrapper.js index 44b1420a..d78558bf 100644 --- a/packages/bruno-app/src/components/RequestPane/GraphQLRequestPane/StyledWrapper.js +++ b/packages/bruno-app/src/components/RequestPane/GraphQLRequestPane/StyledWrapper.js @@ -1,22 +1,14 @@ import styled from 'styled-components'; const StyledWrapper = styled.div` - .react-tabs__tab-list { - border-bottom: none !important; - padding-top: 0; - padding-left: 0 !important; - display: flex; - align-items: center; - margin: 0; - - .react-tabs__tab { + div.tabs { + div.tab { padding: 6px 0px; border: none; - user-select: none; border-bottom: solid 2px transparent; - margin-right: 20px; - color: rgb(125 125 125); - outline: none !important; + margin-right: 1.25rem; + color: var(--color-tab-inactive); + cursor: pointer; &:focus, &:active, @@ -27,36 +19,12 @@ const StyledWrapper = styled.div` box-shadow: none !important; } - &:after { - display: none !important; + &.active { + color: ${(props) => props.theme.tabs.active.color} !important; + border-bottom: solid 2px ${(props) => props.theme.tabs.active.border} !important; } } } - - .react-tabs__tab--selected { - border: none; - color: ${(props) => props.theme.tabs.active.color} !important; - border-bottom: solid 2px ${(props) => props.theme.tabs.active.border} !important; - border-color: var(--color-tab-active-border) !important; - background: inherit; - outline: none !important; - box-shadow: none !important; - - &:focus, - &:active, - &:focus-within, - &:focus-visible, - &:target { - border: none; - outline: none !important; - box-shadow: none !important; - border-bottom: solid 2px ${(props) => props.theme.tabs.active.border} !important; - border-color: var(--color-tab-active-border) !important; - background: inherit; - outline: none !important; - box-shadow: none !important; - } - } `; export default StyledWrapper; diff --git a/packages/bruno-app/src/components/RequestPane/GraphQLRequestPane/index.js b/packages/bruno-app/src/components/RequestPane/GraphQLRequestPane/index.js index 089d494b..c2cdea8b 100644 --- a/packages/bruno-app/src/components/RequestPane/GraphQLRequestPane/index.js +++ b/packages/bruno-app/src/components/RequestPane/GraphQLRequestPane/index.js @@ -1,26 +1,115 @@ import React from 'react'; -import { Tab, TabList, TabPanel, Tabs } from 'react-tabs'; +import find from 'lodash/find'; +import get from 'lodash/get'; +import classnames from 'classnames'; +import { IconRefresh, IconLoader2, IconBook, IconDownload } from '@tabler/icons'; +import { useSelector, useDispatch } from 'react-redux'; +import { updateRequestPaneTab } from 'providers/ReduxStore/slices/tabs'; import QueryEditor from 'components/RequestPane/QueryEditor'; import RequestHeaders from 'components/RequestPane/RequestHeaders'; +import { useTheme } from 'providers/Theme'; +import { updateRequestGraphqlQuery } from 'providers/ReduxStore/slices/collections'; +import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions'; +import useGraphqlSchema from './useGraphqlSchema'; import StyledWrapper from './StyledWrapper'; -const GraphQLRequestPane = ({ onRunQuery, schema, leftPaneWidth, value, onQueryChange }) => { +const GraphQLRequestPane = ({ item, collection, leftPaneWidth }) => { + const dispatch = useDispatch(); + const tabs = useSelector((state) => state.tabs.tabs); + const activeTabUid = useSelector((state) => state.tabs.activeTabUid); + const query = item.draft ? get(item, 'draft.request.body.graphql.query') : get(item, 'request.body.graphql.query'); + const url = item.draft ? get(item, 'draft.request.url') : get(item, 'request.url'); + const { + storedTheme + } = useTheme(); + + let { + schema, + loadSchema, + isLoading: isSchemaLoading, + error: schemaError + } = useGraphqlSchema(url); + + const loadGqlSchema = () => { + if(!isSchemaLoading) { + loadSchema(); + } + }; + + const onQueryChange = (value) => { + dispatch( + updateRequestGraphqlQuery({ + query: value, + itemUid: item.uid, + collectionUid: collection.uid + }) + ); + }; + const onRun = () => dispatch(sendRequest(item, collection.uid)); + const onSave = () => dispatch(saveRequest(item.uid, collection.uid)); + + const selectTab = (tab) => { + dispatch( + updateRequestPaneTab({ + uid: item.uid, + requestPaneTab: tab + }) + ); + }; + + const getTabPanel = (tab) => { + switch (tab) { + case 'query': { + return ; + } + case 'headers': { + return ; + } + default: { + return
404 | Not found
; + } + } + }; + + if (!activeTabUid) { + return
Something went wrong
; + } + + const focusedTab = find(tabs, (t) => t.uid === activeTabUid); + if (!focusedTab || !focusedTab.uid || !focusedTab.requestPaneTab) { + return
An error occured!
; + } + + const getTabClassname = (tabName) => { + return classnames(`tab select-none ${tabName}`, { + active: tabName === focusedTab.requestPaneTab + }); + }; + return ( - - - - Query - Headers - - -
- + +
+
selectTab('query')}> + Query +
+
selectTab('headers')}> + Headers +
+
+
+ {isSchemaLoading ? ( + + ) : null} + {!isSchemaLoading && !schema ? : null } + {!isSchemaLoading && schema ? : null } + {schema ? 'Schema' : 'Load Schema'}
- - - - - + {/*
+ Docs +
*/} +
+
+
{getTabPanel(focusedTab.requestPaneTab)}
); }; diff --git a/packages/bruno-app/src/components/RequestPane/GraphQLRequestPane/useGraphqlSchema.js b/packages/bruno-app/src/components/RequestPane/GraphQLRequestPane/useGraphqlSchema.js new file mode 100644 index 00000000..40daa63e --- /dev/null +++ b/packages/bruno-app/src/components/RequestPane/GraphQLRequestPane/useGraphqlSchema.js @@ -0,0 +1,70 @@ +import { useState } from 'react'; +import toast from 'react-hot-toast'; +import { getIntrospectionQuery, buildClientSchema } from 'graphql'; +import { simpleHash } from 'utils/common'; + +const schemaHashPrefix = 'bruno.graphqlSchema'; + +const fetchSchema = (endpoint) => { + const introspectionQuery = getIntrospectionQuery(); + const queryParams = { + query: introspectionQuery + }; + + return fetch(endpoint, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json' + }, + body: JSON.stringify(queryParams) + }); +} + +const useGraphqlSchema = (endpoint) => { + const localStorageKey = `${schemaHashPrefix}.${simpleHash(endpoint)}`; + const [error, setError] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const [schema, setSchema] = useState(() => { + try { + const saved = localStorage.getItem(localStorageKey); + if(!saved) { + return null; + } + return buildClientSchema(JSON.parse(saved)); + } catch { + localStorage.setItem(localStorageKey, null); + return null; + } + }); + + const loadSchema = () => { + setIsLoading(true); + fetchSchema(endpoint) + .then((res) => res.json()) + .then((s) => { + if (s && s.data) { + setSchema(buildClientSchema(s.data)); + setIsLoading(false); + localStorage.setItem(localStorageKey, JSON.stringify(s.data)); + toast.success('Graphql Schema loaded successfully'); + } else { + return Promise.reject(new Error('An error occurred while introspecting schema')); + } + }) + .catch((err) => { + setIsLoading(false); + setError(err); + toast.error('Error occured while loading Graphql Schema'); + }); + }; + + return { + isLoading, + schema, + loadSchema, + error + }; +}; + +export default useGraphqlSchema; diff --git a/packages/bruno-app/src/components/RequestPane/QueryEditor/StyledWrapper.js b/packages/bruno-app/src/components/RequestPane/QueryEditor/StyledWrapper.js index df93f412..e7133287 100644 --- a/packages/bruno-app/src/components/RequestPane/QueryEditor/StyledWrapper.js +++ b/packages/bruno-app/src/components/RequestPane/QueryEditor/StyledWrapper.js @@ -5,12 +5,30 @@ const StyledWrapper = styled.div` background: ${(props) => props.theme.codemirror.bg}; border: solid 1px ${(props) => props.theme.codemirror.border}; /* todo: find a better way */ - height: calc(100vh - 250px); + height: calc(100vh - 220px); } textarea.cm-editor { position: relative; } + + // Todo: dark mode temporary fix + // Clean this + .cm-s-monokai span.cm-property, .cm-s-monokai span.cm-attribute { + color: #9cdcfe !important; + } + + .cm-s-monokai span.cm-string { + color: #ce9178 !important; + } + + .cm-s-monokai span.cm-number{ + color: #b5cea8 !important; + } + + .cm-s-monokai span.cm-atom{ + color: #569cd6 !important; + } `; export default StyledWrapper; diff --git a/packages/bruno-app/src/components/RequestPane/QueryEditor/index.js b/packages/bruno-app/src/components/RequestPane/QueryEditor/index.js index 7a872983..face6edf 100644 --- a/packages/bruno-app/src/components/RequestPane/QueryEditor/index.js +++ b/packages/bruno-app/src/components/RequestPane/QueryEditor/index.js @@ -38,6 +38,7 @@ export default class QueryEditor extends React.Component { tabSize: 2, mode: 'graphql', theme: this.props.editorTheme || 'graphiql', + theme: this.props.theme === 'dark' ? 'monokai' : 'default', keyMap: 'sublime', autoCloseBrackets: true, matchBrackets: true, @@ -75,54 +76,51 @@ export default class QueryEditor extends React.Component { 'Alt-Space': () => editor.showHint({ completeSingle: true, container: this._node }), 'Shift-Space': () => editor.showHint({ completeSingle: true, container: this._node }), 'Shift-Alt-Space': () => editor.showHint({ completeSingle: true, container: this._node }), - 'Cmd-Enter': () => { - if (this.props.onRunQuery) { - this.props.onRunQuery(); + if (this.props.onRun) { + this.props.onRun(); } }, 'Ctrl-Enter': () => { - if (this.props.onRunQuery) { - this.props.onRunQuery(); + if (this.props.onRun) { + this.props.onRun(); } }, - 'Shift-Ctrl-C': () => { if (this.props.onCopyQuery) { this.props.onCopyQuery(); } }, - 'Shift-Ctrl-P': () => { if (this.props.onPrettifyQuery) { this.props.onPrettifyQuery(); } }, - /* Shift-Ctrl-P is hard coded in Firefox for private browsing so adding an alternative to Pretiffy */ - 'Shift-Ctrl-F': () => { if (this.props.onPrettifyQuery) { this.props.onPrettifyQuery(); } }, - 'Shift-Ctrl-M': () => { if (this.props.onMergeQuery) { this.props.onMergeQuery(); } }, 'Cmd-S': () => { - if (this.props.onRunQuery) { - // empty + if (this.props.onSave) { + this.props.onSave(); + return false; } }, - 'Ctrl-S': () => { - if (this.props.onRunQuery) { - // empty + if (this.props.onSave) { + this.props.onSave(); + return false; } - } + }, + 'Cmd-F': 'findPersistent', + 'Ctrl-F': 'findPersistent' } })); if (editor) { @@ -149,6 +147,10 @@ export default class QueryEditor extends React.Component { this.cachedValue = this.props.value; this.editor.setValue(this.props.value); } + + if (this.props.theme !== prevProps.theme && this.editor) { + this.editor.setOption('theme', this.props.theme === 'dark' ? 'monokai' : 'default'); + } this.ignoreChangeEvent = false; } @@ -164,7 +166,7 @@ export default class QueryEditor extends React.Component { render() { return ( { this._node = node; @@ -173,8 +175,11 @@ export default class QueryEditor extends React.Component { ); } - _onKeyUp = (_cm, event) => { - if (AUTO_COMPLETE_AFTER_KEY.test(event.key) && this.editor) { + _onKeyUp = (_cm, e) => { + if (e.metaKey || e.ctrlKey || e.altKey) { + return; + } + if (AUTO_COMPLETE_AFTER_KEY.test(e.key) && this.editor) { this.editor.execCommand('autocomplete'); } }; diff --git a/packages/bruno-app/src/components/RequestPane/QueryUrl/index.js b/packages/bruno-app/src/components/RequestPane/QueryUrl/index.js index 4c8bd24d..e77b8d3a 100644 --- a/packages/bruno-app/src/components/RequestPane/QueryUrl/index.js +++ b/packages/bruno-app/src/components/RequestPane/QueryUrl/index.js @@ -11,7 +11,7 @@ const QueryUrl = ({ item, collection, handleRun }) => { const { theme } = useTheme(); const dispatch = useDispatch(); const method = item.draft ? get(item, 'draft.request.method') : get(item, 'request.method'); - let url = item.draft ? get(item, 'draft.request.url') : get(item, 'request.url'); + const url = item.draft ? get(item, 'draft.request.url') : get(item, 'request.url'); const onUrlChange = (value) => { dispatch( diff --git a/packages/bruno-app/src/components/RequestTabPanel/index.js b/packages/bruno-app/src/components/RequestTabPanel/index.js index 591793ad..9ffb2a9d 100644 --- a/packages/bruno-app/src/components/RequestTabPanel/index.js +++ b/packages/bruno-app/src/components/RequestTabPanel/index.js @@ -12,7 +12,6 @@ import { sendRequest } from 'providers/ReduxStore/slices/collections/actions'; import RequestNotFound from './RequestNotFound'; import QueryUrl from 'components/RequestPane/QueryUrl'; import NetworkError from 'components/ResponsePane/NetworkError'; -import useGraphqlSchema from '../../hooks/useGraphqlSchema'; import StyledWrapper from './StyledWrapper'; @@ -65,11 +64,6 @@ const RequestTabPanel = () => { setDragging(true); }; - let schema = null; - // let { - // schema - // } = useGraphqlSchema('https://api.spacex.land/graphql'); - useEffect(() => { document.addEventListener('mouseup', handleMouseUp); document.addEventListener('mousemove', handleMouseMove); @@ -105,8 +99,6 @@ const RequestTabPanel = () => { }) ); }; - const onGraphqlQueryChange = (value) => {}; - const runQuery = async () => {}; return ( @@ -118,11 +110,9 @@ const RequestTabPanel = () => {
{item.type === 'graphql-request' ? ( ) : null} diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/index.js index 56c70f0a..b41940c9 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/index.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/index.js @@ -15,6 +15,7 @@ import CloneCollectionItem from './CloneCollectionItem'; import DeleteCollectionItem from './DeleteCollectionItem'; import { isItemARequest, isItemAFolder, itemIsOpenedInTabs } from 'utils/tabs'; import { doesRequestMatchSearchText, doesFolderHaveItemsMatchSearchText } from 'utils/collections/search'; +import { getDefaultRequestPaneTab } from 'utils/collections'; import { hideHomePage } from 'providers/ReduxStore/slices/app'; import StyledWrapper from './StyledWrapper'; @@ -69,7 +70,8 @@ const CollectionItem = ({ item, collection, searchText }) => { dispatch( addTab({ uid: item.uid, - collectionUid: collection.uid + collectionUid: collection.uid, + requestPaneTab: getDefaultRequestPaneTab(item) }) ); } diff --git a/packages/bruno-app/src/components/Sidebar/NewRequest/index.js b/packages/bruno-app/src/components/Sidebar/NewRequest/index.js index 9cc42c2d..987a5272 100644 --- a/packages/bruno-app/src/components/Sidebar/NewRequest/index.js +++ b/packages/bruno-app/src/components/Sidebar/NewRequest/index.js @@ -9,6 +9,7 @@ import { newEphermalHttpRequest } from 'providers/ReduxStore/slices/collections' import { newHttpRequest } from 'providers/ReduxStore/slices/collections/actions'; import { addTab } from 'providers/ReduxStore/slices/tabs'; import HttpMethodSelector from 'components/RequestPane/QueryUrl/HttpMethodSelector'; +import { getDefaultRequestPaneTab } from 'utils/collections'; import StyledWrapper from './StyledWrapper'; const NewRequest = ({ collection, item, isEphermal, onClose }) => { @@ -42,7 +43,8 @@ const NewRequest = ({ collection, item, isEphermal, onClose }) => { dispatch( addTab({ uid: uid, - collectionUid: collection.uid + collectionUid: collection.uid, + requestPaneTab: getDefaultRequestPaneTab({type: values.requestType}) }) ); onClose(); @@ -77,27 +79,27 @@ const NewRequest = ({ collection, item, isEphermal, onClose }) => {
-
+
-
-
+
diff --git a/packages/bruno-app/src/components/Sidebar/StyledWrapper.js b/packages/bruno-app/src/components/Sidebar/StyledWrapper.js index a677c0cb..7781e6dc 100644 --- a/packages/bruno-app/src/components/Sidebar/StyledWrapper.js +++ b/packages/bruno-app/src/components/Sidebar/StyledWrapper.js @@ -54,7 +54,7 @@ const Wrapper = styled.div` &:hover div.drag-request-border { width: 2px; height: 100%; - border-left: solid 1px var(--color-request-dragbar-background-active); + border-left: solid 1px ${(props) => props.theme.sidebar.dragbar}; } } `; diff --git a/packages/bruno-app/src/globalStyles.js b/packages/bruno-app/src/globalStyles.js index c9b212a5..2e49f4a6 100644 --- a/packages/bruno-app/src/globalStyles.js +++ b/packages/bruno-app/src/globalStyles.js @@ -22,6 +22,10 @@ const GlobalStyle = createGlobalStyle` padding: .215rem .6rem .215rem .6rem; } + .btn-xs { + padding: .2rem .4rem .2rem .4rem; + } + .btn-md { padding: .4rem 1.1rem; line-height: 1.47; diff --git a/packages/bruno-app/src/hooks/useGraphqlSchema/index.js b/packages/bruno-app/src/hooks/useGraphqlSchema/index.js deleted file mode 100644 index 783faa68..00000000 --- a/packages/bruno-app/src/hooks/useGraphqlSchema/index.js +++ /dev/null @@ -1,44 +0,0 @@ -import { useState, useEffect } from 'react'; -import { getIntrospectionQuery, buildClientSchema } from 'graphql'; - -const useGraphqlSchema = (endpoint) => { - const [isLoaded, setIsLoaded] = useState(false); - const [schema, setSchema] = useState(null); - const [error, setError] = useState(null); - - const introspectionQuery = getIntrospectionQuery(); - const queryParams = { - query: introspectionQuery - }; - - useEffect(() => { - fetch(endpoint, { - method: 'POST', - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json' - }, - body: JSON.stringify(queryParams) - }) - .then((res) => res.json()) - .then((s) => { - if (s && s.data) { - setSchema(buildClientSchema(s.data)); - setIsLoaded(true); - } else { - return Promise.reject(new Error('An error occurred while introspecting schema')); - } - }) - .catch((err) => { - setError(err); - }); - }, []); - - return { - isLoaded, - schema, - error - }; -}; - -export default useGraphqlSchema; diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/app.js b/packages/bruno-app/src/providers/ReduxStore/slices/app.js index 8c3c08f9..6736370c 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/app.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/app.js @@ -3,8 +3,8 @@ import { createSlice } from '@reduxjs/toolkit'; const initialState = { isDragging: false, idbConnectionReady: false, - leftSidebarWidth: 270, - leftMenuBarOpen: true, + leftSidebarWidth: 222, + leftMenuBarOpen: false, screenWidth: 500, showHomePage: false }; diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js index 190e1e7c..acc9883f 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js @@ -16,7 +16,8 @@ import { findEnvironmentInCollection, isItemAFolder, refreshUidsInItem, - interpolateEnvironmentVars + interpolateEnvironmentVars, + getDefaultRequestPaneTab } from 'utils/collections'; import { collectionSchema, itemSchema, environmentsSchema } from '@usebruno/schema'; import { waitForNextTick } from 'utils/common'; @@ -108,7 +109,8 @@ export const createCollection = (collectionName) => (dispatch, getState) => { dispatch( addTab({ uid: requestItem.uid, - collectionUid: newCollection.uid + collectionUid: newCollection.uid, + requestPaneTab: getDefaultRequestPaneTab(requestItem) }) ) ) @@ -635,7 +637,8 @@ export const newHttpRequest = (params) => (dispatch, getState) => { dispatch( addTab({ uid: item.uid, - collectionUid: collection.uid + collectionUid: collection.uid, + requestPaneTab: getDefaultRequestPaneTab(item) }) ); }) diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js index 4ca807f4..a9ae2259 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js @@ -606,6 +606,22 @@ export const collectionsSlice = createSlice({ } } }, + updateRequestGraphqlQuery: (state, action) => { + const collection = findCollectionByUid(state.collections, action.payload.collectionUid); + + if (collection) { + const item = findItemInCollection(collection, action.payload.itemUid); + + if (item && isItemARequest(item)) { + if (!item.draft) { + item.draft = cloneDeep(item); + } + item.draft.request.body.mode = 'graphql'; + item.draft.request.body.graphql = item.draft.request.body.graphql || {}; + item.draft.request.body.graphql.query = action.payload.query; + } + } + }, updateRequestMethod: (state, action) => { const collection = findCollectionByUid(state.collections, action.payload.collectionUid); @@ -791,6 +807,7 @@ export const { deleteMultipartFormParam, updateRequestBodyMode, updateRequestBody, + updateRequestGraphqlQuery, updateRequestMethod, localCollectionAddFileEvent, localCollectionAddDirectoryEvent, diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/tabs.js b/packages/bruno-app/src/providers/ReduxStore/slices/tabs.js index bc6f1d52..d12f1858 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/tabs.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/tabs.js @@ -23,7 +23,7 @@ export const tabsSlice = createSlice({ uid: action.payload.uid, collectionUid: action.payload.collectionUid, requestPaneWidth: null, - requestPaneTab: 'params', + requestPaneTab: action.payload.requestPaneTab || 'params', responsePaneTab: 'response' }); state.activeTabUid = action.payload.uid; diff --git a/packages/bruno-app/src/themes/dark.js b/packages/bruno-app/src/themes/dark.js index 8f9dbb27..7124c3be 100644 --- a/packages/bruno-app/src/themes/dark.js +++ b/packages/bruno-app/src/themes/dark.js @@ -22,6 +22,7 @@ const darkTheme = { color: '#ccc', muted: '#9d9d9d', bg: '#252526', + dragbar: '#8a8a8a', workspace: { bg: '#3D3D3D' diff --git a/packages/bruno-app/src/themes/light.js b/packages/bruno-app/src/themes/light.js index 3b0cc053..a308208b 100644 --- a/packages/bruno-app/src/themes/light.js +++ b/packages/bruno-app/src/themes/light.js @@ -22,6 +22,7 @@ const lightTheme = { color: 'rgb(52, 52, 52)', muted: '#4b5563', bg: '#F3F3F3', + dragbar: 'rgb(200, 200, 200)', workspace: { bg: '#e1e1e1' diff --git a/packages/bruno-app/src/utils/collections/index.js b/packages/bruno-app/src/utils/collections/index.js index bfa84a1c..8c826380 100644 --- a/packages/bruno-app/src/utils/collections/index.js +++ b/packages/bruno-app/src/utils/collections/index.js @@ -196,7 +196,7 @@ export const transformCollectionToSaveToIdb = (collection, options = {}) => { json: si.draft.request.body.json, text: si.draft.request.body.text, xml: si.draft.request.body.xml, - multipartForm: si.draft.request.body.multipartForm, + graphql: si.draft.request.body.graphql, formUrlEncoded: copyFormUrlEncodedParams(si.draft.request.body.formUrlEncoded), multipartForm: copyMultipartFormParams(si.draft.request.body.multipartForm) } @@ -214,6 +214,7 @@ export const transformCollectionToSaveToIdb = (collection, options = {}) => { json: si.request.body.json, text: si.request.body.text, xml: si.request.body.xml, + graphql: si.request.body.graphql, formUrlEncoded: copyFormUrlEncodedParams(si.request.body.formUrlEncoded), multipartForm: copyMultipartFormParams(si.request.body.multipartForm) } @@ -410,3 +411,13 @@ export const interpolateEnvironmentVars = (item, variables) => { return request; }; + +export const getDefaultRequestPaneTab = (item) => { + if(item.type === 'http-request') { + return 'params'; + } + + if(item.type === 'graphql-request') { + return 'query'; + } +}; diff --git a/packages/bruno-app/src/utils/network/index.js b/packages/bruno-app/src/utils/network/index.js index 33f3a3f5..1efde569 100644 --- a/packages/bruno-app/src/utils/network/index.js +++ b/packages/bruno-app/src/utils/network/index.js @@ -1,14 +1,14 @@ +import get from 'lodash/get'; import each from 'lodash/each'; import filter from 'lodash/filter'; import qs from 'qs'; -import { rawRequest, gql } from 'graphql'; import { sendHttpRequestInBrowser } from './browser'; import { isElectron } from 'utils/common/platform'; import cancelTokens, { deleteCancelToken } from 'utils/network/cancelTokens'; export const sendNetworkRequest = async (item, options) => { return new Promise((resolve, reject) => { - if (item.type === 'http-request') { + if (['http-request', 'graphql-request'].includes(item.type)) { const timeStart = Date.now(); sendHttpRequest(item.draft ? item.draft.request : item.request, options) .then((response) => { @@ -79,6 +79,15 @@ const sendHttpRequest = async (request, options) => { axiosRequest.data = params; } + if (request.body.mode === 'graphql') { + const graphqlQuery = { + query: get(request, 'body.graphql.query'), + variables: JSON.parse(get(request, 'body.graphql.variables') || '{}') + }; + axiosRequest.headers['content-type'] = 'application/json'; + axiosRequest.data = graphqlQuery; + } + console.log('>>> Sending Request'); console.log(axiosRequest); @@ -90,21 +99,6 @@ const sendHttpRequest = async (request, options) => { }); }; -const sendGraphqlRequest = async (request) => { - const query = gql` - ${request.request.body.graphql.query} - `; - - const { data, errors, extensions, headers, status } = await rawRequest(request.request.url, query); - - return { - data, - headers, - data, - errors - }; -}; - export const cancelNetworkRequest = async (cancelTokenUid) => { if (isElectron()) { return new Promise((resolve, reject) => { diff --git a/packages/bruno-schema/src/collections/index.js b/packages/bruno-schema/src/collections/index.js index 60326de7..e4481e6b 100644 --- a/packages/bruno-schema/src/collections/index.js +++ b/packages/bruno-schema/src/collections/index.js @@ -29,13 +29,19 @@ const keyValueSchema = Yup.object({ const requestUrlSchema = Yup.string().min(0).max(2048, 'name must be 2048 characters or less').defined(); const requestMethodSchema = Yup.string().oneOf(['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD']).required('method is required'); +const graphqlBodySchema = Yup.object({ + query: Yup.string().max(10240, 'json must be 10240 characters or less').nullable(), + variables: Yup.string().max(10240, 'text must be 10240 characters or less').nullable(), +}).noUnknown(true).strict(); + const requestBodySchema = Yup.object({ - mode: Yup.string().oneOf(['none', 'json', 'text', 'xml', 'formUrlEncoded', 'multipartForm']).required('mode is required'), + mode: Yup.string().oneOf(['none', 'json', 'text', 'xml', 'formUrlEncoded', 'multipartForm', 'graphql']).required('mode is required'), json: Yup.string().max(10240, 'json must be 10240 characters or less').nullable(), text: Yup.string().max(10240, 'text must be 10240 characters or less').nullable(), xml: Yup.string().max(10240, 'xml must be 10240 characters or less').nullable(), formUrlEncoded: Yup.array().of(keyValueSchema).nullable(), multipartForm: Yup.array().of(keyValueSchema).nullable(), + graphql: graphqlBodySchema.nullable(), }).noUnknown(true).strict(); // Right now, the request schema is very tightly coupled with http request