forked from extern/bruno
feat: graphql support (#65)
This commit is contained in:
parent
530af1f929
commit
2aef7c61a4
@ -92,8 +92,6 @@ export default class QueryEditor extends React.Component {
|
||||
}
|
||||
|
||||
if (this.props.theme !== prevProps.theme && this.editor) {
|
||||
this.cachedValue = this.props.value;
|
||||
this.editor.setValue(this.props.value);
|
||||
this.editor.setOption('theme', this.props.theme === 'dark' ? 'monokai' : 'default');
|
||||
}
|
||||
this.ignoreChangeEvent = false;
|
||||
|
@ -1,22 +1,14 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
.react-tabs__tab-list {
|
||||
border-bottom: none !important;
|
||||
padding-top: 0;
|
||||
padding-left: 0 !important;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 0;
|
||||
|
||||
.react-tabs__tab {
|
||||
div.tabs {
|
||||
div.tab {
|
||||
padding: 6px 0px;
|
||||
border: none;
|
||||
user-select: none;
|
||||
border-bottom: solid 2px transparent;
|
||||
margin-right: 20px;
|
||||
color: rgb(125 125 125);
|
||||
outline: none !important;
|
||||
margin-right: 1.25rem;
|
||||
color: var(--color-tab-inactive);
|
||||
cursor: pointer;
|
||||
|
||||
&:focus,
|
||||
&:active,
|
||||
@ -27,36 +19,12 @@ const StyledWrapper = styled.div`
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
&:after {
|
||||
display: none !important;
|
||||
&.active {
|
||||
color: ${(props) => props.theme.tabs.active.color} !important;
|
||||
border-bottom: solid 2px ${(props) => props.theme.tabs.active.border} !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.react-tabs__tab--selected {
|
||||
border: none;
|
||||
color: ${(props) => props.theme.tabs.active.color} !important;
|
||||
border-bottom: solid 2px ${(props) => props.theme.tabs.active.border} !important;
|
||||
border-color: var(--color-tab-active-border) !important;
|
||||
background: inherit;
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
|
||||
&:focus,
|
||||
&:active,
|
||||
&:focus-within,
|
||||
&:focus-visible,
|
||||
&:target {
|
||||
border: none;
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
border-bottom: solid 2px ${(props) => props.theme.tabs.active.border} !important;
|
||||
border-color: var(--color-tab-active-border) !important;
|
||||
background: inherit;
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
|
@ -1,26 +1,115 @@
|
||||
import React from 'react';
|
||||
import { Tab, TabList, TabPanel, Tabs } from 'react-tabs';
|
||||
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';
|
||||
import RequestHeaders from 'components/RequestPane/RequestHeaders';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { updateRequestGraphqlQuery } from 'providers/ReduxStore/slices/collections';
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import useGraphqlSchema from './useGraphqlSchema';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const GraphQLRequestPane = ({ onRunQuery, schema, leftPaneWidth, value, onQueryChange }) => {
|
||||
const GraphQLRequestPane = ({ item, collection, leftPaneWidth }) => {
|
||||
const dispatch = useDispatch();
|
||||
const tabs = useSelector((state) => state.tabs.tabs);
|
||||
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
|
||||
const query = item.draft ? get(item, 'draft.request.body.graphql.query') : get(item, 'request.body.graphql.query');
|
||||
const url = item.draft ? get(item, 'draft.request.url') : get(item, 'request.url');
|
||||
const {
|
||||
storedTheme
|
||||
} = useTheme();
|
||||
|
||||
let {
|
||||
schema,
|
||||
loadSchema,
|
||||
isLoading: isSchemaLoading,
|
||||
error: schemaError
|
||||
} = useGraphqlSchema(url);
|
||||
|
||||
const loadGqlSchema = () => {
|
||||
if(!isSchemaLoading) {
|
||||
loadSchema();
|
||||
}
|
||||
};
|
||||
|
||||
const onQueryChange = (value) => {
|
||||
dispatch(
|
||||
updateRequestGraphqlQuery({
|
||||
query: value,
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
};
|
||||
const onRun = () => dispatch(sendRequest(item, collection.uid));
|
||||
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||
|
||||
const selectTab = (tab) => {
|
||||
dispatch(
|
||||
updateRequestPaneTab({
|
||||
uid: item.uid,
|
||||
requestPaneTab: tab
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const getTabPanel = (tab) => {
|
||||
switch (tab) {
|
||||
case 'query': {
|
||||
return <QueryEditor theme={storedTheme} schema={schema} width={leftPaneWidth} onSave={onSave} value={query} onRun={onRun} onEdit={onQueryChange} />;
|
||||
}
|
||||
case 'headers': {
|
||||
return <RequestHeaders item={item} collection={collection} />;
|
||||
}
|
||||
default: {
|
||||
return <div className="mt-4">404 | Not found</div>;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (!activeTabUid) {
|
||||
return <div>Something went wrong</div>;
|
||||
}
|
||||
|
||||
const focusedTab = find(tabs, (t) => t.uid === activeTabUid);
|
||||
if (!focusedTab || !focusedTab.uid || !focusedTab.requestPaneTab) {
|
||||
return <div className="pb-4 px-4">An error occured!</div>;
|
||||
}
|
||||
|
||||
const getTabClassname = (tabName) => {
|
||||
return classnames(`tab select-none ${tabName}`, {
|
||||
active: tabName === focusedTab.requestPaneTab
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="h-full">
|
||||
<Tabs className="react-tabs mt-1 flex flex-grow flex-col h-full" forceRenderTabPanel>
|
||||
<TabList>
|
||||
<Tab tabIndex="-1">Query</Tab>
|
||||
<Tab tabIndex="-1">Headers</Tab>
|
||||
</TabList>
|
||||
<TabPanel>
|
||||
<div className="mt-4">
|
||||
<QueryEditor schema={schema} width={leftPaneWidth} value={value} onRunQuery={onRunQuery} onEdit={onQueryChange} />
|
||||
<StyledWrapper className="flex flex-col h-full relative">
|
||||
<div className="flex items-center tabs" role="tablist">
|
||||
<div className={getTabClassname('query')} role="tab" onClick={() => selectTab('query')}>
|
||||
Query
|
||||
</div>
|
||||
<div className={getTabClassname('headers')} role="tab" onClick={() => selectTab('headers')}>
|
||||
Headers
|
||||
</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 ? 'Schema' : 'Load Schema'}</span>
|
||||
</div>
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<RequestHeaders />
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
{/* <div className='flex items-center cursor-pointer hover:underline ml-2'>
|
||||
<IconBook size={18} strokeWidth={1.5} /><span className='ml-1'>Docs</span>
|
||||
</div> */}
|
||||
</div>
|
||||
</div>
|
||||
<section className="flex w-full mt-5">{getTabPanel(focusedTab.requestPaneTab)}</section>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
@ -0,0 +1,70 @@
|
||||
import { useState } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import { getIntrospectionQuery, buildClientSchema } from 'graphql';
|
||||
import { simpleHash } from 'utils/common';
|
||||
|
||||
const schemaHashPrefix = 'bruno.graphqlSchema';
|
||||
|
||||
const fetchSchema = (endpoint) => {
|
||||
const introspectionQuery = getIntrospectionQuery();
|
||||
const queryParams = {
|
||||
query: introspectionQuery
|
||||
};
|
||||
|
||||
return fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(queryParams)
|
||||
});
|
||||
}
|
||||
|
||||
const useGraphqlSchema = (endpoint) => {
|
||||
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);
|
||||
fetchSchema(endpoint)
|
||||
.then((res) => res.json())
|
||||
.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 occured while loading Graphql Schema');
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
isLoading,
|
||||
schema,
|
||||
loadSchema,
|
||||
error
|
||||
};
|
||||
};
|
||||
|
||||
export default useGraphqlSchema;
|
@ -5,12 +5,30 @@ const StyledWrapper = styled.div`
|
||||
background: ${(props) => props.theme.codemirror.bg};
|
||||
border: solid 1px ${(props) => props.theme.codemirror.border};
|
||||
/* todo: find a better way */
|
||||
height: calc(100vh - 250px);
|
||||
height: calc(100vh - 220px);
|
||||
}
|
||||
|
||||
textarea.cm-editor {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
// Todo: dark mode temporary fix
|
||||
// Clean this
|
||||
.cm-s-monokai span.cm-property, .cm-s-monokai span.cm-attribute {
|
||||
color: #9cdcfe !important;
|
||||
}
|
||||
|
||||
.cm-s-monokai span.cm-string {
|
||||
color: #ce9178 !important;
|
||||
}
|
||||
|
||||
.cm-s-monokai span.cm-number{
|
||||
color: #b5cea8 !important;
|
||||
}
|
||||
|
||||
.cm-s-monokai span.cm-atom{
|
||||
color: #569cd6 !important;
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
|
@ -38,6 +38,7 @@ export default class QueryEditor extends React.Component {
|
||||
tabSize: 2,
|
||||
mode: 'graphql',
|
||||
theme: this.props.editorTheme || 'graphiql',
|
||||
theme: this.props.theme === 'dark' ? 'monokai' : 'default',
|
||||
keyMap: 'sublime',
|
||||
autoCloseBrackets: true,
|
||||
matchBrackets: true,
|
||||
@ -75,54 +76,51 @@ export default class QueryEditor extends React.Component {
|
||||
'Alt-Space': () => editor.showHint({ completeSingle: true, container: this._node }),
|
||||
'Shift-Space': () => editor.showHint({ completeSingle: true, container: this._node }),
|
||||
'Shift-Alt-Space': () => editor.showHint({ completeSingle: true, container: this._node }),
|
||||
|
||||
'Cmd-Enter': () => {
|
||||
if (this.props.onRunQuery) {
|
||||
this.props.onRunQuery();
|
||||
if (this.props.onRun) {
|
||||
this.props.onRun();
|
||||
}
|
||||
},
|
||||
'Ctrl-Enter': () => {
|
||||
if (this.props.onRunQuery) {
|
||||
this.props.onRunQuery();
|
||||
if (this.props.onRun) {
|
||||
this.props.onRun();
|
||||
}
|
||||
},
|
||||
|
||||
'Shift-Ctrl-C': () => {
|
||||
if (this.props.onCopyQuery) {
|
||||
this.props.onCopyQuery();
|
||||
}
|
||||
},
|
||||
|
||||
'Shift-Ctrl-P': () => {
|
||||
if (this.props.onPrettifyQuery) {
|
||||
this.props.onPrettifyQuery();
|
||||
}
|
||||
},
|
||||
|
||||
/* Shift-Ctrl-P is hard coded in Firefox for private browsing so adding an alternative to Pretiffy */
|
||||
|
||||
'Shift-Ctrl-F': () => {
|
||||
if (this.props.onPrettifyQuery) {
|
||||
this.props.onPrettifyQuery();
|
||||
}
|
||||
},
|
||||
|
||||
'Shift-Ctrl-M': () => {
|
||||
if (this.props.onMergeQuery) {
|
||||
this.props.onMergeQuery();
|
||||
}
|
||||
},
|
||||
'Cmd-S': () => {
|
||||
if (this.props.onRunQuery) {
|
||||
// empty
|
||||
if (this.props.onSave) {
|
||||
this.props.onSave();
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
'Ctrl-S': () => {
|
||||
if (this.props.onRunQuery) {
|
||||
// empty
|
||||
if (this.props.onSave) {
|
||||
this.props.onSave();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
},
|
||||
'Cmd-F': 'findPersistent',
|
||||
'Ctrl-F': 'findPersistent'
|
||||
}
|
||||
}));
|
||||
if (editor) {
|
||||
@ -149,6 +147,10 @@ export default class QueryEditor extends React.Component {
|
||||
this.cachedValue = this.props.value;
|
||||
this.editor.setValue(this.props.value);
|
||||
}
|
||||
|
||||
if (this.props.theme !== prevProps.theme && this.editor) {
|
||||
this.editor.setOption('theme', this.props.theme === 'dark' ? 'monokai' : 'default');
|
||||
}
|
||||
this.ignoreChangeEvent = false;
|
||||
}
|
||||
|
||||
@ -164,7 +166,7 @@ export default class QueryEditor extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<StyledWrapper
|
||||
className="h-full"
|
||||
className="h-full w-full"
|
||||
aria-label="Query Editor"
|
||||
ref={(node) => {
|
||||
this._node = node;
|
||||
@ -173,8 +175,11 @@ export default class QueryEditor extends React.Component {
|
||||
);
|
||||
}
|
||||
|
||||
_onKeyUp = (_cm, event) => {
|
||||
if (AUTO_COMPLETE_AFTER_KEY.test(event.key) && this.editor) {
|
||||
_onKeyUp = (_cm, e) => {
|
||||
if (e.metaKey || e.ctrlKey || e.altKey) {
|
||||
return;
|
||||
}
|
||||
if (AUTO_COMPLETE_AFTER_KEY.test(e.key) && this.editor) {
|
||||
this.editor.execCommand('autocomplete');
|
||||
}
|
||||
};
|
||||
|
@ -11,7 +11,7 @@ const QueryUrl = ({ item, collection, handleRun }) => {
|
||||
const { theme } = useTheme();
|
||||
const dispatch = useDispatch();
|
||||
const method = item.draft ? get(item, 'draft.request.method') : get(item, 'request.method');
|
||||
let url = item.draft ? get(item, 'draft.request.url') : get(item, 'request.url');
|
||||
const url = item.draft ? get(item, 'draft.request.url') : get(item, 'request.url');
|
||||
|
||||
const onUrlChange = (value) => {
|
||||
dispatch(
|
||||
|
@ -12,7 +12,6 @@ import { sendRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import RequestNotFound from './RequestNotFound';
|
||||
import QueryUrl from 'components/RequestPane/QueryUrl';
|
||||
import NetworkError from 'components/ResponsePane/NetworkError';
|
||||
import useGraphqlSchema from '../../hooks/useGraphqlSchema';
|
||||
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
@ -65,11 +64,6 @@ const RequestTabPanel = () => {
|
||||
setDragging(true);
|
||||
};
|
||||
|
||||
let schema = null;
|
||||
// let {
|
||||
// schema
|
||||
// } = useGraphqlSchema('https://api.spacex.land/graphql');
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('mouseup', handleMouseUp);
|
||||
document.addEventListener('mousemove', handleMouseMove);
|
||||
@ -105,8 +99,6 @@ const RequestTabPanel = () => {
|
||||
})
|
||||
);
|
||||
};
|
||||
const onGraphqlQueryChange = (value) => {};
|
||||
const runQuery = async () => {};
|
||||
|
||||
return (
|
||||
<StyledWrapper className={`flex flex-col flex-grow ${dragging ? 'dragging' : ''}`}>
|
||||
@ -118,11 +110,9 @@ const RequestTabPanel = () => {
|
||||
<div className="px-4" style={{ width: `${leftPaneWidth}px`, height: 'calc(100% - 5px)' }}>
|
||||
{item.type === 'graphql-request' ? (
|
||||
<GraphQLRequestPane
|
||||
onRunQuery={runQuery}
|
||||
schema={schema}
|
||||
item={item}
|
||||
collection={collection}
|
||||
leftPaneWidth={leftPaneWidth}
|
||||
value={item.request.body.graphql.query}
|
||||
onQueryChange={onGraphqlQueryChange}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
|
@ -15,6 +15,7 @@ import CloneCollectionItem from './CloneCollectionItem';
|
||||
import DeleteCollectionItem from './DeleteCollectionItem';
|
||||
import { isItemARequest, isItemAFolder, itemIsOpenedInTabs } from 'utils/tabs';
|
||||
import { doesRequestMatchSearchText, doesFolderHaveItemsMatchSearchText } from 'utils/collections/search';
|
||||
import { getDefaultRequestPaneTab } from 'utils/collections';
|
||||
import { hideHomePage } from 'providers/ReduxStore/slices/app';
|
||||
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
@ -69,7 +70,8 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
||||
dispatch(
|
||||
addTab({
|
||||
uid: item.uid,
|
||||
collectionUid: collection.uid
|
||||
collectionUid: collection.uid,
|
||||
requestPaneTab: getDefaultRequestPaneTab(item)
|
||||
})
|
||||
);
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ import { newEphermalHttpRequest } from 'providers/ReduxStore/slices/collections'
|
||||
import { newHttpRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { addTab } from 'providers/ReduxStore/slices/tabs';
|
||||
import HttpMethodSelector from 'components/RequestPane/QueryUrl/HttpMethodSelector';
|
||||
import { getDefaultRequestPaneTab } from 'utils/collections';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const NewRequest = ({ collection, item, isEphermal, onClose }) => {
|
||||
@ -42,7 +43,8 @@ const NewRequest = ({ collection, item, isEphermal, onClose }) => {
|
||||
dispatch(
|
||||
addTab({
|
||||
uid: uid,
|
||||
collectionUid: collection.uid
|
||||
collectionUid: collection.uid,
|
||||
requestPaneTab: getDefaultRequestPaneTab({type: values.requestType})
|
||||
})
|
||||
);
|
||||
onClose();
|
||||
@ -77,27 +79,27 @@ const NewRequest = ({ collection, item, isEphermal, onClose }) => {
|
||||
<StyledWrapper>
|
||||
<Modal size="md" title="New Request" confirmText="Create" handleConfirm={onSubmit} handleCancel={onClose}>
|
||||
<form className="bruno-form" onSubmit={formik.handleSubmit}>
|
||||
<div className="hidden">
|
||||
<div>
|
||||
<label htmlFor="requestName" className="block font-semibold">
|
||||
Type
|
||||
</label>
|
||||
|
||||
<div className="flex items-center mt-2">
|
||||
<input
|
||||
id="http"
|
||||
id="http-request"
|
||||
className="cursor-pointer"
|
||||
type="radio"
|
||||
name="requestType"
|
||||
onChange={formik.handleChange}
|
||||
value="http"
|
||||
value="http-request"
|
||||
checked={formik.values.requestType === 'http-request'}
|
||||
/>
|
||||
<label htmlFor="http" className="ml-1 cursor-pointer select-none">
|
||||
<label htmlFor="http-request" className="ml-1 cursor-pointer select-none">
|
||||
Http
|
||||
</label>
|
||||
|
||||
<input
|
||||
id="graphql"
|
||||
id="graphql-request"
|
||||
className="ml-4 cursor-pointer"
|
||||
type="radio"
|
||||
name="requestType"
|
||||
@ -105,16 +107,16 @@ const NewRequest = ({ collection, item, isEphermal, onClose }) => {
|
||||
formik.setFieldValue('requestMethod', 'POST');
|
||||
formik.handleChange(event);
|
||||
}}
|
||||
value="graphql"
|
||||
value="graphql-request"
|
||||
checked={formik.values.requestType === 'graphql-request'}
|
||||
/>
|
||||
<label htmlFor="graphql" className="ml-1 cursor-pointer select-none">
|
||||
<label htmlFor="graphql-request" className="ml-1 cursor-pointer select-none">
|
||||
Graphql
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="mt-4">
|
||||
<label htmlFor="requestName" className="block font-semibold">
|
||||
Name
|
||||
</label>
|
||||
|
@ -54,7 +54,7 @@ const Wrapper = styled.div`
|
||||
&:hover div.drag-request-border {
|
||||
width: 2px;
|
||||
height: 100%;
|
||||
border-left: solid 1px var(--color-request-dragbar-background-active);
|
||||
border-left: solid 1px ${(props) => props.theme.sidebar.dragbar};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
@ -22,6 +22,10 @@ const GlobalStyle = createGlobalStyle`
|
||||
padding: .215rem .6rem .215rem .6rem;
|
||||
}
|
||||
|
||||
.btn-xs {
|
||||
padding: .2rem .4rem .2rem .4rem;
|
||||
}
|
||||
|
||||
.btn-md {
|
||||
padding: .4rem 1.1rem;
|
||||
line-height: 1.47;
|
||||
|
@ -1,44 +0,0 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { getIntrospectionQuery, buildClientSchema } from 'graphql';
|
||||
|
||||
const useGraphqlSchema = (endpoint) => {
|
||||
const [isLoaded, setIsLoaded] = useState(false);
|
||||
const [schema, setSchema] = useState(null);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
const introspectionQuery = getIntrospectionQuery();
|
||||
const queryParams = {
|
||||
query: introspectionQuery
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(queryParams)
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((s) => {
|
||||
if (s && s.data) {
|
||||
setSchema(buildClientSchema(s.data));
|
||||
setIsLoaded(true);
|
||||
} else {
|
||||
return Promise.reject(new Error('An error occurred while introspecting schema'));
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
setError(err);
|
||||
});
|
||||
}, []);
|
||||
|
||||
return {
|
||||
isLoaded,
|
||||
schema,
|
||||
error
|
||||
};
|
||||
};
|
||||
|
||||
export default useGraphqlSchema;
|
@ -3,8 +3,8 @@ import { createSlice } from '@reduxjs/toolkit';
|
||||
const initialState = {
|
||||
isDragging: false,
|
||||
idbConnectionReady: false,
|
||||
leftSidebarWidth: 270,
|
||||
leftMenuBarOpen: true,
|
||||
leftSidebarWidth: 222,
|
||||
leftMenuBarOpen: false,
|
||||
screenWidth: 500,
|
||||
showHomePage: false
|
||||
};
|
||||
|
@ -16,7 +16,8 @@ import {
|
||||
findEnvironmentInCollection,
|
||||
isItemAFolder,
|
||||
refreshUidsInItem,
|
||||
interpolateEnvironmentVars
|
||||
interpolateEnvironmentVars,
|
||||
getDefaultRequestPaneTab
|
||||
} from 'utils/collections';
|
||||
import { collectionSchema, itemSchema, environmentsSchema } from '@usebruno/schema';
|
||||
import { waitForNextTick } from 'utils/common';
|
||||
@ -108,7 +109,8 @@ export const createCollection = (collectionName) => (dispatch, getState) => {
|
||||
dispatch(
|
||||
addTab({
|
||||
uid: requestItem.uid,
|
||||
collectionUid: newCollection.uid
|
||||
collectionUid: newCollection.uid,
|
||||
requestPaneTab: getDefaultRequestPaneTab(requestItem)
|
||||
})
|
||||
)
|
||||
)
|
||||
@ -635,7 +637,8 @@ export const newHttpRequest = (params) => (dispatch, getState) => {
|
||||
dispatch(
|
||||
addTab({
|
||||
uid: item.uid,
|
||||
collectionUid: collection.uid
|
||||
collectionUid: collection.uid,
|
||||
requestPaneTab: getDefaultRequestPaneTab(item)
|
||||
})
|
||||
);
|
||||
})
|
||||
|
@ -606,6 +606,22 @@ export const collectionsSlice = createSlice({
|
||||
}
|
||||
}
|
||||
},
|
||||
updateRequestGraphqlQuery: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
|
||||
if (collection) {
|
||||
const item = findItemInCollection(collection, action.payload.itemUid);
|
||||
|
||||
if (item && isItemARequest(item)) {
|
||||
if (!item.draft) {
|
||||
item.draft = cloneDeep(item);
|
||||
}
|
||||
item.draft.request.body.mode = 'graphql';
|
||||
item.draft.request.body.graphql = item.draft.request.body.graphql || {};
|
||||
item.draft.request.body.graphql.query = action.payload.query;
|
||||
}
|
||||
}
|
||||
},
|
||||
updateRequestMethod: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
|
||||
@ -791,6 +807,7 @@ export const {
|
||||
deleteMultipartFormParam,
|
||||
updateRequestBodyMode,
|
||||
updateRequestBody,
|
||||
updateRequestGraphqlQuery,
|
||||
updateRequestMethod,
|
||||
localCollectionAddFileEvent,
|
||||
localCollectionAddDirectoryEvent,
|
||||
|
@ -23,7 +23,7 @@ export const tabsSlice = createSlice({
|
||||
uid: action.payload.uid,
|
||||
collectionUid: action.payload.collectionUid,
|
||||
requestPaneWidth: null,
|
||||
requestPaneTab: 'params',
|
||||
requestPaneTab: action.payload.requestPaneTab || 'params',
|
||||
responsePaneTab: 'response'
|
||||
});
|
||||
state.activeTabUid = action.payload.uid;
|
||||
|
@ -22,6 +22,7 @@ const darkTheme = {
|
||||
color: '#ccc',
|
||||
muted: '#9d9d9d',
|
||||
bg: '#252526',
|
||||
dragbar: '#8a8a8a',
|
||||
|
||||
workspace: {
|
||||
bg: '#3D3D3D'
|
||||
|
@ -22,6 +22,7 @@ const lightTheme = {
|
||||
color: 'rgb(52, 52, 52)',
|
||||
muted: '#4b5563',
|
||||
bg: '#F3F3F3',
|
||||
dragbar: 'rgb(200, 200, 200)',
|
||||
|
||||
workspace: {
|
||||
bg: '#e1e1e1'
|
||||
|
@ -196,7 +196,7 @@ export const transformCollectionToSaveToIdb = (collection, options = {}) => {
|
||||
json: si.draft.request.body.json,
|
||||
text: si.draft.request.body.text,
|
||||
xml: si.draft.request.body.xml,
|
||||
multipartForm: si.draft.request.body.multipartForm,
|
||||
graphql: si.draft.request.body.graphql,
|
||||
formUrlEncoded: copyFormUrlEncodedParams(si.draft.request.body.formUrlEncoded),
|
||||
multipartForm: copyMultipartFormParams(si.draft.request.body.multipartForm)
|
||||
}
|
||||
@ -214,6 +214,7 @@ export const transformCollectionToSaveToIdb = (collection, options = {}) => {
|
||||
json: si.request.body.json,
|
||||
text: si.request.body.text,
|
||||
xml: si.request.body.xml,
|
||||
graphql: si.request.body.graphql,
|
||||
formUrlEncoded: copyFormUrlEncodedParams(si.request.body.formUrlEncoded),
|
||||
multipartForm: copyMultipartFormParams(si.request.body.multipartForm)
|
||||
}
|
||||
@ -410,3 +411,13 @@ export const interpolateEnvironmentVars = (item, variables) => {
|
||||
|
||||
return request;
|
||||
};
|
||||
|
||||
export const getDefaultRequestPaneTab = (item) => {
|
||||
if(item.type === 'http-request') {
|
||||
return 'params';
|
||||
}
|
||||
|
||||
if(item.type === 'graphql-request') {
|
||||
return 'query';
|
||||
}
|
||||
};
|
||||
|
@ -1,14 +1,14 @@
|
||||
import get from 'lodash/get';
|
||||
import each from 'lodash/each';
|
||||
import filter from 'lodash/filter';
|
||||
import qs from 'qs';
|
||||
import { rawRequest, gql } from 'graphql';
|
||||
import { sendHttpRequestInBrowser } from './browser';
|
||||
import { isElectron } from 'utils/common/platform';
|
||||
import cancelTokens, { deleteCancelToken } from 'utils/network/cancelTokens';
|
||||
|
||||
export const sendNetworkRequest = async (item, options) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (item.type === 'http-request') {
|
||||
if (['http-request', 'graphql-request'].includes(item.type)) {
|
||||
const timeStart = Date.now();
|
||||
sendHttpRequest(item.draft ? item.draft.request : item.request, options)
|
||||
.then((response) => {
|
||||
@ -79,6 +79,15 @@ const sendHttpRequest = async (request, options) => {
|
||||
axiosRequest.data = params;
|
||||
}
|
||||
|
||||
if (request.body.mode === 'graphql') {
|
||||
const graphqlQuery = {
|
||||
query: get(request, 'body.graphql.query'),
|
||||
variables: JSON.parse(get(request, 'body.graphql.variables') || '{}')
|
||||
};
|
||||
axiosRequest.headers['content-type'] = 'application/json';
|
||||
axiosRequest.data = graphqlQuery;
|
||||
}
|
||||
|
||||
console.log('>>> Sending Request');
|
||||
console.log(axiosRequest);
|
||||
|
||||
@ -90,21 +99,6 @@ const sendHttpRequest = async (request, options) => {
|
||||
});
|
||||
};
|
||||
|
||||
const sendGraphqlRequest = async (request) => {
|
||||
const query = gql`
|
||||
${request.request.body.graphql.query}
|
||||
`;
|
||||
|
||||
const { data, errors, extensions, headers, status } = await rawRequest(request.request.url, query);
|
||||
|
||||
return {
|
||||
data,
|
||||
headers,
|
||||
data,
|
||||
errors
|
||||
};
|
||||
};
|
||||
|
||||
export const cancelNetworkRequest = async (cancelTokenUid) => {
|
||||
if (isElectron()) {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
@ -29,13 +29,19 @@ const keyValueSchema = Yup.object({
|
||||
const requestUrlSchema = Yup.string().min(0).max(2048, 'name must be 2048 characters or less').defined();
|
||||
const requestMethodSchema = Yup.string().oneOf(['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD']).required('method is required');
|
||||
|
||||
const graphqlBodySchema = Yup.object({
|
||||
query: Yup.string().max(10240, 'json must be 10240 characters or less').nullable(),
|
||||
variables: Yup.string().max(10240, 'text must be 10240 characters or less').nullable(),
|
||||
}).noUnknown(true).strict();
|
||||
|
||||
const requestBodySchema = Yup.object({
|
||||
mode: Yup.string().oneOf(['none', 'json', 'text', 'xml', 'formUrlEncoded', 'multipartForm']).required('mode is required'),
|
||||
mode: Yup.string().oneOf(['none', 'json', 'text', 'xml', 'formUrlEncoded', 'multipartForm', 'graphql']).required('mode is required'),
|
||||
json: Yup.string().max(10240, 'json must be 10240 characters or less').nullable(),
|
||||
text: Yup.string().max(10240, 'text must be 10240 characters or less').nullable(),
|
||||
xml: Yup.string().max(10240, 'xml must be 10240 characters or less').nullable(),
|
||||
formUrlEncoded: Yup.array().of(keyValueSchema).nullable(),
|
||||
multipartForm: Yup.array().of(keyValueSchema).nullable(),
|
||||
graphql: graphqlBodySchema.nullable(),
|
||||
}).noUnknown(true).strict();
|
||||
|
||||
// Right now, the request schema is very tightly coupled with http request
|
||||
|
Loading…
Reference in New Issue
Block a user