mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
feat: federated docs for linter (#22472)
* feat: federated docs for linter * style: clean up * fix: toc and link rewrite * fix: remove console logs
This commit is contained in:
1 parent
b5ef7af6b7
commit
1d52dfacde
15 files changed
+348
-388
No files matched your search
@@ -1,5 +1,14 @@
|
||||
import { FC } from 'react'
|
||||
import { usePathname } from 'next/navigation'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
import { cn, TAB_CHANGE_EVENT_NAME } from 'ui'
|
||||
import { ExpandableVideo } from 'ui-patterns'
|
||||
|
||||
import { highlightSelectedTocItem } from '~/components/CustomHTMLElements/CustomHTMLElements.utils'
|
||||
import useHash from '~/hooks/useHash'
|
||||
import { useRerenderOnEvt } from '~/hooks/useManualRerender'
|
||||
import { removeAnchor } from './CustomHTMLElements/CustomHTMLElements.utils'
|
||||
import { Feedback } from './Feedback'
|
||||
|
||||
const formatSlug = (slug: string) => {
|
||||
// [Joshen] We will still provide support for headers declared like this:
|
||||
@@ -28,30 +37,78 @@ const formatTOCHeader = (content: string) => {
|
||||
return res.join('')
|
||||
}
|
||||
|
||||
interface TOCHeader {
|
||||
id: number
|
||||
level: number
|
||||
text: string
|
||||
link: string
|
||||
}
|
||||
const GuidesTableOfContents = ({
|
||||
className,
|
||||
overrideToc,
|
||||
video,
|
||||
}: {
|
||||
className?: string
|
||||
overrideToc?: Array<{ text: string; link: string; level: number }>
|
||||
video?: string
|
||||
}) => {
|
||||
const [tocList, setTocList] = useState([])
|
||||
const pathname = usePathname()
|
||||
const [hash] = useHash()
|
||||
|
||||
interface Props {
|
||||
list: TOCHeader[]
|
||||
}
|
||||
const displayedList = overrideToc ?? tocList
|
||||
|
||||
useEffect(() => {
|
||||
if (overrideToc) return
|
||||
|
||||
const headings = Array.from(
|
||||
document.querySelector('#sb-docs-guide-main-article')?.querySelectorAll('h2, h3') ?? []
|
||||
)
|
||||
const newHeadings = headings
|
||||
.filter((heading) => heading.id)
|
||||
.map((heading) => {
|
||||
const text = heading.textContent.replace('#', '')
|
||||
const link = heading.querySelector('a').getAttribute('href')
|
||||
const level = heading.tagName === 'H2' ? 2 : 3
|
||||
return { text, link, level }
|
||||
})
|
||||
setTocList(newHeadings)
|
||||
}, [pathname]) // needed to recalculate the toc when path changes
|
||||
|
||||
useEffect(() => {
|
||||
if (hash && displayedList.length > 0) {
|
||||
highlightSelectedTocItem(hash)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [hash, JSON.stringify(displayedList)])
|
||||
|
||||
/**
|
||||
* Displayed headings may change if the tab changes, so the table of contents
|
||||
* needs to rerender.
|
||||
*/
|
||||
useRerenderOnEvt(TAB_CHANGE_EVENT_NAME)
|
||||
|
||||
if (!displayedList.length) return
|
||||
|
||||
const tocVideoPreview = `http://img.youtube.com/vi/${video}/0.jpg`
|
||||
|
||||
const GuidesTableOfContents: FC<Props> = ({ list }) => {
|
||||
return (
|
||||
<ul className="toc-menu list-none pl-5 text-[0.8rem] grid gap-2">
|
||||
{list.map((item, i) => (
|
||||
<li key={`${item.level}-${i}`} className={item.level === 3 ? 'ml-4' : ''}>
|
||||
<a
|
||||
href={`#${formatSlug(item.link)}`}
|
||||
className="text-foreground-lighter hover:text-brand-link transition-colors"
|
||||
dangerouslySetInnerHTML={{ __html: formatTOCHeader(removeAnchor(item.text)) }}
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div className={cn('border-l', 'thin-scrollbar overflow-y-auto', 'px-2', className)}>
|
||||
{video && (
|
||||
<div className="relative mb-6 pl-5">
|
||||
<ExpandableVideo imgUrl={tocVideoPreview} videoId={video} />
|
||||
</div>
|
||||
)}
|
||||
<Feedback key={pathname} />
|
||||
<span className="block font-mono text-xs uppercase text-foreground px-5 mb-6">
|
||||
On this page
|
||||
</span>
|
||||
<ul className="toc-menu list-none pl-5 text-[0.8rem] grid gap-2">
|
||||
{displayedList.map((item, i) => (
|
||||
<li key={`${item.level}-${i}`} className={item.level === 3 ? 'ml-4' : ''}>
|
||||
<a
|
||||
href={`#${formatSlug(item.link)}`}
|
||||
className="text-foreground-lighter hover:text-brand-link transition-colors"
|
||||
dangerouslySetInnerHTML={{ __html: formatTOCHeader(removeAnchor(item.text)) }}
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -794,7 +794,14 @@ export const database: NavMenuConstant = {
|
||||
name: 'Query optimization',
|
||||
url: '/guides/database/query-optimization',
|
||||
},
|
||||
{ name: 'Debugging and monitoring', url: '/guides/database/inspect' },
|
||||
{
|
||||
name: 'Database linter',
|
||||
url: '/guides/database/database-linter',
|
||||
},
|
||||
{
|
||||
name: 'Debugging and monitoring',
|
||||
url: '/guides/database/inspect',
|
||||
},
|
||||
{
|
||||
name: 'Debugging performance issues',
|
||||
url: '/guides/database/debugging-performance',
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
// [Terry]
|
||||
// Delete this after we've implemented GuidesTableofContents and moved all guides
|
||||
// and rename GuidesTableofContents to TableOfContents
|
||||
|
||||
import { FC } from 'react'
|
||||
import { getAnchor, removeAnchor } from './CustomHTMLElements/CustomHTMLElements.utils'
|
||||
|
||||
interface TOCHeader {
|
||||
id: number
|
||||
lvl: number
|
||||
seen: number
|
||||
content: string
|
||||
slug: string
|
||||
}
|
||||
|
||||
interface Props {
|
||||
toc: any
|
||||
video?: string
|
||||
}
|
||||
|
||||
const formatSlug = (slug: string) => {
|
||||
// [Joshen] We will still provide support for headers declared like this:
|
||||
// ## REST API {#rest-api-overview}
|
||||
// At least for now, this was a docusaurus thing.
|
||||
if (slug.includes('#')) return slug.split('-#')[1]
|
||||
return slug
|
||||
}
|
||||
|
||||
const formatTOCHeader = (content: string) => {
|
||||
let begin = false
|
||||
const res = []
|
||||
for (const x of content) {
|
||||
if (x === '`') {
|
||||
if (!begin) {
|
||||
begin = true
|
||||
res.push(`<code class="text-xs border rounded bg-surface-200 border-overlay">`)
|
||||
} else {
|
||||
begin = false
|
||||
res.push(`</code>`)
|
||||
}
|
||||
} else {
|
||||
res.push(x)
|
||||
}
|
||||
}
|
||||
return res.join('')
|
||||
}
|
||||
|
||||
const TableOfContents: FC<Props> = ({ toc, video }) => {
|
||||
// [Joshen] markdown-toc doesn't seem to read maxdepth from the options passed in
|
||||
// Our first level headers will be H2s (H1 is ignored), and we only show up to H3
|
||||
|
||||
return (
|
||||
<>
|
||||
{video && (
|
||||
<div className="video-container">
|
||||
<iframe
|
||||
src="https://www.youtube-nocookie.com/embed/0Fs96oZ4se0"
|
||||
frameBorder="1"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowFullScreen
|
||||
></iframe>
|
||||
</div>
|
||||
)}
|
||||
<ul className="toc-menu list-none pl-4 text-[0.8rem] grid gap-2 mt-12">
|
||||
{(toc.json as TOCHeader[])
|
||||
.filter((item) => item.lvl !== 1 && item.lvl <= 3)
|
||||
.map((item: any, i: number) => {
|
||||
return (
|
||||
<li key={i} id={item.lvl} style={{ marginLeft: `${(item.lvl - 2) * 1}rem` }}>
|
||||
<a
|
||||
href={`#${formatSlug(item.slug)}`}
|
||||
className="text-foreground-lighter hover:text-brand transition-colors"
|
||||
dangerouslySetInnerHTML={{ __html: formatTOCHeader(removeAnchor(item.content)) }}
|
||||
/>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default TableOfContents
|
||||
@@ -14,7 +14,7 @@ const useHash = () => {
|
||||
return () => {
|
||||
window.removeEventListener('hashchange', hashChangeHandler)
|
||||
}
|
||||
}, [])
|
||||
}, [hashChangeHandler])
|
||||
|
||||
const updateHash = useCallback(
|
||||
(newHash) => {
|
||||
@@ -23,7 +23,7 @@ const useHash = () => {
|
||||
[hash]
|
||||
)
|
||||
|
||||
return [hash, updateHash]
|
||||
return [hash, updateHash] as const
|
||||
}
|
||||
|
||||
export default useHash
|
||||
@@ -0,0 +1,16 @@
|
||||
import { useEffect, useReducer } from 'react'
|
||||
|
||||
/**
|
||||
* An escape hatch to manually trigger component rerenders in response to
|
||||
* external events, such as fired custom events.
|
||||
*/
|
||||
const useRerenderOnEvt = (event: string, listeningElem?: Document | Window | HTMLElement) => {
|
||||
const [, rerender] = useReducer((state) => !state, true)
|
||||
|
||||
useEffect(() => {
|
||||
;(listeningElem ?? window).addEventListener(event, rerender)
|
||||
return () => (listeningElem ?? window).removeEventListener(event, rerender)
|
||||
}, [event, listeningElem, rerender])
|
||||
}
|
||||
|
||||
export { useRerenderOnEvt }
|
||||
@@ -1,91 +1,6 @@
|
||||
import { MDXProvider } from '@mdx-js/react'
|
||||
import { NextSeo } from 'next-seo'
|
||||
import Head from 'next/head'
|
||||
import { useRouter } from 'next/router'
|
||||
import { FC, PropsWithChildren } from 'react'
|
||||
import components from '~/components'
|
||||
import TableOfContents from '~/components/TableOfContents'
|
||||
import { MainSkeleton } from './MainSkeleton'
|
||||
|
||||
interface Props {
|
||||
meta: {
|
||||
title: string
|
||||
description?: string
|
||||
hide_table_of_contents?: boolean
|
||||
video?: string
|
||||
tocVideo?: string
|
||||
canonical?: string
|
||||
}
|
||||
children: any
|
||||
toc?: any
|
||||
menuItems: any
|
||||
}
|
||||
|
||||
const Layout: FC<Props> = (props: Props) => {
|
||||
const { asPath } = useRouter()
|
||||
|
||||
const hasTableOfContents =
|
||||
props.toc !== undefined &&
|
||||
props.toc.json.filter((item) => item.lvl !== 1 && item.lvl <= 3).length > 0
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{asPath === '/' ? 'Supabase Docs' : `${props.meta?.title} | Supabase Docs`}</title>
|
||||
<meta name="description" content={props.meta?.description} />
|
||||
<meta property="og:image" content={`https://supabase.com/docs/img/supabase-og-image.png`} />
|
||||
<meta
|
||||
name="twitter:image"
|
||||
content={`https://supabase.com/docs/img/supabase-og-image.png`}
|
||||
/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
</Head>
|
||||
<NextSeo
|
||||
canonical={props.meta?.canonical ?? `https://supabase.com/docs${asPath}`}
|
||||
openGraph={{
|
||||
url: `https://supabase.com/docs${asPath}`,
|
||||
type: 'article',
|
||||
videos: props.meta?.video && [
|
||||
{
|
||||
// youtube based video meta
|
||||
url: props.meta?.video,
|
||||
width: 640,
|
||||
height: 385,
|
||||
type: 'application/x-shockwave-flash',
|
||||
},
|
||||
],
|
||||
article: {
|
||||
publishedTime: new Date().toISOString(),
|
||||
modifiedTime: new Date().toISOString(),
|
||||
authors: ['Supabase'],
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<LayoutMainContent>
|
||||
<div className={['relative transition-all ease-out', 'duration-150 '].join(' ')}>
|
||||
<article className="prose max-w-none">
|
||||
<h1>{props.meta.title}</h1>
|
||||
<MDXProvider components={components}>{props.children}</MDXProvider>
|
||||
</article>
|
||||
</div>
|
||||
{hasTableOfContents && !props.meta?.hide_table_of_contents && (
|
||||
<div
|
||||
className={[
|
||||
'border-default bg-background table-of-contents-height border-l',
|
||||
'thin-scrollbar overflow-y-auto sticky hidden md:block md:col-span-3 px-2',
|
||||
].join(' ')}
|
||||
>
|
||||
<TableOfContents toc={props.toc} video={props.meta.video} />
|
||||
</div>
|
||||
)}
|
||||
</LayoutMainContent>
|
||||
</>
|
||||
)
|
||||
}
|
||||
import { type FC, type PropsWithChildren } from 'react'
|
||||
|
||||
export const LayoutMainContent: FC<PropsWithChildren<{ className?: string }>> = ({
|
||||
className,
|
||||
children,
|
||||
}) => <div className={['max-w-7xl px-5 mx-auto py-16', className].join(' ')}>{children}</div>
|
||||
|
||||
export default Layout
|
||||
@@ -6,30 +6,22 @@ import { FC } from 'react'
|
||||
import components from '~/components'
|
||||
import HomePageCover from '~/components/HomePageCover'
|
||||
import { MenuId } from '~/components/Navigation/NavigationMenu/NavigationMenu'
|
||||
import TableOfContents from '~/components/TableOfContents'
|
||||
import { LayoutMainContent } from './DefaultLayout'
|
||||
import { MainSkeleton } from './MainSkeleton'
|
||||
|
||||
interface Props {
|
||||
meta: {
|
||||
title: string
|
||||
description?: string
|
||||
hide_table_of_contents?: boolean
|
||||
video?: string
|
||||
canonical?: string
|
||||
description?: string
|
||||
video?: string
|
||||
}
|
||||
children: any
|
||||
toc?: any
|
||||
menuItems: any
|
||||
}
|
||||
|
||||
const HomeLayout: FC<Props> = (props: Props) => {
|
||||
const { asPath } = useRouter()
|
||||
|
||||
const hasTableOfContents =
|
||||
props.toc !== undefined &&
|
||||
props.toc.json.filter((item) => item.lvl !== 1 && item.lvl <= 3).length > 0
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
@@ -72,16 +64,6 @@ const HomeLayout: FC<Props> = (props: Props) => {
|
||||
<MDXProvider components={components}>{props.children}</MDXProvider>
|
||||
</div>
|
||||
</div>
|
||||
{hasTableOfContents && !props.meta?.hide_table_of_contents && (
|
||||
<div
|
||||
className={[
|
||||
'border-default bg-background table-of-contents-height border-l',
|
||||
'thin-scrollbar overflow-y-auto sticky hidden md:block md:col-span-3 px-2',
|
||||
].join(' ')}
|
||||
>
|
||||
<TableOfContents toc={props.toc} video={props.meta.video} />
|
||||
</div>
|
||||
)}
|
||||
</LayoutMainContent>
|
||||
</article>
|
||||
</MainSkeleton>
|
||||
|
||||
@@ -1,22 +1,18 @@
|
||||
import { MDXProvider } from '@mdx-js/react'
|
||||
import 'katex/dist/katex.min.css'
|
||||
import { ExternalLink } from 'lucide-react'
|
||||
import { NextSeo } from 'next-seo'
|
||||
import { usePathname } from 'next/navigation'
|
||||
import { useRouter } from 'next/router'
|
||||
import { FC, useEffect, useRef, useState } from 'react'
|
||||
import { type FC } from 'react'
|
||||
|
||||
import { IconExternalLink, cn } from 'ui'
|
||||
import { ExpandableVideo } from 'ui-patterns/ExpandableVideo'
|
||||
import { cn } from 'ui'
|
||||
|
||||
import components from '~/components'
|
||||
import { highlightSelectedTocItem } from '~/components/CustomHTMLElements/CustomHTMLElements.utils'
|
||||
import { FooterHelpCalloutType } from '~/components/FooterHelpCallout'
|
||||
import GuidesTableOfContents from '~/components/GuidesTableOfContents'
|
||||
import { type MenuId } from '~/components/Navigation/NavigationMenu/NavigationMenu'
|
||||
import useHash from '~/hooks/useHash'
|
||||
import { Feedback } from '~/components/Feedback'
|
||||
import { LayoutMainContent } from '~/layouts/DefaultLayout'
|
||||
import { MainSkeleton } from '~/layouts/MainSkeleton'
|
||||
import 'katex/dist/katex.min.css'
|
||||
|
||||
interface Props {
|
||||
meta: {
|
||||
@@ -39,12 +35,6 @@ interface Props {
|
||||
}
|
||||
|
||||
const Layout: FC<Props> = (props) => {
|
||||
const pathname = usePathname()
|
||||
const [hash] = useHash()
|
||||
|
||||
const articleRef = useRef()
|
||||
const [tocList, setTocList] = useState([])
|
||||
|
||||
const { asPath } = useRouter()
|
||||
const router = useRouter()
|
||||
|
||||
@@ -52,31 +42,6 @@ const Layout: FC<Props> = (props) => {
|
||||
|
||||
const EDIT_BUTTON_EXCLUDE_LIST = ['/404']
|
||||
|
||||
useEffect(() => {
|
||||
if (hash && tocList.length > 0) {
|
||||
highlightSelectedTocItem(hash as string)
|
||||
}
|
||||
}, [hash, JSON.stringify(tocList)])
|
||||
|
||||
useEffect(() => {
|
||||
const articleEl = articleRef.current as HTMLElement
|
||||
|
||||
if (!articleRef.current) return
|
||||
const headings = Array.from(articleEl.querySelectorAll('h2, h3'))
|
||||
const newHeadings = headings
|
||||
.filter((heading) => heading.id)
|
||||
.map((heading) => {
|
||||
const text = heading.textContent.replace('#', '')
|
||||
const link = heading.querySelector('a').getAttribute('href')
|
||||
const level = heading.tagName === 'H2' ? 2 : 3
|
||||
return { text, link, level }
|
||||
})
|
||||
setTocList(newHeadings)
|
||||
}, [pathname]) // needed to recalculate the toc when path changes
|
||||
|
||||
const hasTableOfContents = tocList.length > 0
|
||||
const tocVideoPreview = `http://img.youtube.com/vi/${props.meta?.tocVideo}/0.jpg`
|
||||
|
||||
// page type, ie, Auth, Database, Storage etc
|
||||
const ogPageType = asPath.split('/')[2]
|
||||
// open graph image url constructor
|
||||
@@ -141,10 +106,9 @@ const Layout: FC<Props> = (props) => {
|
||||
<p className="text-brand tracking-wider mb-3">{props.meta.breadcrumb}</p>
|
||||
)}
|
||||
<article
|
||||
ref={articleRef}
|
||||
className={`${
|
||||
props.meta?.hide_table_of_contents || !hasTableOfContents ? '' : ''
|
||||
} prose max-w-none`}
|
||||
// Used to get headings for the table of contents
|
||||
id="sb-docs-guide-main-article"
|
||||
className="prose max-w-none"
|
||||
>
|
||||
<h1 className="mb-0">{props.meta.title}</h1>
|
||||
{props.meta?.subtitle && (
|
||||
@@ -166,35 +130,23 @@ const Layout: FC<Props> = (props) => {
|
||||
`}
|
||||
className="text-sm transition flex items-center gap-1 text-scale-1000 hover:text-scale-1200 w-fit"
|
||||
>
|
||||
Edit this page on GitHub <IconExternalLink size={14} strokeWidth={1.5} />
|
||||
Edit this page on GitHub <ExternalLink size={14} strokeWidth={1.5} />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</article>
|
||||
</div>
|
||||
{!props.hideToc && hasTableOfContents && !props.meta?.hide_table_of_contents && (
|
||||
<div
|
||||
{!props.hideToc && !props.meta?.hide_table_of_contents && (
|
||||
<GuidesTableOfContents
|
||||
video={props.meta?.tocVideo}
|
||||
className={cn(
|
||||
'col-span-3 self-start',
|
||||
'border-overlay bg-background',
|
||||
'thin-scrollbar overflow-y-auto hidden md:block md:col-span-3 px-2',
|
||||
'sticky top-[calc(var(--mobile-header-height,40px)+2rem)] lg:top-[calc(var(--desktop-header-height,60px)+2rem)]'
|
||||
'hidden md:block md:col-span-3',
|
||||
'sticky top-[calc(var(--mobile-header-height,40px)+2rem)] lg:top-[calc(var(--desktop-header-height,60px)+2rem)]',
|
||||
'max-h-[calc(100vh-60px-5rem)]'
|
||||
)}
|
||||
>
|
||||
<div className="border-l">
|
||||
{props.meta?.tocVideo && !!tocVideoPreview && (
|
||||
<div className="relative mb-6 pl-5">
|
||||
<ExpandableVideo imgUrl={tocVideoPreview} videoId={props.meta.tocVideo} />
|
||||
</div>
|
||||
)}
|
||||
<Feedback key={pathname} />
|
||||
<span className="block font-mono text-xs uppercase text-foreground px-5 mb-6">
|
||||
On this page
|
||||
</span>
|
||||
<GuidesTableOfContents list={tocList} />
|
||||
</div>
|
||||
</div>
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</LayoutMainContent>
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
import { MDXProvider } from '@mdx-js/react'
|
||||
import Head from 'next/head'
|
||||
import { FC, useEffect, useState } from 'react'
|
||||
import components from '~/components'
|
||||
import SideBar from '~/components/Navigation/SideBar'
|
||||
import TableOfContents from '~/components/TableOfContents'
|
||||
|
||||
interface Props {
|
||||
meta: {
|
||||
title: string
|
||||
description?: string
|
||||
hide_table_of_contents?: boolean
|
||||
video?: string
|
||||
tocVideo?: string
|
||||
}
|
||||
children: any
|
||||
toc?: any
|
||||
menuItems: any
|
||||
currentPage: string
|
||||
}
|
||||
|
||||
const Layout: FC<Props> = (props: Props) => {
|
||||
const [active, setActive] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(function () {
|
||||
setActive(true)
|
||||
}, 150)
|
||||
}, [])
|
||||
|
||||
// const contentString = renderToString(props.children)
|
||||
|
||||
// const content = serialize(contentString || '')
|
||||
|
||||
// console.log('contentString', contentString)
|
||||
|
||||
// const _toc = toc('#hello world', { maxdepth: 1, firsth1: false })
|
||||
|
||||
const hasTableOfContents =
|
||||
props.toc !== undefined &&
|
||||
props.toc.json.filter((item) => item.lvl !== 1 && item.lvl <= 3).length > 0
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{props.meta?.title} | Supabase</title>
|
||||
<meta name="description" content={props.meta?.description} />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="icon" href="/docs/favicon.ico" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:site_name" content={props.meta?.title} />
|
||||
<meta property="og:description" content={props.meta?.description} />
|
||||
<meta property="og:title" content={props.meta?.title} />
|
||||
</Head>
|
||||
|
||||
<div
|
||||
className={[
|
||||
'relative transition-all ease-out',
|
||||
'duration-150',
|
||||
active ? 'opacity-100 left-0' : 'opacity-0 left-10',
|
||||
].join(' ')}
|
||||
>
|
||||
<div>
|
||||
<p className="text-brand tracking-wider">Tutorials</p>
|
||||
<article className={['prose', 'max-w-none'].join(' ')}>
|
||||
<h1>{props.meta.title}</h1>
|
||||
<div className="max-w-xs w-32 h-[1px] bg-gradient-to-r from-brand-300 to-brand my-16"></div>
|
||||
|
||||
<MDXProvider components={components} children={props.children} />
|
||||
</article>
|
||||
</div>
|
||||
{hasTableOfContents && !props.meta?.hide_table_of_contents && (
|
||||
<div
|
||||
className={[
|
||||
'border-overlay bg-background table-of-contents-height border-l',
|
||||
'thin-scrollbar overflow-y-auto sticky hidden md:block md:col-span-3 px-2',
|
||||
].join(' ')}
|
||||
>
|
||||
<TableOfContents toc={props.toc} video={props.meta.video} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Layout
|
||||
@@ -25,7 +25,7 @@
|
||||
"@next/bundle-analyzer": "^14.1.4",
|
||||
"@next/mdx": "^14.1.4",
|
||||
"@octokit/auth-app": "^6.0.3",
|
||||
"@octokit/core": "^5.0.2",
|
||||
"@octokit/core": "^5.1.0",
|
||||
"@octokit/graphql": "^7.0.2",
|
||||
"@octokit/plugin-paginate-graphql": "^4.0.0",
|
||||
"@radix-ui/react-accordion": "^1.1.2",
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import specFile from '~/spec/cli_v1_config.yaml' assert { type: 'yml' }
|
||||
import { Parameter } from '~/lib/refGenerator/refTypes'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import GuidesTableOfContents from '~/components/GuidesTableOfContents'
|
||||
import { Heading } from '~/components/CustomHTMLElements'
|
||||
import Head from 'next/head'
|
||||
import { CodeBlock } from 'ui'
|
||||
import { MainSkeleton } from '~/layouts/MainSkeleton'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
|
||||
import { CodeBlock, cn } from 'ui'
|
||||
|
||||
import { Heading } from '~/components/CustomHTMLElements'
|
||||
import GuidesTableOfContents from '~/components/GuidesTableOfContents'
|
||||
import { MenuId } from '~/components/Navigation/NavigationMenu/NavigationMenu'
|
||||
import { MainSkeleton } from '~/layouts/MainSkeleton'
|
||||
import { Parameter } from '~/lib/refGenerator/refTypes'
|
||||
import specFile from '~/spec/cli_v1_config.yaml' assert { type: 'yml' }
|
||||
|
||||
// Parameters are grouped on the page by tag
|
||||
const tocList = []
|
||||
@@ -41,14 +43,15 @@ export default function Config() {
|
||||
<div>{content}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="md:col-span-3">
|
||||
<div className="sticky overflow-auto max-h-[calc(100vh-60px-5rem)] pb-3 top-32 border-l">
|
||||
<span className="block font-mono text-xs uppercase text-foreground pl-5 mb-4">
|
||||
On this page
|
||||
</span>
|
||||
<GuidesTableOfContents list={tocList} />
|
||||
</div>
|
||||
</div>
|
||||
<GuidesTableOfContents
|
||||
overrideToc={tocList}
|
||||
className={cn(
|
||||
'col-span-3 self-start',
|
||||
'hidden md:block md:col-span-3',
|
||||
'sticky top-[calc(var(--mobile-header-height,40px)+2rem)] lg:top-[calc(var(--desktop-header-height,60px)+2rem)]',
|
||||
'max-h-[calc(100vh-60px-5rem)]'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</MainSkeleton>
|
||||
</>
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
import { CodeHikeConfig, remarkCodeHike } from '@code-hike/mdx'
|
||||
import { Octokit } from '@octokit/core'
|
||||
import { capitalize } from 'lodash'
|
||||
import { GetStaticProps, InferGetStaticPropsType } from 'next'
|
||||
import { MDXRemote } from 'next-mdx-remote'
|
||||
import { serialize } from 'next-mdx-remote/serialize'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
import rehypeSlug from 'rehype-slug'
|
||||
|
||||
import codeHikeTheme from 'config/code-hike.theme.json' assert { type: 'json' }
|
||||
import { Tabs } from 'ui'
|
||||
|
||||
import components from '~/components'
|
||||
import { Heading } from '~/components/CustomHTMLElements'
|
||||
import { MenuId } from '~/components/Navigation/NavigationMenu/NavigationMenu'
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
import { UrlTransformFunction, linkTransform } from '~/lib/mdx/plugins/rehypeLinkTransform'
|
||||
import remarkMkDocsAdmonition from '~/lib/mdx/plugins/remarkAdmonition'
|
||||
import { removeTitle } from '~/lib/mdx/plugins/remarkRemoveTitle'
|
||||
import remarkPyMdownTabs from '~/lib/mdx/plugins/remarkTabs'
|
||||
|
||||
// We fetch these docs at build time from an external repo
|
||||
export const org = 'supabase'
|
||||
export const repo = 'splinter'
|
||||
export const branch = 'main'
|
||||
export const docsDir = 'docs'
|
||||
|
||||
const meta = {
|
||||
title: 'Database Linter',
|
||||
subtitle: 'Check your database for performance and security issues',
|
||||
}
|
||||
|
||||
const editLink = 'https://github.com/supabase/splinter/tree/main/docs'
|
||||
|
||||
const markdownIntro = `
|
||||
You can use the Project Linter to check your database for issues such as missing indexes and improperly set-up RLS policies.
|
||||
|
||||
## Using the linter
|
||||
|
||||
In the dashboard, navigate to [Database Linter](/dashboard/project/_/database/linter) under Database. The linter runs automatically. You can also manually rerun it after you're resolved issues, or ignore individual lints.
|
||||
`.trim()
|
||||
|
||||
const getBasename = (path: string) => path.split('/').at(-1).replace(/\.md$/, '')
|
||||
|
||||
export default function ProjectLinterDocs({
|
||||
intro,
|
||||
lints,
|
||||
}: InferGetStaticPropsType<typeof getStaticProps>) {
|
||||
return (
|
||||
<Layout menuId={MenuId.Database} meta={meta} editLink={editLink}>
|
||||
<MDXRemote {...intro} components={components} />
|
||||
<Heading tag="h2">Available lints</Heading>
|
||||
<Tabs listClassNames="flex flex-wrap gap-2 [&>button]:!m-0" queryGroup="lint">
|
||||
{lints.map((lint) => (
|
||||
<Tabs.Panel
|
||||
key={lint.path}
|
||||
id={lint.path}
|
||||
label={capitalize(getBasename(lint.path).replace(/_/g, ' '))}
|
||||
>
|
||||
<section id={getBasename(lint.path)}>
|
||||
<MDXRemote {...lint.content} components={components} />
|
||||
</section>
|
||||
</Tabs.Panel>
|
||||
))}
|
||||
</Tabs>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* The GitHub repo uses relative links, which don't lead to the right locations
|
||||
* in docs.
|
||||
*
|
||||
* @param url The original link, as written in the Markdown file
|
||||
* @returns The rewritten link
|
||||
*/
|
||||
const urlTransform: (lints: Array<{ path: string }>) => UrlTransformFunction = (lints) => (url) => {
|
||||
try {
|
||||
const placeholderHostname = 'placeholder'
|
||||
const { hostname, pathname, hash } = new URL(url, `http://${placeholderHostname}`)
|
||||
|
||||
// Don't modify a url with a FQDN or a url that's only a hash
|
||||
if (hostname !== placeholderHostname || pathname === '/') {
|
||||
return url
|
||||
}
|
||||
|
||||
const relativePath = getBasename(pathname)
|
||||
const section = lints.find(({ path }) => path === relativePath)
|
||||
|
||||
if (section) {
|
||||
const url = new URL(window.location.href)
|
||||
url.searchParams.set('lint', relativePath)
|
||||
return url.toString()
|
||||
}
|
||||
|
||||
// If we don't have this page in our docs, link to GitHub repo
|
||||
return `https://github.com/${org}/${repo}/blob/${branch}${pathname}${hash}`
|
||||
} catch (err) {
|
||||
console.error('Error transforming markdown URL', err)
|
||||
return url
|
||||
}
|
||||
}
|
||||
|
||||
const transformMarkdown = async (
|
||||
rawContent: string,
|
||||
{ replacementLinks = [] }: { replacementLinks?: Array<{ path: string }> } = {}
|
||||
) => {
|
||||
const codeHikeOptions: CodeHikeConfig = {
|
||||
theme: codeHikeTheme,
|
||||
lineNumbers: true,
|
||||
showCopyButton: true,
|
||||
skipLanguages: [],
|
||||
autoImport: false,
|
||||
}
|
||||
|
||||
const content = await serialize(rawContent, {
|
||||
scope: {
|
||||
chCodeConfig: codeHikeOptions,
|
||||
},
|
||||
mdxOptions: {
|
||||
remarkPlugins: [
|
||||
remarkGfm,
|
||||
remarkMkDocsAdmonition,
|
||||
remarkPyMdownTabs,
|
||||
[removeTitle, meta.title],
|
||||
[remarkCodeHike, codeHikeOptions],
|
||||
],
|
||||
rehypePlugins: [[linkTransform, urlTransform(replacementLinks)], rehypeSlug],
|
||||
},
|
||||
})
|
||||
|
||||
return content
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch markdown from external repo and transform links
|
||||
*/
|
||||
export const getStaticProps = (async () => {
|
||||
const octokit = new Octokit()
|
||||
|
||||
const response = await octokit.request('GET /repos/{owner}/{repo}/contents/{path}', {
|
||||
owner: org,
|
||||
repo: repo,
|
||||
path: docsDir,
|
||||
ref: branch,
|
||||
headers: {
|
||||
'X-GitHub-Api-Version': '2022-11-28',
|
||||
},
|
||||
})
|
||||
|
||||
if (response.status >= 400) {
|
||||
throw Error(`Could not get contents of repo ${org}/${repo}`)
|
||||
}
|
||||
|
||||
if (!Array.isArray(response.data)) {
|
||||
throw Error(
|
||||
`Reading a directory, not a file. Should not reach this, solely to appease Typescript.`
|
||||
)
|
||||
}
|
||||
|
||||
const [intro, ...lints] = await Promise.all([
|
||||
await transformMarkdown(markdownIntro),
|
||||
...response.data
|
||||
.filter(({ path }) => /docs\/\d+.+\.md$/.test(path))
|
||||
.map(async ({ path }, _, data) => {
|
||||
const fileResponse = await fetch(
|
||||
`https://raw.githubusercontent.com/${org}/${repo}/${branch}/${path}`
|
||||
)
|
||||
|
||||
if (fileResponse.status >= 400) {
|
||||
throw Error(`Could not get contents of file ${org}/${repo}/${path}`)
|
||||
}
|
||||
|
||||
const rawContent = await fileResponse.text()
|
||||
const content = await transformMarkdown(rawContent, { replacementLinks: data })
|
||||
|
||||
return {
|
||||
path: getBasename(path),
|
||||
content,
|
||||
}
|
||||
}),
|
||||
])
|
||||
|
||||
return {
|
||||
props: {
|
||||
intro,
|
||||
lints,
|
||||
},
|
||||
}
|
||||
}) satisfies GetStaticProps
|
||||
Generated
+4
-4
@@ -230,7 +230,7 @@
|
||||
"@next/bundle-analyzer": "^14.1.4",
|
||||
"@next/mdx": "^14.1.4",
|
||||
"@octokit/auth-app": "^6.0.3",
|
||||
"@octokit/core": "^5.0.2",
|
||||
"@octokit/core": "^5.1.0",
|
||||
"@octokit/graphql": "^7.0.2",
|
||||
"@octokit/plugin-paginate-graphql": "^4.0.0",
|
||||
"@radix-ui/react-accordion": "^1.1.2",
|
||||
@@ -8378,9 +8378,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/core": {
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.0.2.tgz",
|
||||
"integrity": "sha512-cZUy1gUvd4vttMic7C0lwPed8IYXWYp8kHIMatyhY8t8n3Cpw2ILczkV5pGMPqef7v0bLo0pOHrEHarsau2Ydg==",
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.1.0.tgz",
|
||||
"integrity": "sha512-BDa2VAMLSh3otEiaMJ/3Y36GU4qf6GI+VivQ/P41NC6GHcdxpKlqV0ikSZ5gdQsmS3ojXeRx5vasgNTinF0Q4g==",
|
||||
"dependencies": {
|
||||
"@octokit/auth-token": "^4.0.0",
|
||||
"@octokit/graphql": "^7.0.0",
|
||||
|
||||
@@ -375,3 +375,6 @@ export * from './src/components/Icon/icons/IconZap'
|
||||
|
||||
// Export hooks
|
||||
export * from './src/lib/Hooks'
|
||||
|
||||
// Shared constants/utils
|
||||
export { TAB_CHANGE_EVENT_NAME } from './src/lib/events'
|
||||
@@ -81,7 +81,11 @@ const Tabs: React.FC<PropsWithChildren<TabsProps>> & TabsSubComponents = ({
|
||||
*/
|
||||
|
||||
function handleChange(e: CustomEvent) {
|
||||
if (e.detail.queryGroup === queryGroup && tabIds.includes(e.detail.id)) {
|
||||
if (
|
||||
e.detail.queryGroup &&
|
||||
e.detail.queryGroup === queryGroup &&
|
||||
tabIds.includes(e.detail.id)
|
||||
) {
|
||||
setActiveTab(e.detail.id)
|
||||
setGroupActiveId?.(e.detail.id)
|
||||
}
|
||||
@@ -113,11 +117,12 @@ const Tabs: React.FC<PropsWithChildren<TabsProps>> & TabsSubComponents = ({
|
||||
const url = new URL(document.location.href)
|
||||
url.searchParams.set(queryGroup, id)
|
||||
window.history.replaceState(undefined, '', url)
|
||||
currentTarget.dispatchEvent(
|
||||
new CustomEvent(TAB_CHANGE_EVENT_NAME, { bubbles: true, detail: { queryGroup, id } })
|
||||
)
|
||||
}
|
||||
|
||||
currentTarget.dispatchEvent(
|
||||
new CustomEvent(TAB_CHANGE_EVENT_NAME, { bubbles: true, detail: { queryGroup, id } })
|
||||
)
|
||||
|
||||
onClick?.(id)
|
||||
if (id !== active) {
|
||||
onChange?.(id)
|
||||
|
||||
Reference in new issue
Block a user