Created
July 28, 2021 21:20
-
-
Save ggascoigne/7160ba6c4b52b7f615b3df2f5a68287d to your computer and use it in GitHub Desktop.
GraphiQl editor with Schema Reloading - also uses GraphiqlExplorer
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'graphiql/graphiql.css' | |
import { Trans, t } from '@lingui/macro' | |
import { | |
InputLabel, | |
MenuItem, | |
Select, | |
createStyles, | |
makeStyles, | |
} from '@material-ui/core' | |
import { i18n } from '@tw/common/I18nLoader' | |
import classNames from 'classnames' | |
import RealGraphiQL from 'graphiql' | |
import GraphiQLExplorer from 'graphiql-explorer' | |
import { | |
GraphQLSchema, | |
buildClientSchema, | |
getIntrospectionQuery, | |
parse, | |
} from 'graphql' | |
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' | |
import { useSelector } from 'react-redux' | |
type ApiOption = { text: string; value: string; hidden?: boolean } | |
const apis: Array<ApiOption>= [ | |
{ | |
text: 'Custom', | |
value: 'http://localhost:4000/graphql', | |
}, | |
// we have a lot of separate apis, normally they are listed here | |
] | |
export const useStyles = makeStyles( | |
createStyles({ | |
box: { | |
boxSizing: 'content-box', | |
display: 'flex', | |
flexDirection: 'row', | |
width: '100%', | |
}, | |
'@global': { | |
'.graphiql-explorer-root': { | |
overflow: 'unset !important', | |
padding: '0 !important', | |
}, | |
'.graphiql-explorer-root > :first-child': { | |
padding: '8px 8px 0 8px', | |
overflowX: 'hidden !important', | |
}, | |
'.graphiql-explorer-root > :nth-child(2)': { | |
padding: '0px 8px 0 8px', | |
}, | |
'.graphiql-container .historyPaneWrap': { | |
width: '300px !important', | |
boxShadow: 'none !important', | |
}, | |
}, | |
apiLabel: { | |
padding: '0.5em 0.6em 0 1em', | |
}, | |
}) | |
) | |
type Props = { auth: any } | |
const GraphiQL: React.FC<Props> = (props) => { | |
const _graphiql = useRef(null) | |
const [schema, setSchema] = useState<GraphQLSchema | null>(null) | |
const [query, setQuery] = useState<string>('') | |
const [explorerIsOpen, setExplorerIsOpen] = useState<boolean>(true) | |
const [selectedApi, setSelectedApi] = useState(apis[0].value) | |
const classes = useStyles({}) | |
const { | |
auth: { jwtToken }, | |
} = props | |
const graphQLFetcher = useCallback( | |
(jwtToken) => (graphQLParams) => | |
fetch(selectedApi, { | |
method: 'post', | |
headers: { | |
'X-Requested-With': 'graphiql', | |
'Content-Type': 'application/json', | |
Authorization: `Bearer ${jwtToken}`, | |
}, | |
body: JSON.stringify(graphQLParams), | |
}).then((response) => response.json()), | |
[selectedApi] | |
) | |
const handleInspectOperation = useCallback( | |
(cm: any, mousePos: { line: number; ch: number }) => { | |
const parsedQuery = parse(query || '') | |
if (!parsedQuery) { | |
console.error("Couldn't parse query document") | |
return null | |
} | |
const token = cm.getTokenAt(mousePos) | |
const start = { line: mousePos.line, ch: token.start } | |
const end = { line: mousePos.line, ch: token.end } | |
const relevantMousePos = { | |
start: cm.indexFromPos(start), | |
end: cm.indexFromPos(end), | |
} | |
const position = relevantMousePos | |
const def = parsedQuery.definitions.find((definition) => { | |
if (!definition.loc) { | |
console.log('Missing location information for definition') | |
return false | |
} | |
const { start, end } = definition.loc | |
return start <= position.start && end >= position.end | |
}) | |
if (!def) { | |
console.error( | |
'Unable to find definition corresponding to mouse position' | |
) | |
return null | |
} | |
const operationKind = | |
def.kind === 'OperationDefinition' | |
? def.operation | |
: def.kind === 'FragmentDefinition' | |
? 'fragment' | |
: 'unknown' | |
const operationName = | |
def.kind === 'OperationDefinition' && !!def.name | |
? def.name.value | |
: def.kind === 'FragmentDefinition' && !!def.name | |
? def.name.value | |
: 'unknown' | |
const selector = `.graphiql-explorer-root #${operationKind}-${operationName}` | |
const el = document.querySelector(selector) | |
el && el.scrollIntoView() | |
}, | |
[query] | |
) | |
const reloadSchema = useCallback(() => { | |
graphQLFetcher(jwtToken)({ | |
query: getIntrospectionQuery(), | |
}).then((result) => { | |
const editor = _graphiql.current.getQueryEditor() | |
editor && | |
editor.setOption('extraKeys', { | |
...(editor.options.extraKeys || {}), | |
'Shift-Alt-LeftClick': handleInspectOperation, | |
}) | |
setSchema(buildClientSchema(result.data)) | |
}) | |
}, [graphQLFetcher, handleInspectOperation, jwtToken]) | |
useEffect(() => { | |
reloadSchema() | |
}, [reloadSchema]) | |
const handleEditQuery = useCallback((query: string) => setQuery(query), []) | |
const handleToggleExplorer = useCallback(() => { | |
setExplorerIsOpen((old) => !old) | |
}, []) | |
const handleApiChange = useCallback((_, data) => { | |
setSelectedApi(data.props.value) | |
console.log(`setting selectedApi = ${data.props.value}`) | |
}, []) | |
const renderApiValue = useCallback( | |
(value: string) => apis.find((c) => value === c.value).text, | |
[apis] | |
) | |
return ( | |
<div className={classNames(classes.box, 'graphiql-container')}> | |
<GraphiQLExplorer | |
schema={schema} | |
query={query} | |
onEdit={handleEditQuery} | |
onRunOperation={(operationName: string) => | |
_graphiql.current.handleRunQuery(operationName) | |
} | |
explorerIsOpen={explorerIsOpen} | |
onToggleExplorer={handleToggleExplorer} | |
/> | |
<RealGraphiQL | |
ref={_graphiql} | |
fetcher={graphQLFetcher(jwtToken)} | |
schema={schema} | |
query={query} | |
onEditQuery={handleEditQuery} | |
> | |
<RealGraphiQL.Toolbar> | |
<RealGraphiQL.Button | |
onClick={() => _graphiql.current.handlePrettifyQuery()} | |
label={i18n._(t`Prettify`)} | |
title={i18n._(t`Prettify Query (Shift-Ctrl-P)`)} | |
/> | |
<RealGraphiQL.Button | |
onClick={() => _graphiql.current.handleMergeQuery()} | |
title={i18n._(t`Merge Query (Shift-Ctrl-M)`)} | |
label={i18n._(t`Merge`)} | |
/> | |
<RealGraphiQL.Button | |
onClick={reloadSchema} | |
title={i18n._(t`Reload Schema`)} | |
label={i18n._(t`Reload`)} | |
/>{' '} | |
<RealGraphiQL.Button | |
onClick={() => _graphiql.current.handleToggleHistory()} | |
label={i18n._(t`History`)} | |
title={i18n._(t`Show History`)} | |
/> | |
<RealGraphiQL.Button | |
onClick={handleToggleExplorer} | |
label={i18n._(t`Explorer`)} | |
title={i18n._(t`Toggle Explorer`)} | |
/> | |
<InputLabel className={classes.apiLabel}> | |
<Trans>Api</Trans> | |
</InputLabel> | |
<Select | |
disableUnderline | |
name="apiSelect" | |
onChange={handleApiChange} | |
renderValue={renderApiValue} | |
value={selectedApi} | |
> | |
{apis.map((option, index) => ( | |
<MenuItem key={index} value={option.value}> | |
{option.text} | |
</MenuItem> | |
))} | |
</Select> | |
</RealGraphiQL.Toolbar> | |
</RealGraphiQL> | |
</div> | |
) | |
} | |
export default GraphiQL |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment