feat: add loading local graphql schema file

This commit is contained in:
Max Heidinger 2023-10-20 14:37:15 +02:00
parent 5ba3903c31
commit 45126c99ab
5 changed files with 181 additions and 93 deletions

View File

@ -1,8 +1,7 @@
import React, { useEffect } from 'react'; import React, { useEffect, useState } from 'react';
import find from 'lodash/find'; import find from 'lodash/find';
import get from 'lodash/get'; import get from 'lodash/get';
import classnames from 'classnames'; import classnames from 'classnames';
import { IconRefresh, IconLoader2, IconBook, IconDownload } from '@tabler/icons';
import { useSelector, useDispatch } from 'react-redux'; import { useSelector, useDispatch } from 'react-redux';
import { updateRequestPaneTab } from 'providers/ReduxStore/slices/tabs'; import { updateRequestPaneTab } from 'providers/ReduxStore/slices/tabs';
import QueryEditor from 'components/RequestPane/QueryEditor'; import QueryEditor from 'components/RequestPane/QueryEditor';
@ -16,10 +15,9 @@ import Tests from 'components/RequestPane/Tests';
import { useTheme } from 'providers/Theme'; import { useTheme } from 'providers/Theme';
import { updateRequestGraphqlQuery } from 'providers/ReduxStore/slices/collections'; import { updateRequestGraphqlQuery } from 'providers/ReduxStore/slices/collections';
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions'; import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import { findEnvironmentInCollection } from 'utils/collections';
import useGraphqlSchema from './useGraphqlSchema';
import StyledWrapper from './StyledWrapper'; import StyledWrapper from './StyledWrapper';
import Documentation from 'components/Documentation/index'; import Documentation from 'components/Documentation/index';
import GraphQLSchemaActions from '../GraphQLSchemaActions/index';
const GraphQLRequestPane = ({ item, collection, leftPaneWidth, onSchemaLoad, toggleDocs, handleGqlClickReference }) => { const GraphQLRequestPane = ({ item, collection, leftPaneWidth, onSchemaLoad, toggleDocs, handleGqlClickReference }) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
@ -29,25 +27,11 @@ const GraphQLRequestPane = ({ item, collection, leftPaneWidth, onSchemaLoad, tog
const variables = item.draft const variables = item.draft
? get(item, 'draft.request.body.graphql.variables') ? get(item, 'draft.request.body.graphql.variables')
: get(item, '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 { storedTheme } = useTheme();
const [schema, setSchema] = useState(null);
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();
}
};
useEffect(() => { useEffect(() => {
if (onSchemaLoad) { onSchemaLoad(schema);
onSchemaLoad(schema);
}
}, [schema]); }, [schema]);
const onQueryChange = (value) => { const onQueryChange = (value) => {
@ -163,18 +147,7 @@ const GraphQLRequestPane = ({ item, collection, leftPaneWidth, onSchemaLoad, tog
<div className={getTabClassname('docs')} role="tab" onClick={() => selectTab('docs')}> <div className={getTabClassname('docs')} role="tab" onClick={() => selectTab('docs')}>
Docs Docs
</div> </div>
<div className="flex flex-grow justify-end items-center" style={{ fontSize: 13 }}> <GraphQLSchemaActions item={item} collection={collection} onSchemaLoad={setSchema} toggleDocs={toggleDocs} />
<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>
</div> </div>
<section className="flex w-full mt-5">{getTabPanel(focusedTab.requestPaneTab)}</section> <section className="flex w-full mt-5">{getTabPanel(focusedTab.requestPaneTab)}</section>
</StyledWrapper> </StyledWrapper>

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -1,7 +1,7 @@
const _ = require('lodash'); const _ = require('lodash');
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
const { ipcMain, shell } = require('electron'); const { ipcMain, shell, dialog } = require('electron');
const { envJsonToBru, bruToJson, jsonToBru, jsonToCollectionBru } = require('../bru'); const { envJsonToBru, bruToJson, jsonToBru, jsonToCollectionBru } = require('../bru');
const { const {
@ -461,6 +461,22 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
ipcMain.handle('renderer:open-devtools', async () => { ipcMain.handle('renderer:open-devtools', async () => {
mainWindow.webContents.openDevTools(); 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) => { const registerMainEventHandlers = (mainWindow, watcher, lastOpenedCollections) => {