FirstLevelNav.tsx 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318
  1. import { useParams } from 'common'
  2. import { Book, BookOpen } from 'lucide-react'
  3. import Link from 'next/link'
  4. import { Fragment, type ReactNode } from 'react'
  5. import SVG from 'react-inlinesvg'
  6. import { Button, cn } from 'ui'
  7. import { ShimmeringLoader } from 'ui-patterns'
  8. import { navigateToSection } from './Content/Content.utils'
  9. import { API_DOCS_CATEGORIES, DOCS_CONTENT, DOCS_MENU } from './ProjectAPIDocs.constants'
  10. import { InfiniteListDefault, type RowComponentBaseProps } from '@/components/ui/InfiniteList'
  11. import { useEdgeFunctionsQuery } from '@/data/edge-functions/edge-functions-query'
  12. import { useOpenAPISpecQuery } from '@/data/open-api/api-spec-query'
  13. import { usePaginatedBucketsQuery, type Bucket } from '@/data/storage/buckets-query'
  14. import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled'
  15. import { BASE_PATH, DOCS_URL } from '@/lib/constants'
  16. import { useAppStateSnapshot } from '@/state/app-state'
  17. type DocsSections = typeof DOCS_MENU
  18. type DocsSection = DocsSections[number]
  19. type DocsSectionsSubset = readonly DocsSection[]
  20. type DocsCategory = DocsSection['key']
  21. type DocsContentRegistry = typeof DOCS_CONTENT
  22. type DocsSnippet = DocsContentRegistry[keyof DocsContentRegistry]
  23. const Separator = () => <hr className="border-t mt-3! pb-1 mx-3" />
  24. const MENU_BUTTON_CLASSES = cn(
  25. 'w-full px-4',
  26. 'text-left text-sm text-foreground-light',
  27. 'transition hover:text-foreground'
  28. )
  29. /**
  30. * Gets the docs menu items based on feature flags.
  31. * @returns An array of menu items to be displayed in the docs navigation.
  32. */
  33. const useDocsMenu = (): DocsSectionsSubset => {
  34. const {
  35. projectAuthAll: authEnabled,
  36. projectStorageAll: storageEnabled,
  37. projectEdgeFunctionAll: edgeFunctionsEnabled,
  38. realtimeAll: realtimeEnabled,
  39. } = useIsFeatureEnabled([
  40. 'project_auth:all',
  41. 'project_storage:all',
  42. 'project_edge_function:all',
  43. 'realtime:all',
  44. ])
  45. return DOCS_MENU.filter((item) => {
  46. if (item.key === 'user-management') return authEnabled
  47. if (item.key === 'storage') return storageEnabled
  48. if (item.key === 'edge-functions') return edgeFunctionsEnabled
  49. if (item.key === 'realtime') return realtimeEnabled
  50. return true
  51. })
  52. }
  53. /**
  54. * Gets the content snippets for a given documentation category.
  55. * @param category - The category of documentation to retrieve snippets for.
  56. * @returns An array of content snippets belonging to the specified category.
  57. */
  58. const getSectionSnippets = (category: DocsCategory): DocsSnippet[] =>
  59. Object.values(DOCS_CONTENT).filter((snippet) => snippet.category === category)
  60. export const FirstLevelNav = (): ReactNode => {
  61. const { ref } = useParams()
  62. const snap = useAppStateSnapshot()
  63. const currentSection = snap.activeDocsSection[0]
  64. const docsMenu = useDocsMenu()
  65. return (
  66. <>
  67. <nav aria-labelledby="api-docs-rest-categories" className="px-2 py-4 border-b">
  68. <h2 id="api-docs-rest-categories" className="sr-only">
  69. REST API Docs
  70. </h2>
  71. {docsMenu.map((item) => {
  72. const isActive = currentSection === item.key
  73. return (
  74. <Fragment key={item.key}>
  75. <button
  76. aria-current={isActive ? 'page' : undefined}
  77. className={cn(
  78. 'w-full px-3 py-2 rounded-md',
  79. 'text-left text-sm',
  80. 'transition',
  81. isActive && 'bg-surface-300'
  82. )}
  83. onClick={() => snap.setActiveDocsSection([item.key])}
  84. >
  85. {item.name}
  86. </button>
  87. {isActive && <Subsections category={item.key} />}
  88. </Fragment>
  89. )
  90. })}
  91. </nav>
  92. <div className="px-2 py-4 border-b">
  93. <Button
  94. block
  95. asChild
  96. type="text"
  97. size="small"
  98. icon={
  99. <SVG
  100. src={`${BASE_PATH}/img/graphql.svg`}
  101. style={{ width: `${16}px`, height: `${16}px` }}
  102. className="text-foreground"
  103. preProcessor={(code) => code.replace(/svg/, 'svg class="m-auto text-color-inherit"')}
  104. />
  105. }
  106. onClick={() => snap.setShowProjectApiDocs(false)}
  107. >
  108. <Link className="justify-start!" href={`/project/${ref}/integrations/graphiql`}>
  109. GraphiQL
  110. </Link>
  111. </Button>
  112. <Button block asChild type="text" size="small" icon={<BookOpen />}>
  113. <Link
  114. href={`${DOCS_URL}/guides/graphql`}
  115. target="_blank"
  116. rel="noreferrer"
  117. className="justify-start!"
  118. >
  119. GraphQL guide
  120. </Link>
  121. </Button>
  122. </div>
  123. <div className="px-2 py-4">
  124. <Button block asChild type="text" size="small" icon={<Book />}>
  125. <Link href={`${DOCS_URL}`} target="_blank" rel="noreferrer" className="justify-start!">
  126. Documentation
  127. </Link>
  128. </Button>
  129. <Button block asChild type="text" size="small" icon={<BookOpen />}>
  130. <Link
  131. href={`${DOCS_URL}/guides/api`}
  132. target="_blank"
  133. rel="noreferrer"
  134. className="justify-start!"
  135. >
  136. REST guide
  137. </Link>
  138. </Button>
  139. </div>
  140. </>
  141. )
  142. }
  143. type SubsectionsProps = {
  144. category: DocsCategory
  145. }
  146. const Subsections = ({ category }: SubsectionsProps): ReactNode => {
  147. const snippets = getSectionSnippets(category)
  148. return (
  149. <div className="space-y-2 py-2">
  150. {snippets.map((snippet) => (
  151. <button
  152. key={snippet.key}
  153. className={MENU_BUTTON_CLASSES}
  154. onClick={() => {
  155. navigateToSection(snippet.key)
  156. }}
  157. >
  158. {snippet.title}
  159. </button>
  160. ))}
  161. {category === API_DOCS_CATEGORIES.ENTITIES && <TablesSubsections />}
  162. {category === API_DOCS_CATEGORIES.STORED_PROCEDURES && <DbFunctionsSubsections />}
  163. {category === API_DOCS_CATEGORIES.STORAGE && <StorageSubsections />}
  164. {category === API_DOCS_CATEGORIES.EDGE_FUNCTIONS && <EdgeFunctionsSubsections />}
  165. </div>
  166. )
  167. }
  168. const TablesSubsections = (): ReactNode => {
  169. const { ref } = useParams()
  170. const snap = useAppStateSnapshot()
  171. const { data, isLoading } = useOpenAPISpecQuery(
  172. { projectRef: ref },
  173. { staleTime: 1000 * 60 * 10 }
  174. )
  175. const tables = data?.tables ?? []
  176. // TODO: handle infinite loading of tables
  177. return (
  178. <>
  179. {isLoading && <LoadingIndicator />}
  180. {tables.length > 0 && <Separator />}
  181. {tables.map((table) => (
  182. <button
  183. key={table.name}
  184. className={MENU_BUTTON_CLASSES}
  185. onClick={() => snap.setActiveDocsSection([API_DOCS_CATEGORIES.ENTITIES, table.name])}
  186. >
  187. {table.name}
  188. </button>
  189. ))}
  190. </>
  191. )
  192. }
  193. const DbFunctionsSubsections = (): ReactNode => {
  194. const { ref } = useParams()
  195. const snap = useAppStateSnapshot()
  196. const { data, isLoading } = useOpenAPISpecQuery(
  197. { projectRef: ref },
  198. { staleTime: 1000 * 60 * 10 }
  199. )
  200. const functions = data?.functions ?? []
  201. // TODO: handle virtualization of DB functions
  202. return (
  203. <>
  204. {isLoading && <LoadingIndicator />}
  205. {functions.length > 0 && <Separator />}
  206. {functions.map((fn) => (
  207. <button
  208. key={fn.name}
  209. className={MENU_BUTTON_CLASSES}
  210. onClick={() =>
  211. snap.setActiveDocsSection([API_DOCS_CATEGORIES.STORED_PROCEDURES, fn.name])
  212. }
  213. >
  214. {fn.name}
  215. </button>
  216. ))}
  217. </>
  218. )
  219. }
  220. const BucketButton = ({ item: bucket, style }: RowComponentBaseProps<Bucket>) => {
  221. const snap = useAppStateSnapshot()
  222. return (
  223. <button
  224. key={bucket.name}
  225. className={cn(MENU_BUTTON_CLASSES, 'py-1')}
  226. style={style}
  227. onClick={() => snap.setActiveDocsSection([API_DOCS_CATEGORIES.STORAGE, bucket.name])}
  228. >
  229. {bucket.name}
  230. </button>
  231. )
  232. }
  233. const StorageSubsections = (): ReactNode => {
  234. const { ref } = useParams()
  235. const { data, isLoading, isFetchingNextPage, hasNextPage, fetchNextPage } =
  236. usePaginatedBucketsQuery({
  237. projectRef: ref,
  238. })
  239. const buckets = data?.pages.flatMap((page) => page) ?? []
  240. return (
  241. <>
  242. {isLoading && <LoadingIndicator />}
  243. {buckets.length > 0 && <Separator />}
  244. <InfiniteListDefault
  245. className="max-h-80"
  246. items={buckets}
  247. getItemKey={(idx) => buckets[idx]?.name}
  248. getItemSize={() => 28}
  249. hasNextPage={!!hasNextPage}
  250. isLoadingNextPage={isFetchingNextPage}
  251. onLoadNextPage={fetchNextPage}
  252. ItemComponent={BucketButton}
  253. LoaderComponent={({ style }) => <LoadingIndicator style={{ ...style, width: '75%' }} />}
  254. />
  255. </>
  256. )
  257. }
  258. const EdgeFunctionsSubsections = (): ReactNode => {
  259. const { ref } = useParams()
  260. const snap = useAppStateSnapshot()
  261. const { data: edgeFunctions, isLoading } = useEdgeFunctionsQuery({ projectRef: ref })
  262. // TODO: handle virtualization of edge functions
  263. return (
  264. <>
  265. {isLoading && <LoadingIndicator />}
  266. {(edgeFunctions ?? []).length > 0 && <Separator />}
  267. {(edgeFunctions ?? []).map((fn) => (
  268. <button
  269. key={fn.name}
  270. className={MENU_BUTTON_CLASSES}
  271. onClick={() => snap.setActiveDocsSection([API_DOCS_CATEGORIES.EDGE_FUNCTIONS, fn.name])}
  272. >
  273. {fn.name}
  274. </button>
  275. ))}
  276. </>
  277. )
  278. }
  279. type LoadingIndicatorProps = {
  280. className?: string
  281. style?: React.CSSProperties
  282. }
  283. const LoadingIndicator = ({ className, style }: LoadingIndicatorProps) => (
  284. <ShimmeringLoader style={style} className={cn('mx-2', className)} />
  285. )