mirror of
https://github.com/usebruno/bruno.git
synced 2024-11-26 09:53:50 +01:00
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 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>
|
||||||
|
@ -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 _ = 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) => {
|
||||||
|
Loading…
Reference in New Issue
Block a user