diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 5d9f46f73b2..57849f5e09a 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,4 +1,4 @@ /studio/ @supabase/frontend /apps/www/ @supabase/frontend -/apps/reference/ @supabase/docs +/apps/docs/ @supabase/docs /spec/ @supabase/docs diff --git a/.github/workflows/docs-compilation-check.yml b/.github/workflows/docs-compilation-check.yml deleted file mode 100644 index 0c41676a3a6..00000000000 --- a/.github/workflows/docs-compilation-check.yml +++ /dev/null @@ -1,42 +0,0 @@ -# This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node -# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions - -name: Docs Compilation Check - -on: - pull_request: - branches: [master] - paths: - - 'apps/reference/**' - -jobs: - build: - runs-on: ubuntu-latest - - strategy: - matrix: - node-version: [16.x] - # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ - - steps: - - uses: actions/checkout@v2 - - # Ref: https://github.com/actions/setup-node/issues/214 - - name: Reconfigure git to use HTTP authentication - run: > - git config --global url."https://github.com/".insteadOf - ssh://git@github.com/ - - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v2 - with: - node-version: ${{ matrix.node-version }} - cache: 'npm' - - - name: Install Deps - run: npm ci - working-directory: ./apps/reference - - - name: Build - run: npm run build - working-directory: ./apps/reference diff --git a/.github/workflows/publish_image.yml b/.github/workflows/publish_image.yml index 8ed1db87295..84c8b8a88c2 100644 --- a/.github/workflows/publish_image.yml +++ b/.github/workflows/publish_image.yml @@ -2,6 +2,8 @@ name: Publish to Image Registry on: push: + branches: + - 'studio' tags: - '*' workflow_dispatch: @@ -25,6 +27,7 @@ jobs: latest=false tags: | type=ref,event=tag + type=sha,prefix={{date 'YYYYMMDD'}},enable=${{ github.ref_type == 'branch' }} type=raw,value=${{ inputs.version }},enable=${{ github.event_name != 'push' }} - uses: docker/setup-qemu-action@v2 diff --git a/DEVELOPERS.md b/DEVELOPERS.md index 5a1476143f1..0d23fbd42f6 100644 --- a/DEVELOPERS.md +++ b/DEVELOPERS.md @@ -65,7 +65,7 @@ Choose if you want to work on the [Supabase Website](https://supabase.com), [Sup Go to the [Supabase Docs](https://supabase.com/docs) directory ```sh - cd apps/reference + cd apps/docs ``` Go to the [Supabase Studio](https://app.supabase.com) directory @@ -121,16 +121,16 @@ The website is moving to a new monorepo setup. See the [Monorepo](#monorepo) sec npm ```sh - npm run start + npm run dev ``` or with yarn ```sh - yarn start + yarn dev ``` -1. Access the local server in your web browser at http://localhost:3010/docs. +1. Access the local server in your web browser at http://localhost:3001/docs. ### Supabase Studio @@ -183,7 +183,7 @@ Then edit and visit any of the following sites: Site | Directory | Description | Local development server ---- | --------- | ----------- | ------------------------ [supabase.com](https://supabase.com) | `/apps/www` | The main website | http://localhost:3000 -[supabase.com/docs](https://supabase.com/docs) | `apps/reference` | Guides and Reference documentation | http://localhost:3010/docs +[supabase.com/docs](https://supabase.com/docs) | `apps/docs` | Guides and Reference documentation | http://localhost:3001/docs [POC] Community forum | `/apps/temp-community-forum` | GitHub Discussions in a Next.js site | http://localhost:3002 [POC] DEV articles site | `/apps/temp-community-tutorials` | A Next.js site for our DEV articles (which community members can write) | http://localhost:3003 diff --git a/apps/temp-docs/.eslintrc.js b/apps/docs/.eslintrc.js similarity index 100% rename from apps/temp-docs/.eslintrc.js rename to apps/docs/.eslintrc.js diff --git a/apps/temp-docs/.gitignore b/apps/docs/.gitignore similarity index 100% rename from apps/temp-docs/.gitignore rename to apps/docs/.gitignore diff --git a/apps/docs/README.md b/apps/docs/README.md new file mode 100644 index 00000000000..7ae88ade7d0 --- /dev/null +++ b/apps/docs/README.md @@ -0,0 +1,45 @@ +# Reference Docs + +Supabase Reference Docs + +## Maintainers + +If you are a maintainer of any tools in the Supabase ecosystem, you can use this site to provide documentation for the tools & libraries that you maintain. + +## Types of docs + +There are many types of docs: + +1. Guides: teach developers how to use a product. "I have XX problem, how do I solve it?" +2. Tutorials: walk-throughs, have a large outcome. "Build a React application with Supabase". +3. Explanations: teach developers about a broad topic. "What is a database?" +4. Reference: technical descriptions of tools and how to use them. "What errors does the API return?" + +In these docs, you should focus only on the fourth type: "Reference Docs". + +## Versioning + +All tools have versioned docs, which are kept in separate folders. For example, the CLI has the following folders and files: + +- `cli`: the "next" release. +- `cli_spec`: contains the DocSpec for the "next" release (see below). +- `cli_versioned_docs`: a version of the documentation for every release (including the most current version). +- `cli_versioned_sidebars`: a version of the sidebar for every release (including the most current version). + +When you release a new version of a tool, you should also release a new version of the docs. You can do this via the command line. For example, if you just released the CLI version `1.0.1`: + +``` +npm run cli:version 1.0.1 +``` + +## DocSpec + +We use documentation specifications which can be used to generate human-readable docs. + +- OpenAPI: for documenting API endpoints. +- SDKSpec (custom to Supabase): for SDKs and client libraries. +- ConfigSpec (custom to Supabase): for configuration options. +- CLISpec (custom to Supabase): for CLI commands and usage. + +The benefit of using custom specifications is that we can generate many other types from a strict schema (eg, HTML and manpages). +It also means that we can switch any documentation system we want. On this site we use Next.JS, but in Supabase's official website we use a custom React site and expose only a subset of the available API for each tool. diff --git a/apps/temp-docs/components/Admonition.tsx b/apps/docs/components/Admonition.tsx similarity index 87% rename from apps/temp-docs/components/Admonition.tsx rename to apps/docs/components/Admonition.tsx index 39b6b03c044..42f7049f864 100644 --- a/apps/temp-docs/components/Admonition.tsx +++ b/apps/docs/components/Admonition.tsx @@ -14,13 +14,13 @@ const Admonition: FC = ({ type = 'note', label, children }) => { 'shadow p-4 rounded border-l-[5px] space-y-2 my-4', `${ type === 'note' - ? 'bg-scale-500 border-scale-800' + ? 'bg-scale-400 border-scale-800' : type === 'info' ? 'bg-scale-500 border-scale-800' : type === 'tip' - ? 'bg-brand-500 border-brand-800' + ? 'bg-brand-300 border-brand-800' : type === 'caution' - ? 'bg-yellow-500 border-yellow-800' + ? 'bg-yellow-400 border-yellow-800' : type === 'danger' ? 'bg-red-500 border-red-800' : 'bg-scale-500 border-scale-800' @@ -41,7 +41,7 @@ const Admonition: FC = ({ type = 'note', label, children }) => { ) : ( )} -

