From 45126c99ab7861cf0d8c2b50193fb6190d99a677 Mon Sep 17 00:00:00 2001 From: Max Heidinger Date: Fri, 20 Oct 2023 14:37:15 +0200 Subject: [PATCH] feat: add loading local graphql schema file --- .../RequestPane/GraphQLRequestPane/index.js | 37 ++------ .../GraphQLRequestPane/useGraphqlSchema.js | 60 ------------- .../RequestPane/GraphQLSchemaActions/index.js | 70 +++++++++++++++ .../GraphQLSchemaActions/useGraphqlSchema.js | 89 +++++++++++++++++++ packages/bruno-electron/src/ipc/collection.js | 18 +++- 5 files changed, 181 insertions(+), 93 deletions(-) delete mode 100644 packages/bruno-app/src/components/RequestPane/GraphQLRequestPane/useGraphqlSchema.js create mode 100644 packages/bruno-app/src/components/RequestPane/GraphQLSchemaActions/index.js create mode 100644 packages/bruno-app/src/components/RequestPane/GraphQLSchemaActions/useGraphqlSchema.js diff --git a/packages/bruno-app/src/components/RequestPane/GraphQLRequestPane/index.js b/packages/bruno-app/src/components/RequestPane/GraphQLRequestPane/index.js index 98845b55b..288e66a84 100644 --- a/packages/bruno-app/src/components/RequestPane/GraphQLRequestPane/index.js +++ b/packages/bruno-app/src/components/RequestPane/GraphQLRequestPane/index.js @@ -1,8 +1,7 @@ -import React, { useEffect } from 'react'; +import React, { useEffect, useState } from 'react'; 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'; @@ -16,10 +15,9 @@ import Tests from 'components/RequestPane/Tests'; import { useTheme } from 'providers/Theme'; import { updateRequestGraphqlQuery } from 'providers/ReduxStore/slices/collections'; import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions'; -import { findEnvironmentInCollection } from 'utils/collections'; -import useGraphqlSchema from './useGraphqlSchema'; import StyledWrapper from './StyledWrapper'; import Documentation from 'components/Documentation/index'; +import GraphQLSchemaActions from '../GraphQLSchemaActions/index'; const GraphQLRequestPane = ({ item, collection, leftPaneWidth, onSchemaLoad, toggleDocs, handleGqlClickReference }) => { const dispatch = useDispatch(); @@ -29,25 +27,11 @@ const GraphQLRequestPane = ({ item, collection, leftPaneWidth, onSchemaLoad, tog const variables = item.draft ? get(item, 'draft.request.body.graphql.variables') : get(item, 'request.body.graphql.variables'); - const url = item.draft ? get(item, 'draft.request.url') : get(item, 'request.url'); const { storedTheme } = useTheme(); - - const environment = findEnvironmentInCollection(collection, collection.activeEnvironmentUid); - - const request = item.draft ? item.draft.request : item.request; - - let { schema, loadSchema, isLoading: isSchemaLoading } = useGraphqlSchema(url, environment, request, collection); - - const loadGqlSchema = () => { - if (!isSchemaLoading) { - loadSchema(); - } - }; + const [schema, setSchema] = useState(null); useEffect(() => { - if (onSchemaLoad) { - onSchemaLoad(schema); - } + onSchemaLoad(schema); }, [schema]); const onQueryChange = (value) => { @@ -163,18 +147,7 @@ const GraphQLRequestPane = ({ item, collection, leftPaneWidth, onSchemaLoad, tog
selectTab('docs')}> Docs
-
-
- {isSchemaLoading ? : null} - {!isSchemaLoading && !schema ? : null} - {!isSchemaLoading && schema ? : null} - 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 deleted file mode 100644 index c824c5751..000000000 --- a/packages/bruno-app/src/components/RequestPane/GraphQLRequestPane/useGraphqlSchema.js +++ /dev/null @@ -1,60 +0,0 @@ -import { useState } from 'react'; -import toast from 'react-hot-toast'; -import { buildClientSchema } from 'graphql'; -import { fetchGqlSchema } from 'utils/network'; -import { simpleHash } from 'utils/common'; - -const schemaHashPrefix = 'bruno.graphqlSchema'; - -const useGraphqlSchema = (endpoint, environment, request, collection) => { - 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); - fetchGqlSchema(endpoint, environment, request, collection) - .then((res) => { - if (!res || res.status !== 200) { - return Promise.reject(new Error(res.statusText)); - } - return res.data; - }) - .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 occurred while loading GraphQL Schema: ${err.message}`); - }); - }; - - return { - isLoading, - schema, - loadSchema, - error - }; -}; - -export default useGraphqlSchema; diff --git a/packages/bruno-app/src/components/RequestPane/GraphQLSchemaActions/index.js b/packages/bruno-app/src/components/RequestPane/GraphQLSchemaActions/index.js new file mode 100644 index 000000000..954efebf7 --- /dev/null +++ b/packages/bruno-app/src/components/RequestPane/GraphQLSchemaActions/index.js @@ -0,0 +1,70 @@ +import React, { useEffect, useRef, forwardRef } from 'react'; +import useGraphqlSchema from './useGraphqlSchema'; +import { IconBook, IconDownload, IconLoader2, IconCheckmark } from '@tabler/icons'; +import get from 'lodash/get'; +import { findEnvironmentInCollection } from 'utils/collections'; +import Dropdown from '../../Dropdown'; + +const GraphQLSchemaActions = ({ item, collection, onSchemaLoad, toggleDocs }) => { + const url = item.draft ? get(item, 'draft.request.url') : get(item, 'request.url'); + const environment = findEnvironmentInCollection(collection, collection.activeEnvironmentUid); + const request = item.draft ? item.draft.request : item.request; + + let { + schema, + schemaSource, + loadSchema, + isLoading: isSchemaLoading + } = useGraphqlSchema(url, environment, request, collection); + + useEffect(() => { + if (onSchemaLoad) { + onSchemaLoad(schema); + } + }, [schema]); + + const schemaDropdownTippyRef = useRef(); + const onSchemaDropdownCreate = (ref) => (schemaDropdownTippyRef.current = ref); + + const MenuIcon = forwardRef((props, ref) => { + return ( +
+ {isSchemaLoading && } + {!isSchemaLoading && schema && } + {!isSchemaLoading && !schema && } + Schema +
+ ); + }); + + return ( +
+
+ + Docs +
+ } placement="bottom-start"> +
{ + schemaDropdownTippyRef.current.hide(); + loadSchema('introspection'); + }} + > + {schema && schemaSource === 'introspection' ? 'Refresh from Introspection' : 'Load from Introspection'} +
+
{ + schemaDropdownTippyRef.current.hide(); + loadSchema('file'); + }} + > + Load from File +
+
+
+ ); +}; + +export default GraphQLSchemaActions; diff --git a/packages/bruno-app/src/components/RequestPane/GraphQLSchemaActions/useGraphqlSchema.js b/packages/bruno-app/src/components/RequestPane/GraphQLSchemaActions/useGraphqlSchema.js new file mode 100644 index 000000000..0a5f2bd01 --- /dev/null +++ b/packages/bruno-app/src/components/RequestPane/GraphQLSchemaActions/useGraphqlSchema.js @@ -0,0 +1,89 @@ +import { useState } from 'react'; +import toast from 'react-hot-toast'; +import { buildClientSchema } from 'graphql'; +import { fetchGqlSchema } from 'utils/network'; +import { simpleHash } from 'utils/common'; + +const schemaHashPrefix = 'bruno.graphqlSchema'; + +const useGraphqlSchema = (endpoint, environment, request, collection) => { + const { ipcRenderer } = window; + const localStorageKey = `${schemaHashPrefix}.${simpleHash(endpoint)}`; + const [error, setError] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const [schemaSource, setSchemaSource] = useState(''); + 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 loadSchemaFromIntrospection = async () => { + const response = await fetchGqlSchema(endpoint, environment, request, collection); + if (!response) { + throw new Error('Introspection query failed'); + } + if (response.status !== 200) { + throw new Error(response.statusText); + } + const data = response.data?.data; + if (!data) { + throw new Error('No data returned from introspection query'); + } + setSchemaSource('introspection'); + return data; + }; + + const loadSchemaFromFile = async () => { + const schemaContent = await ipcRenderer.invoke('renderer:load-gql-schema-file'); + if (!schemaContent) { + setIsLoading(false); + return; + } + setSchemaSource('file'); + return schemaContent.data; + }; + + const loadSchema = async (schemaSource) => { + if (isLoading) { + return; + } + + setIsLoading(true); + + try { + let data; + if (schemaSource === 'file') { + data = await loadSchemaFromFile(); + } else { + // fallback to introspection if source is unknown + data = await loadSchemaFromIntrospection(); + } + setSchema(buildClientSchema(data)); + localStorage.setItem(localStorageKey, JSON.stringify(data)); + toast.success('GraphQL Schema loaded successfully'); + } catch (err) { + setError(err); + toast.error(`Error occurred while loading GraphQL Schema: ${err.message}`); + } + + setIsLoading(false); + }; + + return { + isLoading, + schema, + schemaSource, + loadSchema, + error + }; +}; + +export default useGraphqlSchema; diff --git a/packages/bruno-electron/src/ipc/collection.js b/packages/bruno-electron/src/ipc/collection.js index ab92d50bd..be6afbeab 100644 --- a/packages/bruno-electron/src/ipc/collection.js +++ b/packages/bruno-electron/src/ipc/collection.js @@ -1,7 +1,7 @@ const _ = require('lodash'); const fs = require('fs'); const path = require('path'); -const { ipcMain, shell } = require('electron'); +const { ipcMain, shell, dialog } = require('electron'); const { envJsonToBru, bruToJson, jsonToBru, jsonToCollectionBru } = require('../bru'); const { @@ -461,6 +461,22 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection ipcMain.handle('renderer:open-devtools', async () => { mainWindow.webContents.openDevTools(); }); + + ipcMain.handle('renderer:load-gql-schema-file', async () => { + try { + const { filePaths } = await dialog.showOpenDialog(mainWindow, { + properties: ['openFile'] + }); + if (filePaths.length === 0) { + return; + } + + const jsonData = fs.readFileSync(filePaths[0], 'utf8'); + return JSON.parse(jsonData); + } catch (err) { + return Promise.reject(new Error('Failed to load GraphQL schema file')); + } + }); }; const registerMainEventHandlers = (mainWindow, watcher, lastOpenedCollections) => {