Skip to content

Instantly share code, notes, and snippets.

@noxify
Last active July 3, 2025 09:14
Show Gist options
  • Save noxify/11c71447229fb858fceef87c63496dcc to your computer and use it in GitHub Desktop.
Save noxify/11c71447229fb858fceef87c63496dcc to your computer and use it in GitHub Desktop.
Custom GraphiQL client
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
}
---
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
        }
      }
    }
  }
`} />

"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,
})
@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;
}
"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>
}
{
"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",
}
"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