{label || type}

+

{label || type}

{children}
diff --git a/apps/temp-docs/components/AuthProviders.tsx b/apps/docs/components/AuthProviders.tsx similarity index 82% rename from apps/temp-docs/components/AuthProviders.tsx rename to apps/docs/components/AuthProviders.tsx index f4f9197af3a..a1903636904 100644 --- a/apps/temp-docs/components/AuthProviders.tsx +++ b/apps/docs/components/AuthProviders.tsx @@ -3,20 +3,12 @@ import ButtonCard from './ButtonCard' export default function AuthProviders() { return ( -
+
{providers.map((x) => (
-
- {/* {x.logo && {x.name}} */} +

{x.name}

{x.official ? ( diff --git a/apps/temp-docs/components/ButtonCard.tsx b/apps/docs/components/ButtonCard.tsx similarity index 74% rename from apps/temp-docs/components/ButtonCard.tsx rename to apps/docs/components/ButtonCard.tsx index c3cf3f5da8f..d1003ed12bc 100644 --- a/apps/temp-docs/components/ButtonCard.tsx +++ b/apps/docs/components/ButtonCard.tsx @@ -1,20 +1,29 @@ -import React from 'react' +import React, { FC, ReactNode } from 'react' import Link from 'next/link' import Image from 'next/image' -export default function ButtonCard({ +interface Props { + title: string + description?: string + to: string + icon?: string | ReactNode + children?: ReactNode + layout?: 'vertical' | 'horizontal' +} + +const ButtonCard: FC = ({ children = undefined, icon = undefined, title, description = '', to, layout = 'vertical', -}) { +}) => { return ( @@ -42,3 +51,5 @@ export default function ButtonCard({ ) } + +export default ButtonCard diff --git a/apps/temp-docs/components/CodeBlock/CodeBlock.tsx b/apps/docs/components/CodeBlock/CodeBlock.tsx similarity index 57% rename from apps/temp-docs/components/CodeBlock/CodeBlock.tsx rename to apps/docs/components/CodeBlock/CodeBlock.tsx index 6c11934248e..1ca6b128c12 100644 --- a/apps/temp-docs/components/CodeBlock/CodeBlock.tsx +++ b/apps/docs/components/CodeBlock/CodeBlock.tsx @@ -1,7 +1,8 @@ +import { FC } from 'react' +import CopyToClipboard from 'react-copy-to-clipboard' import { Light as SyntaxHighlighter } from 'react-syntax-highlighter' import monokaiCustomTheme from './CodeBlock.utils' import { Button, IconCheck, IconCopy } from 'ui' -import CopyToClipboard from 'react-copy-to-clipboard' import js from 'react-syntax-highlighter/dist/cjs/languages/hljs/javascript' import ts from 'react-syntax-highlighter/dist/cjs/languages/hljs/typescript' @@ -14,16 +15,26 @@ import { useState } from 'react' import { useTheme } from '../Providers' interface Props { - lang: 'js' | 'jsx' | 'sql' | 'py' | 'bash' | 'ts' | 'dart' - startingLineNumber?: number + title?: string + language: 'js' | 'jsx' | 'sql' | 'py' | 'bash' | 'ts' | 'dart' + linesToHighlight?: number[] hideCopy?: boolean + hideLineNumbers?: boolean className?: string - children?: string - size?: 'small' | 'medium' | 'large' value?: string + children?: string } -function CodeBlock(props: Props) { +const CodeBlock: FC = ({ + title, + language, + linesToHighlight = [], + className, + value, + children, + hideCopy = false, + hideLineNumbers = false, +}) => { const { isDarkMode } = useTheme() const monokaiTheme = monokaiCustomTheme(isDarkMode) @@ -36,11 +47,7 @@ function CodeBlock(props: Props) { }, 1000) } - let lang = props.lang - ? props.lang - : props.className - ? props.className.replace('language-', '') - : 'js' + let lang = language ? language : className ? className.replace('language-', '') : 'js' // force jsx to be js highlighted if (lang === 'jsx') lang = 'js' SyntaxHighlighter.registerLanguage('js', js) @@ -50,26 +57,40 @@ function CodeBlock(props: Props) { SyntaxHighlighter.registerLanguage('bash', bash) SyntaxHighlighter.registerLanguage('dart', dart) - // const large = props.size === 'large' ? true : false const large = false - // don't show line numbers if bash == lang - const showLineNumbers = lang !== 'bash' + const showLineNumbers = hideLineNumbers || lang !== 'bash' return ( -

- {props.className ? ( +
+ {title && ( +
+ {title.replace(/%20/g, ' ')} +
+ )} + {className ? ( { + if (linesToHighlight.includes(lineNumber)) { + return { + style: { display: 'block', backgroundColor: 'var(--colors-scale6)' }, + } + } + return {} + }} lineNumberContainerStyle={{ paddingTop: '128px', }} @@ -85,20 +106,26 @@ function CodeBlock(props: Props) { paddingBottom: '4px', }} > - {(props.value || props.children)?.trimEnd()} + {(value || children)?.trimEnd()} ) : ( - {props.value || props.children} + {value || children} )} - {!props.hideCopy && (props.value || props.children) && props.className ? ( -
- + {!hideCopy && (value || children) && className ? ( +
+
diff --git a/apps/temp-docs/components/CodeBlock/CodeBlock.utils.js b/apps/docs/components/CodeBlock/CodeBlock.utils.js similarity index 72% rename from apps/temp-docs/components/CodeBlock/CodeBlock.utils.js rename to apps/docs/components/CodeBlock/CodeBlock.utils.js index e4e6aba3e26..48b62cffc1e 100644 --- a/apps/temp-docs/components/CodeBlock/CodeBlock.utils.js +++ b/apps/docs/components/CodeBlock/CodeBlock.utils.js @@ -1,3 +1,32 @@ +// https://github.com/euank/node-parse-numeric-range/blob/master/index.js +export function parseNumericRange(string) { + let res = [] + let m + + for (let str of string.split(',').map((str) => str.trim())) { + // just a number + if (/^-?\d+$/.test(str)) { + res.push(parseInt(str, 10)) + } else if ((m = str.match(/^(-?\d+)(-|\.\.\.?|\u2025|\u2026|\u22EF)(-?\d+)$/))) { + // 1-5 or 1..5 (equivalent) or 1...5 (doesn't include 5) + let [_, lhs, sep, rhs] = m + + if (lhs && rhs) { + lhs = parseInt(lhs) + rhs = parseInt(rhs) + const incr = lhs < rhs ? 1 : -1 + + // Make it inclusive by moving the right 'stop-point' away by one. + if (sep === '-' || sep === '..' || sep === '\u2025') rhs += incr + + for (let i = lhs; i !== rhs; i += incr) res.push(i) + } + } + } + + return res +} + const monokaiCustomTheme = (isDarkMode) => { return { hljs: { diff --git a/apps/docs/components/CustomHTMLElements/CustomHTMLElements.utils.ts b/apps/docs/components/CustomHTMLElements/CustomHTMLElements.utils.ts new file mode 100644 index 00000000000..e1bc479e468 --- /dev/null +++ b/apps/docs/components/CustomHTMLElements/CustomHTMLElements.utils.ts @@ -0,0 +1,79 @@ +// Check if heading has custom anchor first, before forming the anchor based on the title +export const getAnchor = (text: any): string | undefined => { + if (typeof text === 'object') { + if (Array.isArray(text)) { + const customAnchor = text.find( + (x) => typeof x === 'string' && x.includes('{#') && x.endsWith('}') + ) + if (customAnchor !== undefined) return customAnchor.slice(3, customAnchor.indexOf('}')) + + const formattedText = text + .map((x) => { + if (typeof x !== 'string') return x.props.children + else return x.trim() + }) + .map((x) => { + if (typeof x !== 'string') return x + else + return x + .toLowerCase() + .replace(/[^a-z0-9- ]/g, '') + .replace(/[ ]/g, '-') + }) + + return formattedText.join('-').toLowerCase() + } else { + const anchor = text.props.children + if (typeof anchor === 'string') { + return anchor + .toLowerCase() + .replace(/[^a-z0-9- ]/g, '') + .replace(/[ ]/g, '-') + } + return anchor + } + } else if (typeof text === 'string') { + if (text.includes('{#') && text.endsWith('}')) { + return text.slice(text.indexOf('{#') + 2, text.indexOf('}')) + } else { + return text + .toLowerCase() + .replace(/[^a-z0-9- ]/g, '') + .replace(/[ ]/g, '-') + } + } else { + return undefined + } +} + +export const removeAnchor = (text: any) => { + if (typeof text === 'object' && Array.isArray(text)) { + return text.filter((x) => !(typeof x === 'string' && x.includes('{#') && x.endsWith('}'))) + } else if (typeof text === 'string') { + if (text.indexOf('{#') > 0) return text.slice(0, text.indexOf('{#')) + else return text + } + return text +} + +export const highlightSelectedTocItem = (id: string) => { + const tocMenuItems = document.querySelectorAll('.toc-menu a') + + // find any currently active items and remove them + const currentActiveItem = document.querySelector('.toc-menu .toc__menu-item--active') + currentActiveItem?.classList.remove('toc__menu-item--active') + + // Add active class to the current item + tocMenuItems.forEach((item) => { + // @ts-ignore + if (item.href.split('#')[1] === id) { + item.classList.add('toc__menu-item--active') + } + }) +} + +// find any currently active items and remove them on route change +export const unHighlightSelectedTocItems = () => { + const currentActiveItem = document.querySelector('.toc-menu .toc__menu-item--active') + currentActiveItem?.classList.remove('toc__menu-item--active') +} diff --git a/apps/docs/components/CustomHTMLElements/Heading.tsx b/apps/docs/components/CustomHTMLElements/Heading.tsx new file mode 100644 index 00000000000..98703bbd944 --- /dev/null +++ b/apps/docs/components/CustomHTMLElements/Heading.tsx @@ -0,0 +1,37 @@ +import { + getAnchor, + removeAnchor, + highlightSelectedTocItem, + unHighlightSelectedTocItems, +} from './CustomHTMLElements.utils' +import { useInView } from 'react-intersection-observer' + +const Heading = ({ tag, children }) => { + const HeadingTag = `${tag}` as any + + const anchor = getAnchor(children) + const link = `#${anchor}` + + // check if current heading is in view, update TOC active item accordingly + // [Joshen] Ideally we highlight the section in the TOC when it's at the top of the page + // much like when we click on the item in the TOC itself, the rootMargin fix is insufficient + const { ref } = useInView({ + threshold: 1, + onChange: (inView, entry) => { + if (window.scrollY === 0) unHighlightSelectedTocItems() + if (inView) highlightSelectedTocItem(entry.target.id) + }, + }) + + return ( + + {removeAnchor(children)} + {anchor && ( +
+ # + + )} + + ) +} +export default Heading diff --git a/apps/docs/components/CustomHTMLElements/index.tsx b/apps/docs/components/CustomHTMLElements/index.tsx new file mode 100644 index 00000000000..56ded7aefd1 --- /dev/null +++ b/apps/docs/components/CustomHTMLElements/index.tsx @@ -0,0 +1,3 @@ +import Heading from './Heading' + +export { Heading } diff --git a/apps/temp-docs/components/DocSearch.tsx b/apps/docs/components/DocSearch.tsx similarity index 100% rename from apps/temp-docs/components/DocSearch.tsx rename to apps/docs/components/DocSearch.tsx diff --git a/apps/temp-docs/components/Footer/DarkModeToggle.tsx b/apps/docs/components/Footer/DarkModeToggle.tsx similarity index 100% rename from apps/temp-docs/components/Footer/DarkModeToggle.tsx rename to apps/docs/components/Footer/DarkModeToggle.tsx diff --git a/apps/temp-docs/components/Footer/index.tsx b/apps/docs/components/Footer/index.tsx similarity index 100% rename from apps/temp-docs/components/Footer/index.tsx rename to apps/docs/components/Footer/index.tsx diff --git a/apps/temp-docs/components/Frameworks.tsx b/apps/docs/components/Frameworks.tsx similarity index 86% rename from apps/temp-docs/components/Frameworks.tsx rename to apps/docs/components/Frameworks.tsx index ee504ee01e6..1716a2600ef 100644 --- a/apps/temp-docs/components/Frameworks.tsx +++ b/apps/docs/components/Frameworks.tsx @@ -1,6 +1,9 @@ import ButtonCard from './ButtonCard' +import { useTheme } from '~/components/Providers' const Frameworks = () => { + const { isDarkMode } = useTheme() + const frameworks = [ { name: 'Angular', @@ -14,15 +17,15 @@ const Frameworks = () => { name: 'Expo', logo: { light: '/docs/img/libraries/expo-icon.svg', - dark: '/docs/img/libraries/expo-icon.svg', + dark: '/docs/img/libraries/expo-icon-dark.svg', }, href: '/guides/with-expo', }, { name: 'Flutter', logo: { - light: '/docs/img/libraries/dart-icon.svg', - dark: '/docs/img/libraries/dart-icon.svg', + light: '/docs/img/libraries/flutter-icon.svg', + dark: '/docs/img/libraries/flutter-icon.svg', }, href: '/guides/with-flutter', }, @@ -84,7 +87,7 @@ const Frameworks = () => { }, ] return ( -
+
{frameworks.map((x) => (
{ to={x.href} title={x.name} // [Joshen] Nice to have: theming - icon={x.logo.dark} + icon={isDarkMode ? x.logo.dark : x.logo.light} />
))} diff --git a/apps/temp-docs/docs/guides/functions/examples.mdx b/apps/docs/components/FunctionsExamples.tsx similarity index 75% rename from apps/temp-docs/docs/guides/functions/examples.mdx rename to apps/docs/components/FunctionsExamples.tsx index ee64360a142..f4e6353383b 100644 --- a/apps/temp-docs/docs/guides/functions/examples.mdx +++ b/apps/docs/components/FunctionsExamples.tsx @@ -1,10 +1,5 @@ ---- -id: examples -title: Examples -description: Useful Supabase Edge Functions Examples. ---- +import ButtonCard from 'components/ButtonCard' -import ButtonCard from '@site/src/components/ButtonCard' const examples = [ { name: 'With supabase-js', @@ -50,20 +45,16 @@ const examples = [ }, ] -You can find a list of useful [Edge Function Examples](https://github.com/supabase/supabase/tree/master/examples/edge-functions) in our GitHub repository. +const FunctionsExamples = () => { + return ( +
+ {examples.map((x) => ( +
+ +
+ ))} +
+ ) +} -
-
- {examples.map((x) => ( -
- -
- ))} -
-
+export default FunctionsExamples diff --git a/apps/docs/components/GithubCard.tsx b/apps/docs/components/GithubCard.tsx new file mode 100644 index 00000000000..8780bbd0af6 --- /dev/null +++ b/apps/docs/components/GithubCard.tsx @@ -0,0 +1,17 @@ +import React from 'react' + +export default function GithubCard({ title, description, href, stars, handle }) { + return ( + +
+

{title}

+ {description} +
+
+
+
@{handle}
+
{stars} ★
+
+
+ ) +} diff --git a/apps/docs/components/JwtGenerator.js b/apps/docs/components/JwtGenerator.js new file mode 100644 index 00000000000..46dd4758e31 --- /dev/null +++ b/apps/docs/components/JwtGenerator.js @@ -0,0 +1,96 @@ +import React, { useState } from 'react' +import KJUR from 'jsrsasign' +import CodeBlock from './CodeBlock/CodeBlock' +import { Button } from 'ui' + +const JWT_HEADER = { alg: 'HS256', typ: 'JWT' } +const now = new Date() +const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()) +const fiveYears = new Date(now.getFullYear() + 5, now.getMonth(), now.getDate()) +const anonToken = ` +{ + "role": "anon", + "iss": "supabase", + "iat": ${Math.floor(today / 1000)}, + "exp": ${Math.floor(fiveYears / 1000)} +} +`.trim() + +const serviceToken = ` +{ + "role": "service_role", + "iss": "supabase", + "iat": ${Math.floor(today / 1000)}, + "exp": ${Math.floor(fiveYears / 1000)} +} +`.trim() + +export default function JwtGenerator({}) { + const secret = [...Array(40)].map(() => Math.random().toString(36)[2]).join('') + + const [jwtSecret, setJwtSecret] = useState(secret) + const [token, setToken] = useState(anonToken) + const [signedToken, setSignedToken] = useState('') + + const handleKeySelection = (e) => { + const val = e.target.value + if (val == 'service') setToken(serviceToken) + else setToken(anonToken) + } + const generate = () => { + const signedJWT = KJUR.jws.JWS.sign(null, JWT_HEADER, token, jwtSecret) + setSignedToken(signedJWT) + } + + return ( +
+
+ + setJwtSecret(e.target.value)} + /> +
+
+ + +
+ +
+ +