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) => {