---
title: API Usage
---
import {GraphiQLWrapper} from "@/components/graphiql/wrapper"
## Query 1
<GraphiQLWrapper className="max-h-screen! h-[calc(--spacing(120))]!" url="http://localhost:4000/schema" query={`{
listRecords {
pageInfo {
hasNextPage
endCursor
}
edges {
node {
id
name
}
}
}
}
`} />
## Query 2
<GraphiQLWrapper className="max-h-screen! h-[calc(--spacing(120))]!" url="http://localhost:4000/schema" query={`{
listRecords(where: { status: { equals: active } }) {
pageInfo {
hasNextPage
endCursor
}
edges {
node {
id
name
}
}
}
}
`} />
Last active
July 3, 2025 09:14
-
-
Save noxify/11c71447229fb858fceef87c63496dcc to your computer and use it in GitHub Desktop.
Custom GraphiQL client
This file contains hidden or 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 type { ReactNode } from "react" | |
import { createElement, Fragment, useEffect, useState } from "react" | |
export const ClientOnly = ({ children }: { children: ReactNode }) => { | |
const hasMounted = useClientOnly() | |
if (!hasMounted) { | |
return null | |
} | |
return createElement(Fragment, { children }) | |
} | |
/** React hook that returns true if the component has mounted client-side */ | |
export const useClientOnly = () => { | |
const [hasMounted, setHasMounted] = useState(false) | |
useEffect(() => { | |
setHasMounted(true) | |
}, []) | |
return hasMounted | |
} |
This file contains hidden or 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
"use client" | |
/* eslint-disable @typescript-eslint/unbound-method */ | |
/** | |
* Copyright (c) 2020 GraphQL Contributors. | |
* | |
* This source code is licensed under the MIT license found in the | |
* LICENSE file in the root directory of this source tree. | |
*/ | |
import type { | |
EditorProps, | |
HeaderEditor, | |
ResponseEditor, | |
VariableEditor, | |
} from "@graphiql/react" | |
import type { | |
ComponentPropsWithoutRef, | |
FC, | |
// MouseEventHandler, | |
ReactNode, | |
} from "react" | |
import { | |
Children, | |
Fragment /*useEffect, useId, useRef, useState*/, | |
} from "react" | |
import { | |
DOC_EXPLORER_PLUGIN, | |
DocExplorerStore, | |
} from "@graphiql/plugin-doc-explorer" | |
import { HISTORY_PLUGIN, HistoryStore } from "@graphiql/plugin-history" | |
import { | |
// ChevronDownIcon, | |
// ChevronUpIcon, | |
cn, | |
// ExecuteButton, | |
GraphiQLProvider, | |
pick, | |
// PlusIcon, | |
QueryEditor, | |
// Spinner, | |
// Tab, | |
// Tabs, | |
Tooltip, | |
// UnStyledButton, | |
useDragResize, | |
useGraphiQL, | |
// useGraphiQLActions, | |
} from "@graphiql/react" | |
import { ClientOnly } from "../client-only" | |
import { GraphiQLFooter, GraphiQLLogo, GraphiQLToolbar, Sidebar } from "./ui" | |
/** | |
* API docs for this live here: | |
* | |
* https://graphiql-test.netlify.app/typedoc/modules/graphiql.html#graphiqlprops | |
*/ | |
export interface GraphiQLProps | |
// `children` prop should be optional | |
extends GraphiQLInterfaceProps, | |
Omit<ComponentPropsWithoutRef<typeof GraphiQLProvider>, "children">, | |
Omit<ComponentPropsWithoutRef<typeof HistoryStore>, "children"> {} | |
/** | |
* The top-level React component for GraphiQL, intended to encompass the entire | |
* browser viewport. | |
* | |
* @see https://github.com/graphql/graphiql#usage | |
*/ | |
const GraphiQL_: FC<GraphiQLProps> = ({ | |
maxHistoryLength, | |
plugins = [HISTORY_PLUGIN], | |
referencePlugin = DOC_EXPLORER_PLUGIN, | |
onEditQuery, | |
onEditVariables, | |
onEditHeaders, | |
responseTooltip, | |
defaultEditorToolsVisibility, | |
isHeadersEditorEnabled, | |
showPersistHeadersSettings, | |
forcedTheme, | |
confirmCloseTab, | |
className, | |
children, | |
...props | |
}) => { | |
// @ts-expect-error -- Prop is removed | |
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access | |
if (props.toolbar?.additionalContent) { | |
throw new TypeError( | |
"The `toolbar.additionalContent` prop has been removed. Use render props on `GraphiQL.Toolbar` component instead.", | |
) | |
} | |
// @ts-expect-error -- Prop is removed | |
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access | |
if (props.toolbar?.additionalComponent) { | |
throw new TypeError( | |
"The `toolbar.additionalComponent` prop has been removed. Use render props on `GraphiQL.Toolbar` component instead.", | |
) | |
} | |
// @ts-expect-error -- Prop is removed | |
if (props.keyMap) { | |
throw new TypeError( | |
"`keyMap` was removed. To use Vim or Emacs keybindings in Monaco, you can use community plugins. Monaco Vim: https://github.com/brijeshb42/monaco-vim. Monaco Emacs: https://github.com/aioutecism/monaco-emacs", | |
) | |
} | |
// @ts-expect-error -- Prop is removed | |
if (props.readOnly) { | |
throw new TypeError("The `readOnly` prop has been removed.") | |
} | |
const interfaceProps: GraphiQLInterfaceProps = { | |
// TODO check if `showPersistHeadersSettings` prop is needed, or we can just use `shouldPersistHeaders` instead | |
showPersistHeadersSettings: | |
showPersistHeadersSettings ?? props.shouldPersistHeaders !== false, | |
onEditQuery, | |
onEditVariables, | |
onEditHeaders, | |
responseTooltip, | |
defaultEditorToolsVisibility, | |
isHeadersEditorEnabled, | |
forcedTheme, | |
confirmCloseTab, | |
className, | |
} | |
const hasHistoryPlugin = plugins.includes(HISTORY_PLUGIN) | |
const HistoryToUse = hasHistoryPlugin ? HistoryStore : Fragment | |
const DocExplorerToUse = | |
referencePlugin === DOC_EXPLORER_PLUGIN ? DocExplorerStore : Fragment | |
return ( | |
<GraphiQLProvider | |
plugins={[...(referencePlugin ? [referencePlugin] : []), ...plugins]} | |
referencePlugin={referencePlugin} | |
{...props} | |
> | |
{} | |
<HistoryToUse {...(hasHistoryPlugin && { maxHistoryLength })}> | |
<DocExplorerToUse> | |
<GraphiQLInterface {...interfaceProps}>{children}</GraphiQLInterface> | |
</DocExplorerToUse> | |
</HistoryToUse> | |
</GraphiQLProvider> | |
) | |
} | |
// eslint-disable-next-line @typescript-eslint/no-explicit-any | |
type AddSuffix<Obj extends Record<string, any>, Suffix extends string> = { | |
[Key in keyof Obj as `${string & Key}${Suffix}`]: Obj[Key] | |
} | |
type QueryEditorProps = ComponentPropsWithoutRef<typeof QueryEditor> | |
type VariableEditorProps = ComponentPropsWithoutRef<typeof VariableEditor> | |
type HeaderEditorProps = ComponentPropsWithoutRef<typeof HeaderEditor> | |
type ResponseEditorProps = ComponentPropsWithoutRef<typeof ResponseEditor> | |
interface GraphiQLInterfaceProps | |
extends EditorProps, | |
AddSuffix<Pick<QueryEditorProps, "onEdit">, "Query">, | |
AddSuffix<Pick<VariableEditorProps, "onEdit">, "Variables">, | |
AddSuffix<Pick<HeaderEditorProps, "onEdit">, "Headers">, | |
Pick<ResponseEditorProps, "responseTooltip">, | |
Pick< | |
ComponentPropsWithoutRef<typeof Sidebar>, | |
"forcedTheme" | "showPersistHeadersSettings" | |
> { | |
children?: ReactNode | |
/** | |
* Set the default state for the editor tools. | |
* - `false` hides the editor tools | |
* - `true` shows the editor tools | |
* - `'variables'` specifically shows the variables editor | |
* - `'headers'` specifically shows the headers editor | |
* By default, the editor tools are initially shown when at least one of the | |
* editors has contents. | |
*/ | |
defaultEditorToolsVisibility?: boolean | "variables" | "headers" | |
/** | |
* Toggle if the headers' editor should be shown inside the editor tools. | |
* @default true | |
*/ | |
isHeadersEditorEnabled?: boolean | |
/** | |
* Additional class names which will be appended to the container element. | |
*/ | |
className?: string | |
/** | |
* When the user clicks a close tab button, this function is invoked with | |
* the index of the tab that is about to be closed. It can return a promise | |
* that should resolve to `true` (meaning the tab may be closed) or `false` | |
* (meaning the tab may not be closed). | |
* @param index - The index of the tab that should be closed. | |
*/ | |
confirmCloseTab?(index: number): Promise<boolean> | boolean | |
} | |
// type ButtonHandler = MouseEventHandler<HTMLButtonElement> | |
// const LABEL = { | |
// newTab: "New tab", | |
// } | |
const GraphiQLInterface: FC<GraphiQLInterfaceProps> = ({ | |
forcedTheme, | |
// isHeadersEditorEnabled = true, | |
defaultEditorToolsVisibility, | |
children: $children, | |
// confirmCloseTab, | |
className, | |
onEditQuery, | |
//onEditVariables, | |
//onEditHeaders, | |
// responseTooltip, | |
showPersistHeadersSettings, | |
}) => { | |
// const { | |
// addTab, | |
// moveTab, | |
// closeTab, | |
// changeTab, | |
// setVisiblePlugin, | |
// updateActiveTabValues, | |
// storeTabs, | |
// } = useGraphiQLActions() | |
const TAB_CLASS_PREFIX = `graphiql-session-tab-` | |
const { | |
initialVariables, | |
initialHeaders, | |
// tabs, | |
activeTabIndex, | |
visiblePlugin, | |
} = useGraphiQL( | |
pick( | |
"initialVariables", | |
"initialHeaders", | |
"tabs", | |
"activeTabIndex", | |
"isFetching", | |
"visiblePlugin", | |
), | |
) | |
const PluginContent = visiblePlugin?.content | |
const pluginResize = useDragResize({ | |
defaultSizeRelation: 1 / 3, | |
direction: "horizontal", | |
initiallyHidden: visiblePlugin ? undefined : "first", | |
onHiddenElementChange(resizableElement) { | |
if (resizableElement === "first") { | |
//setVisiblePlugin(null) | |
} | |
}, | |
sizeThresholdSecond: 200, | |
storageKey: "docExplorerFlex", | |
}) | |
const editorResize = useDragResize({ | |
direction: "horizontal", | |
storageKey: "editorFlex", | |
}) | |
const editorToolsResize = useDragResize({ | |
defaultSizeRelation: 3, | |
direction: "vertical", | |
initiallyHidden: ((d: typeof defaultEditorToolsVisibility) => { | |
if (d === "variables" || d === "headers") { | |
return | |
} | |
if (typeof d === "boolean") { | |
return d ? undefined : "second" | |
} | |
return initialVariables || initialHeaders ? undefined : "second" | |
})(defaultEditorToolsVisibility), | |
sizeThresholdSecond: 60, | |
storageKey: "secondaryEditorFlex", | |
}) | |
// const [activeSecondaryEditor, setActiveSecondaryEditor] = useState< | |
// "variables" | "headers" | |
// >(() => { | |
// if ( | |
// defaultEditorToolsVisibility === "variables" || | |
// defaultEditorToolsVisibility === "headers" | |
// ) { | |
// return defaultEditorToolsVisibility | |
// } | |
// return !initialVariables && initialHeaders && isHeadersEditorEnabled | |
// ? "headers" | |
// : "variables" | |
// }) | |
const { logo, toolbar, /*footer,*/ children } = Children.toArray( | |
$children, | |
).reduce<{ | |
logo?: ReactNode | |
toolbar?: ReactNode | |
footer?: ReactNode | |
children: ReactNode[] | |
}>( | |
(acc, curr) => { | |
switch (getChildComponentType(curr)) { | |
case GraphiQL.Logo: | |
acc.logo = curr | |
break | |
case GraphiQL.Toolbar: | |
acc.toolbar = curr | |
break | |
case GraphiQL.Footer: | |
acc.footer = curr | |
break | |
default: | |
acc.children.push(curr) | |
} | |
return acc | |
}, | |
{ | |
logo: <GraphiQL.Logo />, | |
toolbar: <GraphiQL.Toolbar />, | |
children: [], | |
}, | |
) | |
function onClickReference() { | |
if (pluginResize.hiddenElement === "first") { | |
pluginResize.setHiddenElement(null) | |
} | |
} | |
// const handleToolsTabClick: ButtonHandler = (event) => { | |
// if (editorToolsResize.hiddenElement === "second") { | |
// editorToolsResize.setHiddenElement(null) | |
// } | |
// const tabName = event.currentTarget.dataset.name as "variables" | "headers" | |
// setActiveSecondaryEditor(tabName) | |
// } | |
// const toggleEditorTools: ButtonHandler = () => { | |
// editorToolsResize.setHiddenElement( | |
// editorToolsResize.hiddenElement === "second" ? null : "second", | |
// ) | |
// } | |
// // eslint-disable-next-line @typescript-eslint/no-misused-promises | |
// const handleTabClose: ButtonHandler = async (event) => { | |
// const tabButton = event.currentTarget.previousSibling as HTMLButtonElement | |
// const index = Number(tabButton.id.replace(TAB_CLASS_PREFIX, "")) | |
// const shouldCloseTab = confirmCloseTab ? await confirmCloseTab(index) : true | |
// if (!shouldCloseTab) { | |
// return | |
// } | |
// closeTab(index) | |
// } | |
// const handleTabClick: ButtonHandler = (event) => { | |
// const index = Number(event.currentTarget.id.replace(TAB_CLASS_PREFIX, "")) | |
// changeTab(index) | |
// } | |
// const editorToolsText = `${editorToolsResize.hiddenElement === "second" ? "Show" : "Hide"} editor tools` | |
// const EditorToolsIcon = | |
// editorToolsResize.hiddenElement === "second" | |
// ? ChevronUpIcon | |
// : ChevronDownIcon | |
const editors = ( | |
<div className="graphiql-editors" ref={editorResize.firstRef}> | |
<section | |
className="graphiql-query-editor" | |
aria-label="Query Editor" | |
ref={editorToolsResize.firstRef} | |
> | |
<ClientOnly> | |
<QueryEditor | |
onClickReference={onClickReference} | |
onEdit={onEditQuery} | |
/> | |
</ClientOnly> | |
<div | |
className="graphiql-toolbar" | |
role="toolbar" | |
aria-label="Editor Commands" | |
> | |
{/* <ExecuteButton /> */} | |
{toolbar} | |
</div> | |
</section> | |
{/* <div ref={editorToolsResize.dragBarRef} className="graphiql-editor-tools"> | |
<UnStyledButton | |
type="button" | |
className={cn( | |
activeSecondaryEditor === "variables" && | |
editorToolsResize.hiddenElement !== "second" && | |
"active", | |
)} | |
onClick={handleToolsTabClick} | |
data-name="variables" | |
> | |
Variables | |
</UnStyledButton> | |
{isHeadersEditorEnabled && ( | |
<UnStyledButton | |
type="button" | |
className={cn( | |
activeSecondaryEditor === "headers" && | |
editorToolsResize.hiddenElement !== "second" && | |
"active", | |
)} | |
onClick={handleToolsTabClick} | |
data-name="headers" | |
> | |
Headers | |
</UnStyledButton> | |
)} | |
<Tooltip label={editorToolsText}> | |
<UnStyledButton | |
type="button" | |
onClick={toggleEditorTools} | |
aria-label={editorToolsText} | |
className="graphiql-toggle-editor-tools" | |
> | |
<EditorToolsIcon | |
className="graphiql-chevron-icon" | |
aria-hidden="true" | |
/> | |
</UnStyledButton> | |
</Tooltip> | |
</div> */} | |
{/* <section | |
className="graphiql-editor-tool" | |
aria-label={ | |
activeSecondaryEditor === "variables" ? "Variables" : "Headers" | |
} | |
ref={editorToolsResize.secondRef} | |
> | |
<VariableEditor | |
className={activeSecondaryEditor === "variables" ? "" : "hidden"} | |
onEdit={onEditVariables} | |
/> | |
{isHeadersEditorEnabled && ( | |
<HeaderEditor | |
className={activeSecondaryEditor === "headers" ? "" : "hidden"} | |
onEdit={onEditHeaders} | |
/> | |
)} | |
</section> */} | |
</div> | |
) | |
// // eslint-disable-next-line @typescript-eslint/no-non-null-assertion | |
// const tabContainerRef = useRef<HTMLUListElement>(null!) | |
// customized the className handling, because we set the height in the MDX component | |
// to shrink the graphiql | |
const _className = className ? ` ${className}` : " h-[calc(100vh-30vh)]!" | |
return ( | |
<Tooltip.Provider> | |
<div className={cn("graphiql-container", _className)}> | |
<Sidebar | |
forcedTheme={forcedTheme} | |
showPersistHeadersSettings={showPersistHeadersSettings} | |
setHiddenElement={pluginResize.setHiddenElement} | |
/> | |
<div className="graphiql-main"> | |
<div | |
ref={pluginResize.firstRef} | |
className="graphiql-plugin" | |
style={{ | |
// Make sure the container shrinks when containing long | |
// non-breaking texts | |
minWidth: "200px", | |
}} | |
> | |
{PluginContent && <PluginContent />} | |
</div> | |
{visiblePlugin && ( | |
<div | |
className="graphiql-horizontal-drag-bar" | |
ref={pluginResize.dragBarRef} | |
/> | |
)} | |
<div ref={pluginResize.secondRef} className="graphiql-sessions"> | |
<div className="graphiql-session-header"> | |
{/* <Tabs | |
ref={tabContainerRef} | |
values={tabs} | |
onReorder={moveTab} | |
aria-label="Select active operation" | |
className="no-scrollbar" | |
> | |
{tabs.map((tab, index, arr) => ( | |
<Tab | |
key={tab.id} | |
// Prevent overscroll over container | |
dragConstraints={tabContainerRef} | |
value={tab} | |
isActive={index === activeTabIndex} | |
> | |
<Tab.Button | |
aria-controls="graphiql-session" | |
id={`graphiql-session-tab-${index}`} | |
title={tab.title} | |
onClick={handleTabClick} | |
> | |
{tab.title} | |
</Tab.Button> | |
{arr.length > 1 && <Tab.Close onClick={handleTabClose} />} | |
</Tab> | |
))} | |
</Tabs> */} | |
{/* <Tooltip label={LABEL.newTab}> | |
<UnStyledButton | |
type="button" | |
className="graphiql-tab-add" | |
onClick={addTab} | |
aria-label={LABEL.newTab} | |
> | |
<PlusIcon aria-hidden="true" /> | |
</UnStyledButton> | |
</Tooltip> */} | |
{logo} | |
</div> | |
<div | |
role="tabpanel" | |
id="graphiql-session" // used by aria-controls="graphiql-session" | |
aria-labelledby={`${TAB_CLASS_PREFIX}${activeTabIndex}`} | |
> | |
{editors} | |
{/* <div | |
className="graphiql-horizontal-drag-bar" | |
ref={editorResize.dragBarRef} | |
/> */} | |
{/* <div className="graphiql-response" ref={editorResize.secondRef}> | |
{isFetching && <Spinner />} | |
<ResponseEditor responseTooltip={responseTooltip} /> | |
{footer} | |
</div> */} | |
</div> | |
</div> | |
</div> | |
</div> | |
{children} | |
</Tooltip.Provider> | |
) | |
} | |
function getChildComponentType(child: ReactNode) { | |
if ( | |
child && | |
typeof child === "object" && | |
"type" in child && | |
typeof child.type === "function" | |
) { | |
return child.type | |
} | |
} | |
// Export main windows/panes to be used separately if desired. | |
export const GraphiQL = Object.assign(GraphiQL_, { | |
Logo: GraphiQLLogo, | |
Toolbar: GraphiQLToolbar, | |
Footer: GraphiQLFooter, | |
}) |
This file contains hidden or 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/react/font/roboto.css"; | |
@import "@graphiql/react/font/fira-code.css"; | |
@import "@graphiql/react/style.css"; | |
@import "@graphiql/plugin-doc-explorer/style.css"; | |
/* Everything */ | |
.graphiql-container { | |
display: flex; | |
height: 100%; | |
margin: 0; | |
overflow: hidden; | |
width: 100%; | |
} | |
/* The sidebar */ | |
.graphiql-container .graphiql-sidebar { | |
display: flex; | |
flex-direction: column; | |
padding: var(--px-8); | |
width: var(--sidebar-width); | |
gap: var(--px-8); | |
overflow-y: auto; | |
} | |
.graphiql-container .graphiql-sidebar > button { | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
color: hsla(var(--color-neutral), var(--alpha-secondary)); | |
height: calc(var(--sidebar-width) - (2 * var(--px-8))); | |
width: calc(var(--sidebar-width) - (2 * var(--px-8))); | |
flex-shrink: 0; | |
} | |
.graphiql-container .graphiql-sidebar button.active { | |
color: hsl(var(--color-neutral)); | |
} | |
.graphiql-container .graphiql-sidebar button > svg { | |
height: var(--px-20); | |
width: var(--px-20); | |
} | |
/* The main content, i.e. everything except the sidebar */ | |
.graphiql-container .graphiql-main { | |
display: flex; | |
flex: 1; | |
min-width: 0; | |
} | |
/* The current session and tabs */ | |
.graphiql-container .graphiql-sessions { | |
background-color: hsla(var(--color-neutral), var(--alpha-background-light)); | |
/* Adding the 8px of padding to the inner border radius of the query editor */ | |
border-radius: calc(var(--border-radius-12) + var(--px-8)); | |
display: flex; | |
flex-direction: column; | |
flex: 1; | |
max-height: 100%; | |
margin: var(--px-16); | |
margin-left: 0; | |
min-width: 0; | |
} | |
/* The session header containing tabs and the logo */ | |
.graphiql-container .graphiql-session-header { | |
height: var(--session-header-height); | |
align-items: center; | |
display: flex; | |
padding: var(--px-8) var(--px-8) 0; | |
gap: var(--px-8); | |
} | |
/* The button to add a new tab */ | |
button.graphiql-tab-add { | |
padding: var(--px-4); | |
& > svg { | |
color: hsla(var(--color-neutral), var(--alpha-secondary)); | |
display: block; | |
height: var(--px-16); | |
width: var(--px-16); | |
} | |
} | |
/* The GraphiQL logo */ | |
.graphiql-container .graphiql-logo { | |
margin-left: auto; | |
color: hsla(var(--color-neutral), var(--alpha-secondary)); | |
font-size: var(--font-size-h4); | |
font-weight: var(--font-weight-medium); | |
} | |
/* Undo default link styling for the default GraphiQL logo link */ | |
.graphiql-container .graphiql-logo .graphiql-logo-link { | |
color: hsla(var(--color-neutral), var(--alpha-secondary)); | |
text-decoration: none; | |
&:focus { | |
outline: hsla(var(--color-neutral), var(--alpha-background-heavy)) auto 1px; | |
} | |
} | |
/* The editor of the session */ | |
.graphiql-container #graphiql-session { | |
display: flex; | |
flex: 1; | |
padding: 0 var(--px-8) var(--px-8); | |
} | |
/* All editors (query, variable, headers) */ | |
.graphiql-container .graphiql-editors { | |
background-color: hsl(var(--color-base)); | |
border-radius: 0 0 var(--border-radius-12) var(--border-radius-12); | |
box-shadow: var(--popover-box-shadow); | |
display: flex; | |
flex: 1; | |
flex-direction: column; | |
} | |
/* The query editor and the toolbar */ | |
.graphiql-container .graphiql-query-editor { | |
border-bottom: 1px solid | |
hsla(var(--color-neutral), var(--alpha-background-heavy)); | |
padding: var(--px-16); | |
column-gap: var(--px-16); | |
display: flex; | |
width: 100%; | |
} | |
/* The vertical toolbar next to the query editor */ | |
.graphiql-container .graphiql-toolbar { | |
width: var(--toolbar-width); | |
display: flex; | |
flex-direction: column; | |
gap: var(--px-8); | |
} | |
.graphiql-container .graphiql-toolbar > button { | |
flex-shrink: 0; | |
} | |
/* The toolbar icons */ | |
.graphiql-toolbar-icon { | |
color: hsla(var(--color-neutral), var(--alpha-tertiary)); | |
display: block; | |
height: calc(var(--toolbar-width) - (var(--px-8) * 2)); | |
width: calc(var(--toolbar-width) - (var(--px-8) * 2)); | |
} | |
/* The tab bar for editor tools */ | |
.graphiql-container .graphiql-editor-tools { | |
cursor: row-resize; | |
display: flex; | |
width: 100%; | |
column-gap: var(--px-8); | |
padding: var(--px-8); | |
} | |
.graphiql-container .graphiql-editor-tools button { | |
color: hsla(var(--color-neutral), var(--alpha-secondary)); | |
} | |
.graphiql-container .graphiql-editor-tools button.active { | |
color: hsl(var(--color-neutral)); | |
} | |
/* The tab buttons to switch between editor tools */ | |
.graphiql-container | |
.graphiql-editor-tools | |
> button:not(.graphiql-toggle-editor-tools) { | |
padding: var(--px-8) var(--px-12); | |
} | |
.graphiql-container .graphiql-editor-tools .graphiql-toggle-editor-tools { | |
margin-left: auto; | |
} | |
/* An editor tool, e.g. variable or header editor */ | |
.graphiql-container .graphiql-editor-tool { | |
flex: 1; | |
padding: var(--px-16); | |
} | |
/** | |
* The way CodeMirror editors are styled they overflow their containing | |
* element. For some OS-browser-combinations this might cause overlap issues, | |
* setting the position of this to `relative` makes sure this element will | |
* always be on top of any editors. | |
*/ | |
.graphiql-container .graphiql-toolbar, | |
.graphiql-container .graphiql-editor-tools, | |
.graphiql-container .graphiql-editor-tool { | |
position: relative; | |
} | |
/* The response view */ | |
.graphiql-container .graphiql-response { | |
--editor-background: transparent; | |
display: flex; | |
width: 100%; | |
flex-direction: column; | |
} | |
/* The results editor wrapping container */ | |
.graphiql-container .graphiql-response .result-window { | |
position: relative; | |
flex: 1; | |
} | |
/* The footer below the response view */ | |
.graphiql-container .graphiql-footer { | |
border-top: 1px solid | |
hsla(var(--color-neutral), var(--alpha-background-heavy)); | |
} | |
/* The plugin container */ | |
.graphiql-container .graphiql-plugin { | |
border-left: 1px solid | |
hsla(var(--color-neutral), var(--alpha-background-heavy)); | |
flex: 1; | |
overflow-y: auto; | |
padding: var(--px-16); | |
} | |
/* Generic drag bar for horizontal resizing */ | |
.graphiql-horizontal-drag-bar { | |
width: var(--px-12); | |
cursor: col-resize; | |
} | |
.graphiql-horizontal-drag-bar:hover::after { | |
border: var(--px-2) solid | |
hsla(var(--color-neutral), var(--alpha-background-heavy)); | |
border-radius: var(--border-radius-2); | |
content: ""; | |
display: block; | |
height: 25%; | |
margin: 0 auto; | |
position: relative; | |
/* (100% - 25%) / 2 = 37.5% */ | |
top: 37.5%; | |
width: 0; | |
} | |
.graphiql-container .graphiql-chevron-icon { | |
color: hsla(var(--color-neutral), var(--alpha-tertiary)); | |
display: block; | |
height: var(--px-12); | |
margin: var(--px-12); | |
width: var(--px-12); | |
} | |
/* Generic spin animation */ | |
.graphiql-spin { | |
animation: spin 0.8s linear 0s infinite; | |
} | |
@keyframes spin { | |
from { | |
transform: rotate(0deg); | |
} | |
to { | |
transform: rotate(360deg); | |
} | |
} | |
/* The header of the settings dialog */ | |
.graphiql-dialog .graphiql-dialog-header { | |
align-items: center; | |
display: flex; | |
justify-content: space-between; | |
padding: var(--px-24); | |
} | |
/* The title of the settings dialog */ | |
.graphiql-dialog .graphiql-dialog-title { | |
font-size: var(--font-size-h3); | |
font-weight: var(--font-weight-medium); | |
margin: 0; | |
} | |
/* A section inside the settings dialog */ | |
.graphiql-dialog .graphiql-dialog-section { | |
align-items: center; | |
border-top: 1px solid | |
hsla(var(--color-neutral), var(--alpha-background-heavy)); | |
display: flex; | |
justify-content: space-between; | |
padding: var(--px-24); | |
} | |
.graphiql-dialog .graphiql-dialog-section > :not(:first-child) { | |
margin-left: var(--px-24); | |
} | |
/* The section title in the settings dialog */ | |
.graphiql-dialog .graphiql-dialog-section-title { | |
font-size: var(--font-size-h4); | |
font-weight: var(--font-weight-medium); | |
} | |
/* The section caption in the settings dialog */ | |
.graphiql-dialog .graphiql-dialog-section-caption { | |
color: hsla(var(--color-neutral), var(--alpha-secondary)); | |
} | |
.graphiql-dialog .graphiql-warning-text { | |
color: hsl(var(--color-warning)); | |
font-weight: var(--font-weight-medium); | |
} | |
.graphiql-dialog .graphiql-table { | |
border-collapse: collapse; | |
width: 100%; | |
} | |
.graphiql-dialog .graphiql-table :is(th, td) { | |
border: 1px solid hsla(var(--color-neutral), var(--alpha-background-heavy)); | |
padding: var(--px-8) var(--px-12); | |
} | |
/* A single key the short-key dialog */ | |
.graphiql-dialog .graphiql-key { | |
background-color: hsla(var(--color-neutral), var(--alpha-background-medium)); | |
border-radius: var(--border-radius-4); | |
padding: var(--px-4); | |
} | |
/* Avoid showing native tooltips for icons with titles */ | |
.graphiql-container svg { | |
pointer-events: none; | |
} | |
.docExplorerWrap { | |
overflow-y: auto !important; | |
} |
This file contains hidden or 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
"use client" | |
/* eslint-disable @typescript-eslint/no-non-null-assertion */ | |
/* eslint-disable @typescript-eslint/unbound-method */ | |
import type { useDragResize } from "@graphiql/react" | |
import type { FC, MouseEventHandler, ReactElement, ReactNode } from "react" | |
import { Fragment, useEffect, useState } from "react" | |
import { | |
// Button, | |
// ButtonGroup, | |
cn, | |
CopyIcon, | |
// Dialog, | |
formatShortcutForOS, | |
KEY_MAP, | |
// KeyboardShortcutIcon, | |
MergeIcon, | |
pick, | |
PrettifyIcon, | |
// ReloadIcon, | |
// SettingsIcon, | |
ToolbarButton, | |
Tooltip, | |
UnStyledButton, | |
useGraphiQL, | |
useGraphiQLActions, | |
// useStorage, | |
useTheme, | |
// VisuallyHidden, | |
} from "@graphiql/react" | |
const defaultGraphiqlLogo = ( | |
<a | |
className="graphiql-logo-link" | |
href="https://github.com/graphql/graphiql" | |
target="_blank" | |
rel="noreferrer" | |
> | |
Graph | |
<em>i</em> | |
QL | |
</a> | |
) | |
type ButtonHandler = MouseEventHandler<HTMLButtonElement> | |
// const LABEL = { | |
// refetchSchema: `Re-fetch GraphQL schema (${KEY_MAP.refetchSchema.key})`, | |
// shortCutDialog: "Open short keys dialog", | |
// settingsDialogs: "Open settings dialog", | |
// } | |
const THEMES = ["light", "dark", "system"] as const | |
interface SidebarProps { | |
/** | |
* `forcedTheme` allows enforcement of a specific theme for GraphiQL. | |
* This is useful when you want to make sure that GraphiQL is always | |
* rendered with a specific theme. | |
*/ | |
forcedTheme?: (typeof THEMES)[number] | |
/** | |
* Indicates if settings for persisting headers should appear in the | |
* settings modal. | |
*/ | |
showPersistHeadersSettings?: boolean | |
setHiddenElement: ReturnType<typeof useDragResize>["setHiddenElement"] | |
} | |
export const Sidebar: FC<SidebarProps> = ({ | |
forcedTheme: $forcedTheme, | |
// showPersistHeadersSettings, | |
setHiddenElement, | |
}) => { | |
const forcedTheme = | |
$forcedTheme && THEMES.includes($forcedTheme) ? $forcedTheme : undefined | |
// const storage = useStorage() | |
const { /*theme,*/ setTheme } = useTheme() | |
const { /*_setShouldPersistHeaders, introspect,*/ setVisiblePlugin } = | |
useGraphiQLActions() | |
const { /*shouldPersistHeaders, isIntrospecting,*/ visiblePlugin, plugins } = | |
useGraphiQL( | |
pick( | |
"shouldPersistHeaders", | |
"isIntrospecting", | |
"visiblePlugin", | |
"plugins", | |
), | |
) | |
useEffect(() => { | |
if (forcedTheme === "system") { | |
setTheme(null) | |
} else if (forcedTheme === "light" || forcedTheme === "dark") { | |
setTheme(forcedTheme) | |
} | |
}, [forcedTheme, setTheme]) | |
const [_showDialog, _setShowDialog] = useState< | |
"settings" | "short-keys" | null | |
>(null) | |
const [_clearStorageStatus, _setClearStorageStatus] = useState< | |
"success" | "error" | undefined | |
>() | |
// function handleOpenShortKeysDialog(isOpen: boolean) { | |
// if (!isOpen) { | |
// setShowDialog(null) | |
// } | |
// } | |
// function handleOpenSettingsDialog(isOpen: boolean) { | |
// if (!isOpen) { | |
// setShowDialog(null) | |
// setClearStorageStatus(undefined) | |
// } | |
// } | |
// function handleClearData() { | |
// try { | |
// storage.clear() | |
// setClearStorageStatus("success") | |
// } catch { | |
// setClearStorageStatus("error") | |
// } | |
// } | |
// const handlePersistHeaders: ButtonHandler = (event) => { | |
// setShouldPersistHeaders(event.currentTarget.dataset.value === "true") | |
// } | |
// const handleChangeTheme: ButtonHandler = (event) => { | |
// const selectedTheme = event.currentTarget.dataset.theme as | |
// | "light" | |
// | "dark" | |
// | undefined | |
// setTheme(selectedTheme ?? null) | |
// } | |
// const handleShowDialog: ButtonHandler = (event) => { | |
// setShowDialog( | |
// event.currentTarget.dataset.value as "short-keys" | "settings", | |
// ) | |
// } | |
const handlePluginClick: ButtonHandler = (event) => { | |
const pluginIndex = Number(event.currentTarget.dataset.index!) | |
const plugin = plugins.find((_, index) => pluginIndex === index)! | |
const isVisible = plugin === visiblePlugin | |
if (isVisible) { | |
setVisiblePlugin(null) | |
setHiddenElement("first") | |
} else { | |
setVisiblePlugin(plugin) | |
setHiddenElement(null) | |
} | |
} | |
return ( | |
<div className="graphiql-sidebar"> | |
{plugins.map((plugin, index) => { | |
const isVisible = plugin === visiblePlugin | |
const label = `${isVisible ? "Hide" : "Show"} ${plugin.title}` | |
return ( | |
<Tooltip key={plugin.title} label={label}> | |
<UnStyledButton | |
type="button" | |
className={cn(isVisible && "active")} | |
onClick={handlePluginClick} | |
data-index={index} | |
aria-label={label} | |
> | |
<plugin.icon aria-hidden="true" /> | |
</UnStyledButton> | |
</Tooltip> | |
) | |
})} | |
{/* <Tooltip label={LABEL.refetchSchema}> | |
<UnStyledButton | |
type="button" | |
disabled={isIntrospecting} | |
onClick={introspect} | |
aria-label={LABEL.refetchSchema} | |
style={{ marginTop: "auto" }} | |
> | |
<ReloadIcon | |
className={cn(isIntrospecting && "graphiql-spin")} | |
aria-hidden="true" | |
/> | |
</UnStyledButton> | |
</Tooltip> */} | |
{/* <Tooltip label={LABEL.shortCutDialog}> | |
<UnStyledButton | |
type="button" | |
data-value="short-keys" | |
onClick={handleShowDialog} | |
aria-label={LABEL.shortCutDialog} | |
> | |
<KeyboardShortcutIcon aria-hidden="true" /> | |
</UnStyledButton> | |
</Tooltip> */} | |
{/* <Tooltip label={LABEL.settingsDialogs}> | |
<UnStyledButton | |
type="button" | |
data-value="settings" | |
onClick={handleShowDialog} | |
aria-label={LABEL.settingsDialogs} | |
> | |
<SettingsIcon aria-hidden="true" /> | |
</UnStyledButton> | |
</Tooltip> */} | |
{/* <Dialog | |
open={showDialog === "short-keys"} | |
onOpenChange={handleOpenShortKeysDialog} | |
> | |
<div className="graphiql-dialog-header"> | |
<Dialog.Title className="graphiql-dialog-title"> | |
Short Keys | |
</Dialog.Title> | |
<VisuallyHidden> | |
<Dialog.Description> | |
This modal provides a list of available keyboard shortcuts and | |
their functions. | |
</Dialog.Description> | |
</VisuallyHidden> | |
<Dialog.Close /> | |
</div> | |
<div className="graphiql-dialog-section"> | |
<ShortKeys /> | |
</div> | |
</Dialog> */} | |
{/* <Dialog | |
open={showDialog === "settings"} | |
onOpenChange={handleOpenSettingsDialog} | |
> | |
<div className="graphiql-dialog-header"> | |
<Dialog.Title className="graphiql-dialog-title"> | |
Settings | |
</Dialog.Title> | |
<VisuallyHidden> | |
<Dialog.Description> | |
This modal lets you adjust header persistence, interface theme, | |
and clear local storage. | |
</Dialog.Description> | |
</VisuallyHidden> | |
<Dialog.Close /> | |
</div> | |
{showPersistHeadersSettings ? ( | |
<div className="graphiql-dialog-section"> | |
<div> | |
<div className="graphiql-dialog-section-title"> | |
Persist headers | |
</div> | |
<div className="graphiql-dialog-section-caption"> | |
Save headers upon reloading.{" "} | |
<span className="graphiql-warning-text"> | |
Only enable if you trust this device. | |
</span> | |
</div> | |
</div> | |
<ButtonGroup> | |
<Button | |
type="button" | |
id="enable-persist-headers" | |
className={cn(shouldPersistHeaders && "active")} | |
data-value="true" | |
onClick={handlePersistHeaders} | |
> | |
On | |
</Button> | |
<Button | |
type="button" | |
id="disable-persist-headers" | |
className={cn(!shouldPersistHeaders && "active")} | |
onClick={handlePersistHeaders} | |
> | |
Off | |
</Button> | |
</ButtonGroup> | |
</div> | |
) : null} | |
{!forcedTheme && ( | |
<div className="graphiql-dialog-section"> | |
<div> | |
<div className="graphiql-dialog-section-title">Theme</div> | |
<div className="graphiql-dialog-section-caption"> | |
Adjust how the interface appears. | |
</div> | |
</div> | |
<ButtonGroup> | |
<Button | |
type="button" | |
className={cn(theme === null && "active")} | |
onClick={handleChangeTheme} | |
> | |
System | |
</Button> | |
<Button | |
type="button" | |
className={cn(theme === "light" && "active")} | |
data-theme="light" | |
onClick={handleChangeTheme} | |
> | |
Light | |
</Button> | |
<Button | |
type="button" | |
className={cn(theme === "dark" && "active")} | |
data-theme="dark" | |
onClick={handleChangeTheme} | |
> | |
Dark | |
</Button> | |
</ButtonGroup> | |
</div> | |
)} | |
<div className="graphiql-dialog-section"> | |
<div> | |
<div className="graphiql-dialog-section-title">Clear storage</div> | |
<div className="graphiql-dialog-section-caption"> | |
Remove all locally stored data and start fresh. | |
</div> | |
</div> | |
<Button | |
type="button" | |
state={clearStorageStatus} | |
disabled={clearStorageStatus === "success"} | |
onClick={handleClearData} | |
> | |
{{ | |
success: "Cleared data", | |
error: "Failed", | |
}[clearStorageStatus!] || "Clear data"} | |
</Button> | |
</div> | |
</Dialog> */} | |
</div> | |
) | |
} | |
const DefaultToolbarRenderProps: FC<{ | |
prettify: ReactNode | |
copy: ReactNode | |
merge: ReactNode | |
}> = ({ prettify, copy, merge }) => ( | |
<> | |
{prettify} | |
{merge} | |
{copy} | |
</> | |
) | |
/** | |
* Configure the UI by providing this Component as a child of GraphiQL. | |
*/ | |
export const GraphiQLToolbar: FC<{ | |
children?: typeof DefaultToolbarRenderProps | ReactNode | |
}> = ({ children = DefaultToolbarRenderProps }) => { | |
const isRenderProp = typeof children === "function" | |
const { copyQuery, prettifyEditors, mergeQuery } = useGraphiQLActions() | |
if (!isRenderProp) { | |
return children as ReactElement | |
} | |
const prettify = ( | |
<ToolbarButton | |
onClick={prettifyEditors} | |
label={`Prettify query (${KEY_MAP.prettify.key})`} | |
> | |
<PrettifyIcon className="graphiql-toolbar-icon" aria-hidden="true" /> | |
</ToolbarButton> | |
) | |
const merge = ( | |
<ToolbarButton | |
onClick={mergeQuery} | |
label={`Merge fragments into query (${KEY_MAP.mergeFragments.key})`} | |
> | |
<MergeIcon className="graphiql-toolbar-icon" aria-hidden="true" /> | |
</ToolbarButton> | |
) | |
const copy = ( | |
<ToolbarButton | |
onClick={copyQuery} | |
label={`Copy query (${KEY_MAP.copyQuery.key})`} | |
> | |
<CopyIcon className="graphiql-toolbar-icon" aria-hidden="true" /> | |
</ToolbarButton> | |
) | |
return children({ prettify, copy, merge }) | |
} | |
const SHORT_KEYS = Object.entries({ | |
"Execute query": formatShortcutForOS(KEY_MAP.runQuery.key), | |
"Open the Command Palette (you must have focus in the editor)": "F1", | |
"Prettify editors": KEY_MAP.prettify.key, | |
"Copy query": KEY_MAP.copyQuery.key, | |
"Re-fetch schema using introspection": KEY_MAP.refetchSchema.key, | |
"Search in documentation": formatShortcutForOS(KEY_MAP.searchInDocs.key), | |
"Search in editor": formatShortcutForOS(KEY_MAP.searchInEditor.key), | |
"Merge fragments definitions into operation definition": | |
KEY_MAP.mergeFragments.key, | |
}) | |
export const ShortKeys: FC = () => { | |
return ( | |
<div> | |
<table className="graphiql-table"> | |
<thead> | |
<tr> | |
<th>Short Key</th> | |
<th>Function</th> | |
</tr> | |
</thead> | |
<tbody> | |
{SHORT_KEYS.map(([title, keys]) => ( | |
<tr key={title}> | |
<td> | |
{keys.split("-").map((key, index, array) => ( | |
<Fragment key={key}> | |
<code className="graphiql-key">{key}</code> | |
{index !== array.length - 1 && " + "} | |
</Fragment> | |
))} | |
</td> | |
<td>{title}</td> | |
</tr> | |
))} | |
</tbody> | |
</table> | |
<p> | |
This Graph<em>i</em>QL editor uses{" "} | |
<a | |
href="https://code.visualstudio.com/docs/reference/default-keybindings" | |
target="_blank" | |
rel="noreferrer" | |
> | |
Monaco editor shortcuts | |
</a> | |
, with keybindings similar to VS Code. See the full list of shortcuts | |
for{" "} | |
<a | |
href="https://code.visualstudio.com/shortcuts/keyboard-shortcuts-macos.pdf" | |
target="_blank" | |
rel="noreferrer" | |
> | |
macOS | |
</a>{" "} | |
or{" "} | |
<a | |
href="https://code.visualstudio.com/shortcuts/keyboard-shortcuts-windows.pdf" | |
target="_blank" | |
rel="noreferrer" | |
> | |
Windows | |
</a> | |
. | |
</p> | |
</div> | |
) | |
} | |
// Configure the UI by providing this Component as a child of GraphiQL. | |
export const GraphiQLLogo: FC<{ children?: ReactNode }> = ({ | |
children = defaultGraphiqlLogo, | |
}) => { | |
return <div className="graphiql-logo">{children}</div> | |
} | |
// Configure the UI by providing this Component as a child of GraphiQL. | |
export const GraphiQLFooter: FC<{ children: ReactNode }> = ({ children }) => { | |
return <div className="graphiql-footer">{children}</div> | |
} |
This file contains hidden or 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
{ | |
"graphiql": "5.0.3", | |
"@graphiql/plugin-doc-explorer": "0.3.0", | |
"@graphiql/plugin-explorer": "5.0.0", | |
"@graphiql/plugin-history": "0.3.0", | |
"@graphiql/react": "0.35.4", | |
"@graphiql/toolkit": "0.11.3", | |
} |
This file contains hidden or 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
"use client" | |
import { DOC_EXPLORER_PLUGIN } from "@graphiql/plugin-doc-explorer" | |
import { ClientOnly } from "../client-only" | |
import "./style.css" | |
import dynamic from "next/dynamic" | |
import { explorerPlugin } from "@graphiql/plugin-explorer" | |
import { createGraphiQLFetcher } from "@graphiql/toolkit" | |
import "graphiql/setup-workers/webpack" | |
const schemaFetcher = (url: string) => | |
createGraphiQLFetcher({ | |
url, | |
fetch: fetch, | |
}) | |
export function GraphiQLWrapper({ | |
url, | |
query, | |
className, | |
}: { | |
url: string | |
query?: string | |
className?: string | |
}) { | |
// this was needed, otherwise I got an error about `window is not defined` from the monaco-editor | |
// even with `use client` and the `ClientOnly` component | |
if (typeof window != "undefined") { | |
const GraphiQL = dynamic( | |
() => import("./graphiql").then((mod) => mod.GraphiQL), | |
{ | |
ssr: false, | |
}, | |
) | |
const explorer = explorerPlugin({ hideActions: true, explorerIsOpen: true }) | |
const defaultQuery = `# Welcome to the GraphQL Schema documentation | |
# | |
# We are using GraphiQL to render our GraphQL schema. | |
# GraphiQL is an in-browser tool for writing, validating, and | |
# testing GraphQL queries. | |
# | |
# Type queries into this side of the screen, and you will see intelligent | |
# typeaheads aware of the current GraphQL type schema and live syntax and | |
# validation errors highlighted within the text. | |
# | |
# GraphQL queries typically start with a "{" character. Lines that starts | |
# with a # are ignored. | |
# | |
# An example GraphQL query might look like: | |
# | |
# { | |
# field(arg: "value") { | |
# subField | |
# } | |
# } | |
# | |
` | |
return ( | |
<ClientOnly> | |
<GraphiQL | |
fetcher={schemaFetcher(url)} | |
isHeadersEditorEnabled={false} | |
defaultEditorToolsVisibility={false} | |
forcedTheme="dark" | |
shouldPersistHeaders={false} | |
showPersistHeadersSettings={false} | |
plugins={[explorer]} | |
referencePlugin={DOC_EXPLORER_PLUGIN} | |
initialQuery={query ?? defaultQuery} | |
defaultQuery={defaultQuery} | |
className={className} | |
/> | |
</ClientOnly> | |
) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment