/* Based on https://github.com/graphql/graphiql/blob/main/packages/graphiql/src/components/GraphiQL.tsx */ import { ChevronDownIcon, ChevronUpIcon, CopyIcon, ExecuteButton, GraphiQLProvider, HeaderEditor, MergeIcon, PlusIcon, PrettifyIcon, QueryEditor, ReloadIcon, ResponseEditor, Spinner, Tab, Tabs, ToolbarButton, Tooltip, UnStyledButton, VariableEditor, useCopyQuery, useDragResize, useEditorContext, useExecutionContext, useMergeQuery, usePluginContext, usePrettifyEditors, useSchemaContext, useTheme, } from '@graphiql/react' import { Fetcher } from '@graphiql/toolkit' import { PermissionAction } from '@supabase/shared-types/out/constants' import { AlertTriangle, XIcon } from 'lucide-react' import { MouseEventHandler, useCallback, useEffect, useState } from 'react' import { LOCAL_STORAGE_KEYS } from 'common' import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions' import { useLocalStorage } from 'hooks/misc/useLocalStorage' import { AlertDescription_Shadcn_, AlertTitle_Shadcn_, Alert_Shadcn_, Button, cn } from 'ui' import { RoleImpersonationSelector } from '../RoleImpersonationSelector' import styles from './graphiql.module.css' export interface GraphiQLProps { fetcher: Fetcher theme?: 'dark' | 'light' } export default function GraphiQL({ fetcher, theme = 'dark' }: GraphiQLProps) { // Ensure props are correct if (typeof fetcher !== 'function') { throw new TypeError( 'The `GraphiQL` component requires a `fetcher` function to be passed as prop.' ) } return ( ) } interface GraphiQLInterfaceProps { theme: 'dark' | 'light' } const GraphiQLInterface = ({ theme }: GraphiQLInterfaceProps) => { const editorContext = useEditorContext({ nonNull: true }) const executionContext = useExecutionContext({ nonNull: true }) const schemaContext = useSchemaContext({ nonNull: true }) const pluginContext = usePluginContext() const copy = useCopyQuery() const merge = useMergeQuery() const prettify = usePrettifyEditors() const { can: canReadJWTSecret } = useAsyncCheckPermissions( PermissionAction.READ, 'field.jwt_secret' ) const [rlsBypassedWarningDismissed, setRlsBypassedWarningDismissed] = useLocalStorage( LOCAL_STORAGE_KEYS.GRAPHIQL_RLS_BYPASS_WARNING, false ) const { setTheme } = useTheme() useEffect(() => { setTheme(theme) }, [theme]) const PluginContent = pluginContext?.visiblePlugin?.content const pluginResize = useDragResize({ defaultSizeRelation: 1 / 3, direction: 'horizontal', initiallyHidden: pluginContext?.visiblePlugin ? undefined : 'second', onHiddenElementChange: (resizableElement) => { if (resizableElement === 'second') { pluginContext?.setVisiblePlugin(null) } }, sizeThresholdSecond: 200, storageKey: 'docExplorerFlex', }) const editorResize = useDragResize({ direction: 'horizontal', storageKey: 'editorFlex', }) const editorToolsResize = useDragResize({ defaultSizeRelation: 3, direction: 'vertical', initiallyHidden: (() => { return editorContext.initialVariables || editorContext.initialHeaders ? undefined : 'second' })(), sizeThresholdSecond: 60, storageKey: 'secondaryEditorFlex', }) const [activeSecondaryEditor, setActiveSecondaryEditor] = useState< 'variables' | 'headers' | 'role-impersonation' >(() => { return !editorContext.initialVariables && editorContext.initialHeaders ? 'headers' : 'variables' }) const toolbar = ( <> ) const onClickReference = useCallback(() => { if (pluginResize.hiddenElement === 'second') { pluginResize.setHiddenElement(null) } }, [pluginResize]) const handleAddTab = editorContext.addTab const handleRefetchSchema = schemaContext.introspect const handleReorder = editorContext.moveTab const handlePluginClick: MouseEventHandler = useCallback( (e) => { const context = pluginContext! const pluginIndex = Number(e.currentTarget.dataset.index!) const plugin = context.plugins.find((_, index) => pluginIndex === index)! const isVisible = plugin === context.visiblePlugin if (isVisible) { context.setVisiblePlugin(null) pluginResize.setHiddenElement('second') } else { context.setVisiblePlugin(plugin) pluginResize.setHiddenElement(null) } }, [pluginContext, pluginResize] ) const handleToolsTabClick: MouseEventHandler = useCallback( (event) => { if (editorToolsResize.hiddenElement === 'second') { editorToolsResize.setHiddenElement(null) } setActiveSecondaryEditor( event.currentTarget.dataset.name as 'variables' | 'headers' | 'role-impersonation' ) }, [editorToolsResize] ) const toggleEditorTools: MouseEventHandler = useCallback(() => { editorToolsResize.setHiddenElement( editorToolsResize.hiddenElement === 'second' ? null : 'second' ) }, [editorToolsResize]) const addTab = ( ) const hasSingleTab = editorContext.tabs.length === 1 return (
{editorContext.tabs.length > 1 && ( <> {editorContext.tabs.map((tab, index) => ( { executionContext.stop() editorContext.changeTab(index) }} > {tab.title} { if (editorContext.activeTabIndex === index) { executionContext.stop() } editorContext.closeTab(index) }} /> ))} {addTab} )}
{hasSingleTab &&
{addTab}
}
{toolbar}
Variables Headers {canReadJWTSecret && ( Role Impersonation )} {editorToolsResize.hiddenElement === 'second' ? (
{canReadJWTSecret && (
)}
{executionContext.isFetching ? : null} {!rlsBypassedWarningDismissed && ( Please note that queries and mutations run in GraphiQL now use the service role key by default.
RLS will be bypassed.
You can send queries as a specific role/user by using the role impersonation tab.
)}
{pluginContext?.visiblePlugin && (
)}
{PluginContent ? : null}
{pluginContext?.plugins.map((plugin, index) => { const isVisible = plugin === pluginContext.visiblePlugin const label = `${isVisible ? 'Hide' : 'Show'} ${plugin.title}` const Icon = plugin.icon return ( ) })}
) }