forked from extern/bruno
feat: add loading local graphql schema file
This commit is contained in:
parent
5ba3903c31
commit
45126c99ab
@ -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
|
||||
<div className={getTabClassname('docs')} role="tab" onClick={() => selectTab('docs')}>
|
||||
Docs
|
||||
</div>
|
||||
<div className="flex flex-grow justify-end items-center" style={{ fontSize: 13 }}>
|
||||
<div className="flex items-center cursor-pointer hover:underline" onClick={loadGqlSchema}>
|
||||
{isSchemaLoading ? <IconLoader2 className="animate-spin" size={18} strokeWidth={1.5} /> : null}
|
||||
{!isSchemaLoading && !schema ? <IconDownload size={18} strokeWidth={1.5} /> : null}
|
||||
{!isSchemaLoading && schema ? <IconRefresh size={18} strokeWidth={1.5} /> : null}
|
||||
<span className="ml-1">Schema</span>
|
||||
</div>
|
||||
<div className="flex items-center cursor-pointer hover:underline ml-2" onClick={toggleDocs}>
|
||||
<IconBook size={18} strokeWidth={1.5} />
|
||||
<span className="ml-1">Docs</span>
|
||||
</div>
|
||||
</div>
|
||||
<GraphQLSchemaActions item={item} collection={collection} onSchemaLoad={setSchema} toggleDocs={toggleDocs} />
|
||||
</div>
|
||||
<section className="flex w-full mt-5">{getTabPanel(focusedTab.requestPaneTab)}</section>
|
||||
</StyledWrapper>
|
||||
|
@ -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;
|
@ -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 (
|
||||
<div ref={ref} className="dropdown-icon cursor-pointer flex hover:underline ml-2">
|
||||
{isSchemaLoading && <IconLoader2 className="animate-spin" size={18} strokeWidth={1.5} />}
|
||||
{!isSchemaLoading && schema && <IconDownload size={18} strokeWidth={1.5} />}
|
||||
{!isSchemaLoading && !schema && <IconCheckmark size={18} strokeWidth={1.5} />}
|
||||
<span className="ml-1">Schema</span>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="flex flex-grow justify-end items-center" style={{ fontSize: 13 }}>
|
||||
<div className="flex items-center cursor-pointer hover:underline" onClick={toggleDocs}>
|
||||
<IconBook size={18} strokeWidth={1.5} />
|
||||
<span className="ml-1">Docs</span>
|
||||
</div>
|
||||
<Dropdown onCreate={onSchemaDropdownCreate} icon={<MenuIcon />} placement="bottom-start">
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={(e) => {
|
||||
schemaDropdownTippyRef.current.hide();
|
||||
loadSchema('introspection');
|
||||
}}
|
||||
>
|
||||
{schema && schemaSource === 'introspection' ? 'Refresh from Introspection' : 'Load from Introspection'}
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={(e) => {
|
||||
schemaDropdownTippyRef.current.hide();
|
||||
loadSchema('file');
|
||||
}}
|
||||
>
|
||||
Load from File
|
||||
</div>
|
||||
</Dropdown>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default GraphQLSchemaActions;
|
@ -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;
|
@ -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) => {
|
||||
|
Loading…
Reference in New Issue
Block a user