GraphiQLTab.tsx 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
  1. import 'graphiql/style.css'
  2. import 'graphiql/setup-workers/webpack'
  3. import { useMonaco, type GraphiQLPlugin } from '@graphiql/react'
  4. import { createGraphiQLFetcher, Fetcher } from '@graphiql/toolkit'
  5. import { PermissionAction } from '@supabase/shared-types/out/constants'
  6. import { useParams } from 'common'
  7. import { GraphiQL, HISTORY_PLUGIN } from 'graphiql'
  8. import { User as IconUser } from 'lucide-react'
  9. import { useTheme } from 'next-themes'
  10. import { useCallback, useEffect, useMemo, useState } from 'react'
  11. import { toast } from 'sonner'
  12. import { LogoLoader } from 'ui'
  13. import { DEFAULT_INTROSPECTION_SCHEMA } from './constants'
  14. import styles from './graphiql.module.css'
  15. import { IntrospectionDisabledNotice } from './IntrospectionDisabledNotice'
  16. import { IntrospectionEnabledNotice } from './IntrospectionEnabledNotice'
  17. import { usePgGraphqlIntrospectionStatus } from './usePgGraphqlIntrospectionStatus'
  18. import { getTheme } from '@/components/interfaces/App/MonacoThemeProvider'
  19. import { RoleImpersonationSelector } from '@/components/interfaces/RoleImpersonationSelector'
  20. import { useSessionAccessTokenQuery } from '@/data/auth/session-access-token-query'
  21. import { useProjectPostgrestConfigQuery } from '@/data/config/project-postgrest-config-query'
  22. import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
  23. import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
  24. import { API_URL, IS_PLATFORM } from '@/lib/constants'
  25. import { getRoleImpersonationJWT } from '@/lib/role-impersonation'
  26. import { useGetImpersonatedRoleState } from '@/state/role-impersonation-state'
  27. const ROLE_IMPERSONATION_PLUGIN: GraphiQLPlugin = {
  28. title: 'Role Impersonation',
  29. icon: () => <IconUser />,
  30. content: () => <RoleImpersonationSelector orientation="vertical" />,
  31. }
  32. const MONACO_THEME = { dark: 'briven-graphql-dark', light: 'briven-graphql-light' }
  33. const GraphiQLMonacoTheme = ({ resolvedTheme }: { resolvedTheme: 'dark' | 'light' }) => {
  34. const { monaco } = useMonaco()
  35. useEffect(() => {
  36. if (!monaco) return
  37. const dark = getTheme('dark')
  38. const light = getTheme('light')
  39. monaco.editor.defineTheme(MONACO_THEME.dark, {
  40. ...dark,
  41. rules: [...dark.rules, { token: 'argument.identifier.gql', foreground: '908aff' }],
  42. })
  43. monaco.editor.defineTheme(MONACO_THEME.light, {
  44. ...light,
  45. rules: [...light.rules, { token: 'argument.identifier.gql', foreground: '6c69ce' }],
  46. // Match the dashboard's bg-default in light mode so the editor doesn't read
  47. // as a darker square against the surrounding UI.
  48. colors: { ...light.colors, 'editor.background': '#fcfcfc' },
  49. })
  50. monaco.editor.setTheme(MONACO_THEME[resolvedTheme])
  51. }, [monaco, resolvedTheme])
  52. return null
  53. }
  54. export const GraphiQLTab = () => {
  55. const { resolvedTheme } = useTheme()
  56. const { ref: projectRef } = useParams()
  57. const currentTheme = resolvedTheme?.includes('dark') ? 'dark' : 'light'
  58. const { data: accessToken } = useSessionAccessTokenQuery({ enabled: IS_PLATFORM })
  59. const { data: project } = useSelectedProjectQuery()
  60. const { data: config } = useProjectPostgrestConfigQuery({ projectRef })
  61. const jwtSecret = config?.jwt_secret
  62. const getImpersonatedRoleState = useGetImpersonatedRoleState()
  63. const { can: canReadJWTSecret } = useAsyncCheckPermissions(
  64. PermissionAction.READ,
  65. 'field.jwt_secret'
  66. )
  67. const { notice, schemaComment } = usePgGraphqlIntrospectionStatus({
  68. projectRef,
  69. connectionString: project?.connectionString,
  70. schema: DEFAULT_INTROSPECTION_SCHEMA,
  71. })
  72. // Bumped to force GraphiQL to re-mount and re-run introspection after the
  73. // introspection setting changes in either direction.
  74. const [graphiqlKey, setGraphiqlKey] = useState(0)
  75. const plugins = useMemo<GraphiQLPlugin[]>(
  76. () => (canReadJWTSecret ? [HISTORY_PLUGIN, ROLE_IMPERSONATION_PLUGIN] : [HISTORY_PLUGIN]),
  77. [canReadJWTSecret]
  78. )
  79. const fetcher = useMemo(() => {
  80. const fetcherFn = createGraphiQLFetcher({
  81. // [Joshen] Opting to hard code /platform for local to match the routes, so that it's clear what's happening
  82. url: `${API_URL}${IS_PLATFORM ? '' : '/platform'}/projects/${projectRef}/api/graphql`,
  83. fetch,
  84. })
  85. const customFetcher: Fetcher = async (graphqlParams, opts) => {
  86. let userAuthorization: string | undefined
  87. const role = getImpersonatedRoleState().role
  88. if (
  89. projectRef !== undefined &&
  90. jwtSecret !== undefined &&
  91. role !== undefined &&
  92. role.type === 'postgrest'
  93. ) {
  94. try {
  95. const token = await getRoleImpersonationJWT(projectRef, jwtSecret, role)
  96. userAuthorization = 'Bearer ' + token
  97. } catch (err: any) {
  98. toast.error(`Failed to get JWT for role: ${err.message}`)
  99. }
  100. }
  101. return fetcherFn(graphqlParams, {
  102. ...opts,
  103. headers: {
  104. ...opts?.headers,
  105. ...(accessToken && {
  106. Authorization: `Bearer ${accessToken}`,
  107. }),
  108. 'x-graphql-authorization':
  109. opts?.headers?.['Authorization'] ??
  110. opts?.headers?.['authorization'] ??
  111. userAuthorization ??
  112. accessToken,
  113. },
  114. })
  115. }
  116. return customFetcher
  117. }, [projectRef, getImpersonatedRoleState, jwtSecret, accessToken])
  118. const handleIntrospectionChanged = useCallback(() => {
  119. setGraphiqlKey((k) => k + 1)
  120. }, [])
  121. if (IS_PLATFORM && !accessToken) {
  122. return <LogoLoader />
  123. }
  124. return (
  125. <div className="flex flex-col h-full">
  126. <GraphiQLMonacoTheme resolvedTheme={currentTheme} />
  127. {notice === 'opt-in' && (
  128. <IntrospectionDisabledNotice
  129. schema={DEFAULT_INTROSPECTION_SCHEMA}
  130. currentSchemaComment={schemaComment}
  131. onEnabled={handleIntrospectionChanged}
  132. />
  133. )}
  134. {notice === 'opt-out' && (
  135. <IntrospectionEnabledNotice
  136. schema={DEFAULT_INTROSPECTION_SCHEMA}
  137. currentSchemaComment={schemaComment}
  138. onDisabled={handleIntrospectionChanged}
  139. />
  140. )}
  141. <div className="flex-1 min-h-0">
  142. <GraphiQL
  143. key={graphiqlKey}
  144. fetcher={fetcher}
  145. forcedTheme={currentTheme}
  146. editorTheme={MONACO_THEME}
  147. className={styles.root}
  148. plugins={plugins}
  149. />
  150. </div>
  151. </div>
  152. )
  153. }