mirror of
https://github.com/supabase/supabase.git
synced 2026-10-19 08:09:11 +03:00
Merge branch 'master' of github.com:supabase/supabase
This commit is contained in:
commit
1dd3221d02
561 files changed
+18142
-19284
No files matched your search
@@ -19,10 +19,10 @@
|
||||
"common": "*",
|
||||
"config": "*",
|
||||
"dayjs": "^1.11.10",
|
||||
"eslint-config-next": "^14.1.0",
|
||||
"eslint-config-next": "^14.1.4",
|
||||
"lodash": "^4.17.21",
|
||||
"lucide-react": "^0.338.0",
|
||||
"next": "^14.0.3",
|
||||
"next": "^14.1.4",
|
||||
"openai": "^4.20.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
@@ -38,7 +38,7 @@
|
||||
"@types/react": "^18.2.24",
|
||||
"@types/react-dom": "^18.2.8",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"nextjs-node-loader": "^1.1.5-alpha.0",
|
||||
"nextjs-node-loader": "^1.1.5",
|
||||
"postcss": "^8.4.31",
|
||||
"typescript": "^5.4.3"
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
useState,
|
||||
} from 'react'
|
||||
|
||||
import { useIsLoggedIn } from 'common'
|
||||
import { type Database, useIsLoggedIn } from 'common'
|
||||
import { Button, cn } from 'ui'
|
||||
|
||||
import { useSendFeedbackMutation } from '~/lib/fetch/feedback'
|
||||
@@ -76,7 +76,7 @@ function Feedback() {
|
||||
const pathname = usePathname()
|
||||
const sendTelemetryEvent = useSendTelemetryEvent()
|
||||
const { mutate: sendFeedbackComment } = useSendFeedbackMutation()
|
||||
const supabase = useSupabaseClient()
|
||||
const supabase = useSupabaseClient<Database>()
|
||||
|
||||
const unanswered = state.type === 'unanswered'
|
||||
const isYes = 'response' in state && state.response === 'yes'
|
||||
@@ -85,7 +85,13 @@ function Feedback() {
|
||||
const showNo = unanswered || isNo
|
||||
|
||||
async function sendFeedbackVote(response: Response) {
|
||||
const { error } = await supabase.from('feedback').insert({ vote: response, page: pathname })
|
||||
const { error } = await supabase.from('feedback').insert({
|
||||
vote: response,
|
||||
page: pathname,
|
||||
metadata: {
|
||||
query: Object.fromEntries(new URLSearchParams(window.location.search).entries()),
|
||||
},
|
||||
})
|
||||
if (error) console.error(error)
|
||||
}
|
||||
|
||||
@@ -111,8 +117,14 @@ function Feedback() {
|
||||
}, 100)
|
||||
}
|
||||
|
||||
async function handleSubmit({ page, comment }: FeedbackFields) {
|
||||
sendFeedbackComment({ message: comment, pathname: page })
|
||||
async function handleSubmit({ page, comment, title }: FeedbackFields) {
|
||||
sendFeedbackComment({
|
||||
message: comment,
|
||||
pathname: page,
|
||||
title,
|
||||
// @ts-expect-error -- can't click this button without having a state.response
|
||||
isHelpful: state.response === 'yes',
|
||||
})
|
||||
setModalOpen(false)
|
||||
refocusButton()
|
||||
}
|
||||
@@ -178,14 +190,12 @@ function Feedback() {
|
||||
)}
|
||||
>
|
||||
<span className="text-foreground-light">Thanks for your feedback!</span>
|
||||
{/**
|
||||
<FeedbackButton
|
||||
ref={feedbackButtonRef}
|
||||
onClick={() => setModalOpen(true)}
|
||||
isYes={isYes}
|
||||
visible={!unanswered}
|
||||
/>
|
||||
*/}
|
||||
</div>
|
||||
</div>
|
||||
<FeedbackModal
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Button, Form, Input, Modal } from 'ui'
|
||||
|
||||
export type FeedbackFields = {
|
||||
page: string
|
||||
title: string
|
||||
comment: string
|
||||
}
|
||||
|
||||
@@ -21,6 +22,10 @@ function FeedbackModal({ visible, page, onCancel, onSubmit }: FeedbackModalProps
|
||||
validate={(vals) => {
|
||||
const errors: Partial<FeedbackFields> = {}
|
||||
|
||||
if (!vals.title) {
|
||||
errors.title = 'Required'
|
||||
}
|
||||
|
||||
if (!vals.comment) {
|
||||
errors.comment = 'Required'
|
||||
}
|
||||
@@ -32,8 +37,9 @@ function FeedbackModal({ visible, page, onCancel, onSubmit }: FeedbackModalProps
|
||||
>
|
||||
{({ isSubmitting }: { isSubmitting: boolean }) => (
|
||||
<>
|
||||
<Modal.Content className="pt-4 pb-2">
|
||||
<Modal.Content className="pt-4 pb-2 flex flex-col gap-2">
|
||||
<Input type="hidden" id="page" name="page" value={page} />
|
||||
<Input type="text" id="title" name="title" label="Title" className="mb-2" />
|
||||
<Input.TextArea
|
||||
label="Comment"
|
||||
id="comment"
|
||||
|
||||
@@ -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,91 @@ 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
|
||||
|
||||
/**
|
||||
* Because we're directly querying the DOM, needs the setTimeout so the DOM
|
||||
* update will happen first.
|
||||
*/
|
||||
const timeoutHandle = setTimeout(() => {
|
||||
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)
|
||||
})
|
||||
|
||||
return () => clearTimeout(timeoutHandle)
|
||||
/**
|
||||
* window.location.href needed to recalculate toc when page changes,
|
||||
* useRerenderOnEvt below will guarantee rerender on change
|
||||
*/
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [overrideToc, typeof window !== 'undefined' && window.location.href])
|
||||
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -3,4 +3,4 @@ The next step requires a callback URL, which looks like this: `https://<project-
|
||||
- Go to your [Supabase Project Dashboard](https://supabase.com/dashboard)
|
||||
- Click on the `Authentication` icon in the left sidebar
|
||||
- Click on [`Providers`](https://supabase.com/dashboard/project/_/auth/providers) under the Configuration section
|
||||
- Click on **{props.provider}** from the accordion list to expand and you'll find your **Redirect URL**, you can click `Copy` to copy it to the clipboard
|
||||
- Click on **{props.provider}** from the accordion list to expand and you'll find your **Callback URL**, you can click `Copy` to copy it to the clipboard
|
||||
@@ -26,58 +26,58 @@ import {
|
||||
IconMenuDevCli,
|
||||
} from './HomeMenuIcons'
|
||||
|
||||
function getMenuIcon(menuKey: string, width: number = 16, height: number = 16) {
|
||||
function getMenuIcon(menuKey: string, width: number = 16, height: number = 16, className) {
|
||||
switch (menuKey) {
|
||||
case 'home':
|
||||
return <IconMenuHome width={width} height={height} />
|
||||
return <IconMenuHome width={width} height={height} className={className} />
|
||||
case 'getting-started':
|
||||
return <IconMenuGettingStarted width={width} height={height} />
|
||||
return <IconMenuGettingStarted width={width} height={height} className={className} />
|
||||
case 'database':
|
||||
return <IconMenuDatabase width={width} height={height} />
|
||||
return <IconMenuDatabase width={width} height={height} className={className} />
|
||||
case 'rest':
|
||||
return <IconMenuRestApis width={width} height={height} />
|
||||
return <IconMenuRestApis width={width} height={height} className={className} />
|
||||
case 'graphql':
|
||||
return <IconMenuGraphQL width={width} height={height} />
|
||||
return <IconMenuGraphQL width={width} height={height} className={className} />
|
||||
case 'auth':
|
||||
return <IconMenuAuth width={width} height={height} />
|
||||
return <IconMenuAuth width={width} height={height} className={className} />
|
||||
case 'edge-functions':
|
||||
return <IconMenuEdgeFunctions width={width} height={height} />
|
||||
return <IconMenuEdgeFunctions width={width} height={height} className={className} />
|
||||
case 'realtime':
|
||||
return <IconMenuRealtime width={width} height={height} />
|
||||
return <IconMenuRealtime width={width} height={height} className={className} />
|
||||
case 'storage':
|
||||
return <IconMenuStorage width={width} height={height} />
|
||||
return <IconMenuStorage width={width} height={height} className={className} />
|
||||
case 'ai':
|
||||
return <IconMenuAI width={width} height={height} />
|
||||
return <IconMenuAI width={width} height={height} className={className} />
|
||||
case 'platform':
|
||||
return <IconMenuPlatform width={width} height={height} />
|
||||
return <IconMenuPlatform width={width} height={height} className={className} />
|
||||
case 'resources':
|
||||
return <IconMenuResources width={width} height={height} />
|
||||
return <IconMenuResources width={width} height={height} className={className} />
|
||||
case 'self-hosting':
|
||||
return <IconMenuSelfHosting width={width} height={height} />
|
||||
return <IconMenuSelfHosting width={width} height={height} className={className} />
|
||||
case 'integrations':
|
||||
return <IconMenuIntegrations width={width} height={height} />
|
||||
return <IconMenuIntegrations width={width} height={height} className={className} />
|
||||
case 'reference-javascript':
|
||||
return <IconMenuJavascript width={width} height={height} />
|
||||
return <IconMenuJavascript width={width} height={height} className={className} />
|
||||
case 'reference-dart':
|
||||
return <IconMenuFlutter width={width} height={height} />
|
||||
return <IconMenuFlutter width={width} height={height} className={className} />
|
||||
case 'reference-python':
|
||||
return <IconMenuPython width={width} height={height} />
|
||||
return <IconMenuPython width={width} height={height} className={className} />
|
||||
case 'reference-csharp':
|
||||
return <IconMenuCsharp width={width} height={height} />
|
||||
return <IconMenuCsharp width={width} height={height} className={className} />
|
||||
case 'reference-swift':
|
||||
return <IconMenuSwift width={width} height={height} />
|
||||
return <IconMenuSwift width={width} height={height} className={className} />
|
||||
case 'reference-kotlin':
|
||||
return <IconMenuKotlin width={width} height={height} />
|
||||
return <IconMenuKotlin width={width} height={height} className={className} />
|
||||
case 'reference-api':
|
||||
return <IconMenuApi width={width} height={height} />
|
||||
return <IconMenuApi width={width} height={height} className={className} />
|
||||
case 'dev-cli':
|
||||
return <IconMenuDevCli width={width} height={height} />
|
||||
return <IconMenuDevCli width={width} height={height} className={className} />
|
||||
case 'reference-cli':
|
||||
return <IconMenuCli width={width} height={height} />
|
||||
return <IconMenuCli width={width} height={height} className={className} />
|
||||
case 'status':
|
||||
return <IconMenuStatus width={width} height={height} />
|
||||
return <IconMenuStatus width={width} height={height} className={className} />
|
||||
default:
|
||||
return <IconMenuPlatform width={width} height={height} />
|
||||
return <IconMenuPlatform width={width} height={height} className={className} />
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,12 +85,14 @@ type HomeMenuIconPickerProps = {
|
||||
icon: string
|
||||
width?: number
|
||||
height?: number
|
||||
className?: string
|
||||
}
|
||||
|
||||
export default function HomeMenuIconPicker({
|
||||
icon,
|
||||
width = 16,
|
||||
height = 16,
|
||||
className,
|
||||
}: HomeMenuIconPickerProps) {
|
||||
return getMenuIcon(icon, width, height)
|
||||
return getMenuIcon(icon, width, height, className)
|
||||
}
|
||||
@@ -3,11 +3,13 @@ import { products } from 'shared-data'
|
||||
type HomeMenuIcon = {
|
||||
width?: number
|
||||
height?: number
|
||||
className?: string
|
||||
}
|
||||
export function IconMenuHome({ width = 16, height = 16 }: HomeMenuIcon) {
|
||||
export function IconMenuHome({ width = 16, height = 16, className }: HomeMenuIcon) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
width={width}
|
||||
height={height}
|
||||
viewBox="0 0 16 16"
|
||||
@@ -23,10 +25,11 @@ export function IconMenuHome({ width = 16, height = 16 }: HomeMenuIcon) {
|
||||
)
|
||||
}
|
||||
|
||||
export function IconMenuGraphQL({ width = 16, height = 16 }: HomeMenuIcon) {
|
||||
export function IconMenuGraphQL({ width = 16, height = 16, className }: HomeMenuIcon) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
width={width}
|
||||
height={height}
|
||||
viewBox="0 0 16 16"
|
||||
@@ -42,10 +45,11 @@ export function IconMenuGraphQL({ width = 16, height = 16 }: HomeMenuIcon) {
|
||||
)
|
||||
}
|
||||
|
||||
export function IconMenuApi({ width = 16, height = 16 }: HomeMenuIcon) {
|
||||
export function IconMenuApi({ width = 16, height = 16, className }: HomeMenuIcon) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
width={width}
|
||||
height={height}
|
||||
viewBox="0 0 16 16"
|
||||
@@ -77,9 +81,10 @@ export function IconMenuApi({ width = 16, height = 16 }: HomeMenuIcon) {
|
||||
)
|
||||
}
|
||||
|
||||
export function IconMenuAuth({ width = 16, height = 16 }: HomeMenuIcon) {
|
||||
export function IconMenuAuth({ width = 16, height = 16, className }: HomeMenuIcon) {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
viewBox="0 0 16 16"
|
||||
width={width}
|
||||
height={height}
|
||||
@@ -96,10 +101,11 @@ export function IconMenuAuth({ width = 16, height = 16 }: HomeMenuIcon) {
|
||||
)
|
||||
}
|
||||
|
||||
export function IconMenuDevCli({ width = 16, height = 16 }: HomeMenuIcon) {
|
||||
export function IconMenuDevCli({ width = 16, height = 16, className }: HomeMenuIcon) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
width={width}
|
||||
height={height}
|
||||
viewBox="0 0 16 16"
|
||||
@@ -115,10 +121,11 @@ export function IconMenuDevCli({ width = 16, height = 16 }: HomeMenuIcon) {
|
||||
)
|
||||
}
|
||||
|
||||
export function IconMenuCli({ width = 16, height = 16 }: HomeMenuIcon) {
|
||||
export function IconMenuCli({ width = 16, height = 16, className }: HomeMenuIcon) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
width={width}
|
||||
height={height}
|
||||
viewBox="0 0 16 16"
|
||||
@@ -132,9 +139,10 @@ export function IconMenuCli({ width = 16, height = 16 }: HomeMenuIcon) {
|
||||
)
|
||||
}
|
||||
|
||||
export function IconMenuCsharp({ width = 16, height = 16 }: HomeMenuIcon) {
|
||||
export function IconMenuCsharp({ width = 16, height = 16, className }: HomeMenuIcon) {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
viewBox="0 0 16 16"
|
||||
width={width}
|
||||
height={height}
|
||||
@@ -151,10 +159,11 @@ export function IconMenuCsharp({ width = 16, height = 16 }: HomeMenuIcon) {
|
||||
)
|
||||
}
|
||||
|
||||
export function IconMenuDatabase({ width = 16, height = 16 }: HomeMenuIcon) {
|
||||
export function IconMenuDatabase({ width = 16, height = 16, className }: HomeMenuIcon) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
width={width}
|
||||
height={width}
|
||||
viewBox="0 0 16 16"
|
||||
@@ -170,9 +179,10 @@ export function IconMenuDatabase({ width = 16, height = 16 }: HomeMenuIcon) {
|
||||
)
|
||||
}
|
||||
|
||||
export function IconMenuRestApis({ width = 16, height = 16 }: HomeMenuIcon) {
|
||||
export function IconMenuRestApis({ width = 16, height = 16, className }: HomeMenuIcon) {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
viewBox="0 0 16 16"
|
||||
width={width}
|
||||
height={height}
|
||||
@@ -189,10 +199,11 @@ export function IconMenuRestApis({ width = 16, height = 16 }: HomeMenuIcon) {
|
||||
)
|
||||
}
|
||||
|
||||
export function IconMenuEdgeFunctions({ width = 16, height = 16 }: HomeMenuIcon) {
|
||||
export function IconMenuEdgeFunctions({ width = 16, height = 16, className }: HomeMenuIcon) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
width={width}
|
||||
height={height}
|
||||
viewBox="0 0 16 16"
|
||||
@@ -208,9 +219,10 @@ export function IconMenuEdgeFunctions({ width = 16, height = 16 }: HomeMenuIcon)
|
||||
)
|
||||
}
|
||||
|
||||
export function IconMenuExtensions({ width = 16, height = 16 }: HomeMenuIcon) {
|
||||
export function IconMenuExtensions({ width = 16, height = 16, className }: HomeMenuIcon) {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
viewBox="0 0 16 16"
|
||||
width={width}
|
||||
height={height}
|
||||
@@ -227,9 +239,10 @@ export function IconMenuExtensions({ width = 16, height = 16 }: HomeMenuIcon) {
|
||||
)
|
||||
}
|
||||
|
||||
export function IconMenuFlutter({ width = 16, height = 16 }: HomeMenuIcon) {
|
||||
export function IconMenuFlutter({ width = 16, height = 16, className }: HomeMenuIcon) {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
viewBox="0 0 16 16"
|
||||
width={width}
|
||||
height={height}
|
||||
@@ -246,10 +259,11 @@ export function IconMenuFlutter({ width = 16, height = 16 }: HomeMenuIcon) {
|
||||
)
|
||||
}
|
||||
|
||||
export function IconMenuGettingStarted({ width = 16, height = 16 }: HomeMenuIcon) {
|
||||
export function IconMenuGettingStarted({ width = 16, height = 16, className }: HomeMenuIcon) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
width={width}
|
||||
height={height}
|
||||
viewBox="0 0 16 16"
|
||||
@@ -265,10 +279,11 @@ export function IconMenuGettingStarted({ width = 16, height = 16 }: HomeMenuIcon
|
||||
)
|
||||
}
|
||||
|
||||
export function IconMenuIntegrations({ width = 16, height = 16 }: HomeMenuIcon) {
|
||||
export function IconMenuIntegrations({ width = 16, height = 16, className }: HomeMenuIcon) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
width={width}
|
||||
height={height}
|
||||
viewBox="0 0 16 16"
|
||||
@@ -284,9 +299,10 @@ export function IconMenuIntegrations({ width = 16, height = 16 }: HomeMenuIcon)
|
||||
)
|
||||
}
|
||||
|
||||
export function IconMenuJavascript({ width = 16, height = 16 }: HomeMenuIcon) {
|
||||
export function IconMenuJavascript({ width = 16, height = 16, className }: HomeMenuIcon) {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
width={width}
|
||||
height={height}
|
||||
fill="none"
|
||||
@@ -303,9 +319,10 @@ export function IconMenuJavascript({ width = 16, height = 16 }: HomeMenuIcon) {
|
||||
)
|
||||
}
|
||||
|
||||
export function IconMenuPlatform({ width = 16, height = 16 }: HomeMenuIcon) {
|
||||
export function IconMenuPlatform({ width = 16, height = 16, className }: HomeMenuIcon) {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
viewBox="0 0 16 16"
|
||||
width={width}
|
||||
height={height}
|
||||
@@ -322,9 +339,10 @@ export function IconMenuPlatform({ width = 16, height = 16 }: HomeMenuIcon) {
|
||||
)
|
||||
}
|
||||
|
||||
export function IconMenuPython({ width = 16, height = 16 }: HomeMenuIcon) {
|
||||
export function IconMenuPython({ width = 16, height = 16, className }: HomeMenuIcon) {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
viewBox="0 0 16 16"
|
||||
width={width}
|
||||
height={height}
|
||||
@@ -341,10 +359,11 @@ export function IconMenuPython({ width = 16, height = 16 }: HomeMenuIcon) {
|
||||
)
|
||||
}
|
||||
|
||||
export function IconMenuRealtime({ width = 16, height = 16 }: HomeMenuIcon) {
|
||||
export function IconMenuRealtime({ width = 16, height = 16, className }: HomeMenuIcon) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
width={width}
|
||||
height={height}
|
||||
viewBox="0 0 16 16"
|
||||
@@ -360,10 +379,11 @@ export function IconMenuRealtime({ width = 16, height = 16 }: HomeMenuIcon) {
|
||||
)
|
||||
}
|
||||
|
||||
export function IconMenuResources({ width = 16, height = 16 }: HomeMenuIcon) {
|
||||
export function IconMenuResources({ width = 16, height = 16, className }: HomeMenuIcon) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
width={width}
|
||||
height={height}
|
||||
viewBox="0 0 16 16"
|
||||
@@ -379,9 +399,10 @@ export function IconMenuResources({ width = 16, height = 16 }: HomeMenuIcon) {
|
||||
)
|
||||
}
|
||||
|
||||
export function IconMenuSelfHosting({ width = 16, height = 16 }: HomeMenuIcon) {
|
||||
export function IconMenuSelfHosting({ width = 16, height = 16, className }: HomeMenuIcon) {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
viewBox="0 0 16 16"
|
||||
width={width}
|
||||
height={height}
|
||||
@@ -398,9 +419,10 @@ export function IconMenuSelfHosting({ width = 16, height = 16 }: HomeMenuIcon) {
|
||||
)
|
||||
}
|
||||
|
||||
export function IconMenuStorage({ width = 16, height = 16 }: HomeMenuIcon) {
|
||||
export function IconMenuStorage({ width = 16, height = 16, className }: HomeMenuIcon) {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
viewBox="0 0 16 16"
|
||||
width={width}
|
||||
height={height}
|
||||
@@ -417,9 +439,10 @@ export function IconMenuStorage({ width = 16, height = 16 }: HomeMenuIcon) {
|
||||
)
|
||||
}
|
||||
|
||||
export function IconMenuAI({ width = 16, height = 16 }: HomeMenuIcon) {
|
||||
export function IconMenuAI({ width = 16, height = 16, className }: HomeMenuIcon) {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
width={width}
|
||||
height={height}
|
||||
viewBox="0 0 16 16"
|
||||
@@ -437,9 +460,10 @@ export function IconMenuAI({ width = 16, height = 16 }: HomeMenuIcon) {
|
||||
)
|
||||
}
|
||||
|
||||
export function IconMenuSwift({ width = 16, height = 16 }: HomeMenuIcon) {
|
||||
export function IconMenuSwift({ width = 16, height = 16, className }: HomeMenuIcon) {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
viewBox="0 0 16 16"
|
||||
width={width}
|
||||
height={height}
|
||||
@@ -456,10 +480,11 @@ export function IconMenuSwift({ width = 16, height = 16 }: HomeMenuIcon) {
|
||||
)
|
||||
}
|
||||
|
||||
export function IconMenuKotlin({ width = 16, height = 16 }: HomeMenuIcon) {
|
||||
export function IconMenuKotlin({ width = 16, height = 16, className }: HomeMenuIcon) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
viewBox="0 0 16 16"
|
||||
width={width}
|
||||
height={height}
|
||||
@@ -475,9 +500,10 @@ export function IconMenuKotlin({ width = 16, height = 16 }: HomeMenuIcon) {
|
||||
)
|
||||
}
|
||||
|
||||
export function IconMenuStatus({ width = 16, height = 16 }: HomeMenuIcon) {
|
||||
export function IconMenuStatus({ width = 16, height = 16, className }: HomeMenuIcon) {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
width={width}
|
||||
height={height}
|
||||
viewBox="0 0 18 18"
|
||||
|
||||
@@ -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',
|
||||
@@ -917,10 +924,18 @@ export const database: NavMenuConstant = {
|
||||
name: 'Overview',
|
||||
url: '/guides/database/extensions/wrappers/overview',
|
||||
},
|
||||
{
|
||||
name: 'Connecting to Auth0',
|
||||
url: '/guides/database/extensions/wrappers/auth0',
|
||||
},
|
||||
{
|
||||
name: 'Connecting to Airtable',
|
||||
url: '/guides/database/extensions/wrappers/airtable',
|
||||
},
|
||||
{
|
||||
name: 'Connecting to AWS Cognito',
|
||||
url: '/guides/database/extensions/wrappers/cognito',
|
||||
},
|
||||
{
|
||||
name: 'Connecting to AWS S3',
|
||||
url: '/guides/database/extensions/wrappers/s3',
|
||||
@@ -941,6 +956,14 @@ export const database: NavMenuConstant = {
|
||||
name: 'Connecting to Logflare',
|
||||
url: '/guides/database/extensions/wrappers/logflare',
|
||||
},
|
||||
{
|
||||
name: 'Connecting to MSSQL',
|
||||
url: '/guides/database/extensions/wrappers/mssql',
|
||||
},
|
||||
{
|
||||
name: 'Connecting to Redis',
|
||||
url: '/guides/database/extensions/wrappers/redis',
|
||||
},
|
||||
{
|
||||
name: 'Connecting to Stripe',
|
||||
url: '/guides/database/extensions/wrappers/stripe',
|
||||
|
||||
@@ -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
|
||||
@@ -22,7 +22,7 @@ This is different from the `anon` API key which does not create a user and can b
|
||||
|
||||
</Admonition>
|
||||
|
||||
To get started, [enable anonymous sign-ins](/dashboard/project/_/auth/providers) for your Supabase Project.
|
||||
To get started, [enable anonymous sign-ins](/dashboard/project/_/settings/auth) for your Supabase Project.
|
||||
|
||||
<Admonition type="caution">
|
||||
|
||||
@@ -55,6 +55,15 @@ Call the [`signInAnonymously()`](/docs/reference/javascript/auth-signinanonymous
|
||||
const { data, error } = await supabase.auth.signInAnonymously()
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="kotlin" label="Kotlin">
|
||||
|
||||
Call the [`signInAnonymously()`](/docs/reference/kotlin/auth-signinanonymously) method:
|
||||
|
||||
```kotlin
|
||||
supabase.auth.signInAnonymously()
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
@@ -79,6 +88,17 @@ You can use the [`updateUser()`](/docs/reference/javascript/auth-updateuser) met
|
||||
const { data, error } = await supabase.auth.updateUser({ email: 'example@email.com' })
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="kotlin" label="Kotlin">
|
||||
|
||||
You can use the [`modifyUser()`](/docs/reference/kotlin/auth-updateuser) method to link an email or phone identity to the anonymous user.
|
||||
|
||||
```kotlin
|
||||
supabase.auth.modifyUser {
|
||||
email = "example@email.com"
|
||||
}
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
@@ -99,6 +119,15 @@ You can use the [`linkIdentity()`](/docs/reference/javascript/auth-linkidentity)
|
||||
const { data, error } = await supabase.auth.linkIdentity({ provider: 'google' })
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="kotlin" label="Kotlin">
|
||||
|
||||
You can use the [`linkIdentity()`](/docs/reference/kotlin/auth-linkidentity) method to link an oauth identity to the anonymous user.
|
||||
|
||||
```kotlin
|
||||
supabase.auth.linkIdentity(Google)
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
@@ -108,7 +137,7 @@ An anonymous user assumes the `authenticated` role just like a permanent user. Y
|
||||
|
||||
```sql
|
||||
create policy "Only permanent users can post to the news feed"
|
||||
on news_feed for insert
|
||||
on news_feed as restrictive for insert
|
||||
to authenticated
|
||||
with check ((auth.jwt()->>'is_anonymous')::boolean is false );
|
||||
|
||||
@@ -118,6 +147,12 @@ to authenticated
|
||||
using ( true );
|
||||
```
|
||||
|
||||
<Admonition variant="note" label="Use restrictive policies">
|
||||
|
||||
RLS policies are permissive by default, which means that they are combined using an "OR" operator when multiple policies are applied. It is important to construct restrictive policies to ensure that the checks for an anonymous user are always enforced when combined with other policies.
|
||||
|
||||
</Admonition>
|
||||
|
||||
## Resolving identity conflicts
|
||||
|
||||
Depending on your application requirements, data conflicts can arise when an anonymous user is converted to a permanent user. For example, in the context of an e-commerce application, an anonymous user would be allowed to add items to the shopping cart without signing up / signing in. When they decide to sign-in to an existing account, you will need to decide how you want to resolve data conflicts in the shopping cart:
|
||||
|
||||
@@ -11,6 +11,20 @@ The `auth-helpers` package has been replaced with the `@supabase/ssr` package. W
|
||||
|
||||
</Admonition>
|
||||
|
||||
<Accordion
|
||||
type="default"
|
||||
openBehaviour="multiple"
|
||||
chevronAlign="right"
|
||||
justified
|
||||
size="medium"
|
||||
className="text-foreground-light border-b mt-8 pb-2"
|
||||
>
|
||||
|
||||
<Accordion.Item
|
||||
header="See legacy docs"
|
||||
id="legacy-docs"
|
||||
>
|
||||
|
||||
This submodule provides convenience helpers for implementing user authentication in Next.js applications using the pages directory.
|
||||
|
||||
> Note: As of [Next.js 13.4](https://nextjs.org/blog/next-13-4), the App Router has reached stable status. This is now the recommended path for new Next.js app. Check out our guide on using [Auth Helpers with the Next.js App Directory](/docs/guides/auth/auth-helpers/nextjs).
|
||||
@@ -914,3 +928,7 @@ import { Database } from '../database.types'
|
||||
|
||||
const supabaseClient = useSupabaseClient<Database>()
|
||||
```
|
||||
|
||||
</Accordion.Item>
|
||||
|
||||
</Accordion>
|
||||
@@ -11,6 +11,20 @@ The `auth-helpers` package has been replaced with the `@supabase/ssr` package. W
|
||||
|
||||
</Admonition>
|
||||
|
||||
<Accordion
|
||||
type="default"
|
||||
openBehaviour="multiple"
|
||||
chevronAlign="right"
|
||||
justified
|
||||
size="medium"
|
||||
className="text-foreground-light border-b mt-8 pb-2"
|
||||
>
|
||||
|
||||
<Accordion.Item
|
||||
header="See legacy docs"
|
||||
id="legacy-docs"
|
||||
>
|
||||
|
||||
The [Next.js Auth Helpers package](https://github.com/supabase/auth-helpers) configures Supabase Auth to store the user's `session` in a `cookie`, rather than `localStorage`. This makes it available across the client and server of the App Router - [Client Components](/docs/guides/auth/auth-helpers/nextjs#client-components), [Server Components](/docs/guides/auth/auth-helpers/nextjs#server-components), [Server Actions](/docs/guides/auth/auth-helpers/nextjs#server-actions), [Route Handlers](/docs/guides/auth/auth-helpers/nextjs#route-handlers) and [Middleware](/docs/guides/auth/auth-helpers/nextjs#middleware). The `session` is automatically sent along with any requests to Supabase.
|
||||
|
||||
<div className="video-container">
|
||||
@@ -1312,3 +1326,7 @@ export default function() {
|
||||
```
|
||||
|
||||
For an example of creating multiple Supabase clients, check [Singleton section](/docs/guides/auth/auth-helpers/nextjs#singleton) above.
|
||||
|
||||
</Accordion.Item>
|
||||
|
||||
</Accordion>
|
||||
@@ -11,6 +11,20 @@ We generally recommend using the new `@supabase/ssr` package instead of `auth-he
|
||||
|
||||
</Admonition>
|
||||
|
||||
<Accordion
|
||||
type="default"
|
||||
openBehaviour="multiple"
|
||||
chevronAlign="right"
|
||||
justified
|
||||
size="medium"
|
||||
className="text-foreground-light border-b mt-8 pb-2"
|
||||
>
|
||||
|
||||
<Accordion.Item
|
||||
header="See legacy docs"
|
||||
id="legacy-docs"
|
||||
>
|
||||
|
||||
This submodule provides convenience helpers for implementing user authentication in Remix applications.
|
||||
|
||||
<div className="video-container">
|
||||
@@ -782,3 +796,7 @@ supabaseClient.auth.signUp({
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
</Accordion.Item>
|
||||
|
||||
</Accordion>
|
||||
@@ -11,6 +11,20 @@ We generally recommend using the new `@supabase/ssr` package instead of `auth-he
|
||||
|
||||
</Admonition>
|
||||
|
||||
<Accordion
|
||||
type="default"
|
||||
openBehaviour="multiple"
|
||||
chevronAlign="right"
|
||||
justified
|
||||
size="medium"
|
||||
className="text-foreground-light border-b mt-8 pb-2"
|
||||
>
|
||||
|
||||
<Accordion.Item
|
||||
header="See legacy docs"
|
||||
id="legacy-docs"
|
||||
>
|
||||
|
||||
This submodule provides convenience helpers for implementing user authentication in [SvelteKit](https://kit.svelte.dev/) applications.
|
||||
|
||||
## Configuration
|
||||
@@ -46,8 +60,6 @@ PUBLIC_SUPABASE_ANON_KEY=your-anon-key
|
||||
|
||||
Create a new `hooks.server.js` file in the root of your project and populate with the following to retrieve the user session.
|
||||
|
||||
The session information from `getSession` is supplied to the Supabase client so it can retrieve the auth token. This is safe, since the auth token signature will be revalidated on the Auth server. But you shouldn't trust the unsigned data that is stored alongside the auth token:
|
||||
|
||||
<GetSessionWarning />
|
||||
|
||||
```js src/hooks.server.js
|
||||
@@ -63,15 +75,23 @@ export const handle = async ({ event, resolve }) => {
|
||||
})
|
||||
|
||||
/**
|
||||
* a little helper that is written for convenience so that instead
|
||||
* of calling `const { data: { session } } = await supabase.auth.getSession()`
|
||||
* you just call this `await getSession()`
|
||||
* Unlike `supabase.auth.getSession`, which is unsafe on the server because it
|
||||
* doesn't validate the JWT, this function validates the JWT by first calling
|
||||
* `getUser` and aborts early if the JWT signature is invalid.
|
||||
*/
|
||||
event.locals.getSession = async () => {
|
||||
event.locals.safeGetSession = async () => {
|
||||
const {
|
||||
data: { user },
|
||||
error,
|
||||
} = await supabase.auth.getUser()
|
||||
if (error) {
|
||||
return { session: null, user: null }
|
||||
}
|
||||
|
||||
const {
|
||||
data: { session },
|
||||
} = await event.locals.supabase.auth.getSession()
|
||||
return session
|
||||
return { session, user }
|
||||
}
|
||||
|
||||
return resolve(event, {
|
||||
@@ -102,15 +122,23 @@ export const handle: Handle = async ({ event, resolve }) => {
|
||||
})
|
||||
|
||||
/**
|
||||
* a little helper that is written for convenience so that instead
|
||||
* of calling `const { data: { session } } = await supabase.auth.getSession()`
|
||||
* you just call this `await getSession()`
|
||||
* Unlike `supabase.auth.getSession`, which is unsafe on the server because it
|
||||
* doesn't validate the JWT, this function validates the JWT by first calling
|
||||
* `getUser` and aborts early if the JWT signature is invalid.
|
||||
*/
|
||||
event.locals.getSession = async () => {
|
||||
event.locals.safeGetSession = async () => {
|
||||
const {
|
||||
data: { user },
|
||||
error,
|
||||
} = await supabase.auth.getUser()
|
||||
if (error) {
|
||||
return { session: null, user: null }
|
||||
}
|
||||
|
||||
const {
|
||||
data: { session },
|
||||
} = await event.locals.supabase.auth.getSession()
|
||||
return session
|
||||
return { session, user }
|
||||
}
|
||||
|
||||
return resolve(event, {
|
||||
@@ -189,17 +217,18 @@ In order to get the most out of TypeScript and it's intellisense, you should imp
|
||||
```ts src/app.d.ts
|
||||
// src/app.d.ts
|
||||
|
||||
import { SupabaseClient, Session } from '@supabase/supabase-js'
|
||||
import { SupabaseClient, Session, User } from '@supabase/supabase-js'
|
||||
import { Database } from './DatabaseDefinitions'
|
||||
|
||||
declare global {
|
||||
namespace App {
|
||||
interface Locals {
|
||||
supabase: SupabaseClient<Database>
|
||||
getSession(): Promise<Session | null>
|
||||
safeGetSession(): Promise<{ session: Session | null; user: User | null }>
|
||||
}
|
||||
interface PageData {
|
||||
session: Session | null
|
||||
user: User | null
|
||||
}
|
||||
// interface Error {}
|
||||
// interface Platform {}
|
||||
@@ -234,9 +263,12 @@ To make the session available across the UI, including pages and layouts, it is
|
||||
|
||||
```js src/routes/+layout.server.js
|
||||
// src/routes/+layout.server.js
|
||||
export const load = async ({ locals: { getSession } }) => {
|
||||
export const load = async ({ locals: { safeGetSession } }) => {
|
||||
const { session, user } = await safeGetSession()
|
||||
|
||||
return {
|
||||
session: await getSession(),
|
||||
session,
|
||||
user,
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -246,9 +278,12 @@ export const load = async ({ locals: { getSession } }) => {
|
||||
|
||||
```ts src/routes/+layout.server.ts
|
||||
// src/routes/+layout.server.ts
|
||||
export const load = async ({ locals: { getSession } }) => {
|
||||
export const load = async ({ locals: { safeGetSession } }) => {
|
||||
const { session, user } = await safeGetSession()
|
||||
|
||||
return {
|
||||
session: await getSession(),
|
||||
session,
|
||||
user,
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -284,6 +319,11 @@ export const load = async ({ fetch, data, depends }) => {
|
||||
serverSession: data.session,
|
||||
})
|
||||
|
||||
/**
|
||||
* It's fine to use `getSession` here, because on the client, `getSession` is
|
||||
* safe, and on the server, it reads `session` from the `LayoutData`, which
|
||||
* safely checked the session using `safeGetSession`.
|
||||
*/
|
||||
const {
|
||||
data: { session },
|
||||
} = await supabase.auth.getSession()
|
||||
@@ -312,6 +352,11 @@ export const load = async ({ fetch, data, depends }) => {
|
||||
serverSession: data.session,
|
||||
})
|
||||
|
||||
/**
|
||||
* It's fine to use `getSession` here, because on the client, `getSession` is
|
||||
* safe, and on the server, it reads `session` from the `LayoutData`, which
|
||||
* safely checked the session using `safeGetSession`.
|
||||
*/
|
||||
const {
|
||||
data: { session },
|
||||
} = await supabase.auth.getSession()
|
||||
@@ -532,8 +577,8 @@ Wrap an API Route to check that the user has a valid session. If they're not log
|
||||
// src/routes/api/protected-route/+server.ts
|
||||
import { json, error } from '@sveltejs/kit'
|
||||
|
||||
export const GET = async ({ locals: { supabase, getSession } }) => {
|
||||
const session = await getSession()
|
||||
export const GET = async ({ locals: { supabase, safeGetSession } }) => {
|
||||
const { session } = await safeGetSession()
|
||||
if (!session) {
|
||||
// the user is not signed in
|
||||
throw error(401, { message: 'Unauthorized' })
|
||||
@@ -555,8 +600,8 @@ Wrap an Action to check that the user has a valid session. If they're not logged
|
||||
import { error, fail } from '@sveltejs/kit'
|
||||
|
||||
export const actions = {
|
||||
createPost: async ({ request, locals: { supabase, getSession } }) => {
|
||||
const session = await getSession()
|
||||
createPost: async ({ request, locals: { supabase, safeGetSession } }) => {
|
||||
const { session } = await safeGetSession()
|
||||
|
||||
if (!session) {
|
||||
// the user is not signed in
|
||||
@@ -615,15 +660,21 @@ async function supabase({ event, resolve }) {
|
||||
})
|
||||
|
||||
/**
|
||||
* a little helper that is written for convenience so that instead
|
||||
* of calling `const { data: { session } } = await supabase.auth.getSession()`
|
||||
* you just call this `await getSession()`
|
||||
* Unlike `supabase.auth.getSession`, which is unsafe on the server because it
|
||||
* doesn't validate the JWT, this function validates the JWT by first calling
|
||||
* `getUser` and aborts early if the JWT signature is invalid.
|
||||
*/
|
||||
event.locals.getSession = async () => {
|
||||
event.locals.safeGetSession = async () => {
|
||||
const {
|
||||
data: { user },
|
||||
error,
|
||||
} = await event.locals.supabase.auth.getUser()
|
||||
if (error) return { session: null, user: null }
|
||||
|
||||
const {
|
||||
data: { session },
|
||||
} = await event.locals.supabase.auth.getSession()
|
||||
return session
|
||||
return { session, user }
|
||||
}
|
||||
|
||||
return resolve(event, {
|
||||
@@ -636,7 +687,7 @@ async function supabase({ event, resolve }) {
|
||||
async function authorization({ event, resolve }) {
|
||||
// protect requests to all routes that start with /protected-routes
|
||||
if (event.url.pathname.startsWith('/protected-routes') && event.request.method === 'GET') {
|
||||
const session = await event.locals.getSession()
|
||||
const { session } = await event.locals.safeGetSession()
|
||||
if (!session) {
|
||||
// the user is not signed in
|
||||
redirect(303, '/')
|
||||
@@ -645,7 +696,7 @@ async function authorization({ event, resolve }) {
|
||||
|
||||
// protect POST requests to all routes that start with /protected-posts
|
||||
if (event.url.pathname.startsWith('/protected-posts') && event.request.method === 'POST') {
|
||||
const session = await event.locals.getSession()
|
||||
const { session } = await event.locals.safeGetSession()
|
||||
if (!session) {
|
||||
// the user is not signed in
|
||||
throw error(303, '/')
|
||||
@@ -677,15 +728,21 @@ async function supabase({ event, resolve }) {
|
||||
})
|
||||
|
||||
/**
|
||||
* a little helper that is written for convenience so that instead
|
||||
* of calling `const { data: { session } } = await supabase.auth.getSession()`
|
||||
* you just call this `await getSession()`
|
||||
* Unlike `supabase.auth.getSession`, which is unsafe on the server because it
|
||||
* doesn't validate the JWT, this function validates the JWT by first calling
|
||||
* `getUser` and aborts early if the JWT signature is invalid.
|
||||
*/
|
||||
event.locals.getSession = async () => {
|
||||
event.locals.safeGetSession = async () => {
|
||||
const {
|
||||
data: { user },
|
||||
error,
|
||||
} = await event.locals.supabase.auth.getUser()
|
||||
if (error) return { session: null, user: null }
|
||||
|
||||
const {
|
||||
data: { session },
|
||||
} = await event.locals.supabase.auth.getSession()
|
||||
return session
|
||||
return { session, user }
|
||||
}
|
||||
|
||||
return resolve(event, {
|
||||
@@ -698,7 +755,7 @@ async function supabase({ event, resolve }) {
|
||||
async function authorization({ event, resolve }) {
|
||||
// protect requests to all routes that start with /protected-routes
|
||||
if (event.url.pathname.startsWith('/protected-routes') && event.request.method === 'GET') {
|
||||
const session = await event.locals.getSession()
|
||||
const { session } = await event.locals.safeGetSession()
|
||||
if (!session) {
|
||||
// the user is not signed in
|
||||
redirect(303, '/')
|
||||
@@ -707,7 +764,7 @@ async function authorization({ event, resolve }) {
|
||||
|
||||
// protect POST requests to all routes that start with /protected-posts
|
||||
if (event.url.pathname.startsWith('/protected-posts') && event.request.method === 'POST') {
|
||||
const session = await event.locals.getSession()
|
||||
const { session } = await event.locals.safeGetSession()
|
||||
if (!session) {
|
||||
// the user is not signed in
|
||||
throw error(303, '/')
|
||||
@@ -896,15 +953,21 @@ export const handle: Handle = async ({ event, resolve }) => {
|
||||
})
|
||||
|
||||
/**
|
||||
* a little helper that is written for convenience so that instead
|
||||
* of calling `const { data: { session } } = await supabase.auth.getSession()`
|
||||
* you just call this `await getSession()`
|
||||
* Unlike `supabase.auth.getSession`, which is unsafe on the server because it
|
||||
* doesn't validate the JWT, this function validates the JWT by first calling
|
||||
* `getUser` and aborts early if the JWT signature is invalid.
|
||||
*/
|
||||
event.locals.getSession = async () => {
|
||||
event.locals.safeGetSession = async () => {
|
||||
const {
|
||||
data: { user },
|
||||
error,
|
||||
} = await event.locals.supabase.auth.getUser()
|
||||
if (error) return { session: null, user: null }
|
||||
|
||||
const {
|
||||
data: { session },
|
||||
} = await event.locals.supabase.auth.getSession()
|
||||
return session
|
||||
return { session, user }
|
||||
}
|
||||
|
||||
return resolve(event, {
|
||||
@@ -1055,17 +1118,18 @@ declare namespace App {
|
||||
|
||||
```ts src/app.d.ts
|
||||
// src/app.d.ts
|
||||
import { SupabaseClient, Session } from '@supabase/supabase-js'
|
||||
import { SupabaseClient, Session, User } from '@supabase/supabase-js'
|
||||
import { Database } from './DatabaseDefinitions'
|
||||
|
||||
declare global {
|
||||
namespace App {
|
||||
interface Locals {
|
||||
supabase: SupabaseClient<Database>
|
||||
getSession(): Promise<Session | null>
|
||||
safeGetSession(): Promise<{ session: Session | null; user: User | null }>
|
||||
}
|
||||
interface PageData {
|
||||
session: Session | null
|
||||
user: User | null
|
||||
}
|
||||
// interface Error {}
|
||||
// interface Platform {}
|
||||
@@ -1196,7 +1260,7 @@ import type { RequestHandler } from './$types'
|
||||
import { json, error } from '@sveltejs/kit'
|
||||
|
||||
export const GET: RequestHandler = async ({ locals: { supabase, getSession } }) => {
|
||||
const session = await getSession()
|
||||
const { session } = await getSession()
|
||||
if (!session) {
|
||||
// the user is not signed in
|
||||
throw error(401, { message: 'Unauthorized' })
|
||||
@@ -1966,3 +2030,7 @@ export const GET: RequestHandler = withAuth(async ({ session, getSupabaseClient
|
||||
|
||||
- [Auth Helpers Source code](https://github.com/supabase/auth-helpers)
|
||||
- [SvelteKit example](https://github.com/supabase/auth-helpers/tree/main/examples/sveltekit)
|
||||
|
||||
</Accordion.Item>
|
||||
|
||||
</Accordion>
|
||||
@@ -429,12 +429,6 @@ export default function Index () {
|
||||
|
||||
Creating a Supabase client with the `ssr` package automatically configures it to use Cookies. This means your user's session is available throughout the entire SvelteKit stack - page, layout, server, hooks.
|
||||
|
||||
<Admonition type="danger">
|
||||
|
||||
Beware when accessing the session object on the server, because it is not revalidated on every request from the client. That means the sender can tamper with unencoded data in the session object. If you need to verify the integrity of user data for server logic, call `auth.getUser` instead, which will query the Supabase Auth server for trusted user data.
|
||||
|
||||
</Admonition>
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
@@ -475,15 +469,23 @@ export const handle: Handle = async ({ event, resolve }) => {
|
||||
})
|
||||
|
||||
/**
|
||||
* a little helper that is written for convenience so that instead
|
||||
* of calling `const { data: { session } } = await supabase.auth.getSession()`
|
||||
* you just call this `await getSession()`
|
||||
* Unlike `supabase.auth.getSession`, which is unsafe on the server because it
|
||||
* doesn't validate the JWT, this function validates the JWT by first calling
|
||||
* `getUser` and aborts early if the JWT signature is invalid.
|
||||
*/
|
||||
event.locals.getSession = async () => {
|
||||
event.locals.safeGetSession = async () => {
|
||||
const {
|
||||
data: { user },
|
||||
error,
|
||||
} = await event.locals.supabase.auth.getUser()
|
||||
if (error) {
|
||||
return { session: null, user: null }
|
||||
}
|
||||
|
||||
const {
|
||||
data: { session },
|
||||
} = await event.locals.supabase.auth.getSession()
|
||||
return session
|
||||
return { session, user }
|
||||
}
|
||||
|
||||
return resolve(event, {
|
||||
@@ -528,6 +530,11 @@ export const load: LayoutLoad = async ({ fetch, data, depends }) => {
|
||||
},
|
||||
})
|
||||
|
||||
/**
|
||||
* It's fine to use `getSession` here, because on the client, `getSession` is
|
||||
* safe, and on the server, it reads `session` from the `LayoutData`, which
|
||||
* safely checked the session using `safeGetSession`.
|
||||
*/
|
||||
const {
|
||||
data: { session },
|
||||
} = await supabase.auth.getSession()
|
||||
@@ -543,9 +550,12 @@ export const load: LayoutLoad = async ({ fetch, data, depends }) => {
|
||||
```ts +layout.server.ts
|
||||
import type { LayoutServerLoad } from './$types'
|
||||
|
||||
export const load: LayoutServerLoad = async ({ locals: { getSession } }) => {
|
||||
export const load: LayoutServerLoad = async ({ locals: { safeGetSession } }) => {
|
||||
const { session, user } = await safeGetSession()
|
||||
|
||||
return {
|
||||
session: await getSession(),
|
||||
session,
|
||||
user,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -812,11 +812,11 @@ Since this is an API route, use the Supabase client from `@/utils/supabase/api.t
|
||||
|
||||
<CH.Code>
|
||||
|
||||
```ts pages/api/auth/confirm.tsx
|
||||
```ts pages/api/auth/confirm.ts
|
||||
import { type EmailOtpType } from '@supabase/supabase-js'
|
||||
import type { NextApiRequest, NextApiResponse } from 'next'
|
||||
|
||||
import { createClient } from '@/utils/supabase/api'
|
||||
import createClient from '@/utils/supabase/api'
|
||||
|
||||
function stringOrFirstString(item: string | string[] | undefined) {
|
||||
return Array.isArray(item) ? item[0] : item
|
||||
@@ -869,7 +869,7 @@ export default function ErrorPage() {
|
||||
|
||||
If you use dynamic server-side rendering, you can serve a page to authenticated users only by checking for the user data in `getServerSideProps`. Unauthenticated users will be redirected to the home page.
|
||||
|
||||
Since you're calling Supabse from `getServerSideProps`, use the client from `@/utils/supabase/server-props.ts`.
|
||||
Since you're calling Supabase from `getServerSideProps`, use the client from `@/utils/supabase/server-props.ts`.
|
||||
|
||||
<Admonition type="danger">
|
||||
|
||||
|
||||
@@ -231,7 +231,7 @@ await supabase.auth.signInWithOAuth({
|
||||
|
||||
<TabPanel id="server" label="Server">
|
||||
|
||||
In the server, you need to handle the redirect to the SSO provider's authentication endpoint. The `signInWithOAuth` method returns the endpoint URL, which you can redirect to.
|
||||
In the server, you need to handle the redirect to the OAuth provider's authentication endpoint. The `signInWithOAuth` method returns the endpoint URL, which you can redirect to.
|
||||
|
||||
```js
|
||||
const { data, error } = await supabase.auth.signInWithOAuth({
|
||||
|
||||
@@ -461,7 +461,17 @@ The buckets and objects themselves are rows in the storage schema so they won't
|
||||
|
||||
### Local logging
|
||||
|
||||
Local logs rely on the Supabase Analytics Server, and are available in the Studio automatically.
|
||||
Local logs rely on the Supabase Analytics Server, and are not enabled by default. To enable this, edit your `supabase/config.toml` file and set `enabled` to `true`.
|
||||
|
||||
```bash supabase/config.toml
|
||||
[analytics]
|
||||
# Set enabled to true to see logs in the Logs Explorer
|
||||
enabled = true
|
||||
port = 54327
|
||||
vector_port = 54328
|
||||
# Configure one of the supported backends: `postgres`, `bigquery`.
|
||||
backend = "postgres"
|
||||
```
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
|
||||
@@ -374,3 +374,36 @@ Connection pooler logs are found [here](https://supabase.com/dashboard/project/_
|
||||
|
||||
- Terminating with reason :client_termination when state was :busy
|
||||
This error happens when the client terminates the connection before the connection with the database is completed.
|
||||
|
||||
## Finding your database hostname
|
||||
|
||||
Your database’s hostname is crucial for establishing a direct connection. It resolves to the underlying IP address of your database. To find your hostname, navigate to your [Database Settings](/dashboard/project/_/settings/database).
|
||||
|
||||
Note that the pooler (connection pooler) has a different IP than your database. To reveal the database host and direct connection string, hide the pooler connection string.
|
||||
|
||||
Example hostname: `db.<PROJECT_REF>.supabase.co`
|
||||
|
||||
## Managing your IP address
|
||||
|
||||
To determine your current IP address, you can use an IP address [lookup website](https://whatismyipaddress.com/hostname-ip) or the terminal command:
|
||||
|
||||
```shell
|
||||
nslookup db.<PROJECT_REF>.supabase.co
|
||||
```
|
||||
|
||||
This command queries the domain name servers to find the IP address of the given hostname.
|
||||
|
||||
Example IPv6 Address: `2a05:d014:1c06:5f0c:d7a9:8616:bee2:30df`
|
||||
|
||||
### IPv6 address
|
||||
|
||||
Upon project creation, a static IPv6 address is assigned. This address might change when:
|
||||
|
||||
- A project is paused or resumed.
|
||||
- The database is upgraded.
|
||||
|
||||
### IPv4 address
|
||||
|
||||
Opting for the static [IPv4 add-on](https://supabase.com/docs/guides/platform/ipv4-address) provides a more stable connection address. The IPv4 address remains constant unless the project is paused or resumed.
|
||||
|
||||
Unlike the IPv6 address, upgrading your database doesn't affect the IPv4 address.
|
||||
@@ -62,11 +62,13 @@ The schedule uses the standard cron syntax, in which \* means "run every time pe
|
||||
* * * * *
|
||||
```
|
||||
|
||||
You can use [crontab.guru](https://crontab.guru/) to help validate your cron schedules.
|
||||
You can use `[1-59] seconds` as the cron syntax to schedule sub-minute jobs. This is available on `pg_cron` v1.5.0+; upgrade your existing Supabase project to use this syntax.
|
||||
|
||||
Head over to [crontab.guru](https://crontab.guru/) to validate your cron schedules.
|
||||
|
||||
### Scheduling system maintenance
|
||||
|
||||
Be extremely careful when setting up pg_cron jobs for system maintenance tasks as they can have unintended consequences. For instance, scheduling a command to terminate idle connections with `pg_terminate_backend(pid)` can disrupt critical background processes like nightly backups. Often, there is an existing Postgres setting e.g. `idle_session_timeout` that can perform these common maintenance tasks without the risk.
|
||||
Be extremely careful when setting up `pg_cron` jobs for system maintenance tasks as they can have unintended consequences. For instance, scheduling a command to terminate idle connections with `pg_terminate_backend(pid)` can disrupt critical background processes like nightly backups. Often, there is an existing Postgres setting e.g. `idle_session_timeout` that can perform these common maintenance tasks without the risk.
|
||||
|
||||
Reach out to [Supabase Support](https://supabase.com/support) if you're unsure if that applies to your use case.
|
||||
|
||||
@@ -74,7 +76,7 @@ Reach out to [Supabase Support](https://supabase.com/support) if you're unsure i
|
||||
|
||||
### Delete data every week
|
||||
|
||||
Delete old data on Saturday at 3:30am (GMT):
|
||||
Delete old data every Saturday at 3:30am (GMT):
|
||||
|
||||
```sql
|
||||
select cron.schedule (
|
||||
@@ -86,21 +88,35 @@ select cron.schedule (
|
||||
|
||||
### Run a vacuum every day
|
||||
|
||||
Vacuum every day at 3:00am (GMT)
|
||||
Vacuum every day at 3:00am (GMT):
|
||||
|
||||
```sql
|
||||
select cron.schedule('nightly-vacuum', '0 3 * * *', 'VACUUM');
|
||||
```
|
||||
|
||||
### Invoke Supabase Edge Function every minute
|
||||
### Call a database function every 5 minutes
|
||||
|
||||
Make a POST request to a Supabase Edge Function every minute. Note: this requires the [`pg_net` extension](/docs/guides/database/extensions/pgnet) to be enabled.
|
||||
Create a [`hello_world()`](https://supabase.com/docs/guides/database/functions?language=sql#simple-functions) database function and then call it every 5 minutes:
|
||||
|
||||
```sql
|
||||
select cron.schedule('call-db-function', '*/5 * * * *', 'CALL hello_world()');
|
||||
```
|
||||
|
||||
### Invoke Supabase Edge Function every 30 seconds
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
This requires `pg_cron` v1.5.0+ and the [`pg_net` extension](/docs/guides/database/extensions/pgnet) to be enabled.
|
||||
|
||||
</Admonition>
|
||||
|
||||
Make a POST request to a Supabase Edge Function every 30 seconds:
|
||||
|
||||
```sql
|
||||
select
|
||||
cron.schedule(
|
||||
'invoke-function-every-minute',
|
||||
'* * * * *', -- every minute
|
||||
'invoke-function-every-half-minute',
|
||||
'30 seconds',
|
||||
$$
|
||||
select
|
||||
net.http_post(
|
||||
@@ -164,4 +180,4 @@ The records in cron.job_run_details are not cleaned automatically which will tak
|
||||
|
||||
## Resources
|
||||
|
||||
- [pg_cron GitHub Repository](https://github.com/citusdata/pg_cron)
|
||||
- [`pg_cron` GitHub Repository](https://github.com/citusdata/pg_cron)
|
||||
@@ -4,7 +4,7 @@ title: 'PGAudit: Postgres Auditing'
|
||||
description: 'Session and object auditing via PostgreSQL standard logging'
|
||||
---
|
||||
|
||||
[PGAudit](https://www.pgaudit.org) is a PostgreSQL extension that extends PostgreSQL's built-in logging abilities. It can be used to selectively track activities within your database.
|
||||
[PGAudit](https://www.pgaudit.org) extends Postgres's built-in logging abilities. It can be used to selectively track activities within your database.
|
||||
|
||||
This helps you with:
|
||||
|
||||
@@ -12,7 +12,7 @@ This helps you with:
|
||||
- **Security**: Detecting suspicious database activity
|
||||
- **Troubleshooting**: Identifying and fixing database issues
|
||||
|
||||
## Enable the Extension
|
||||
## Enable the extension
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
@@ -30,7 +30,6 @@ This helps you with:
|
||||
</TabPanel>
|
||||
<TabPanel id="sql" label="SQL">
|
||||
|
||||
{/* prettier-ignore */}
|
||||
```sql
|
||||
-- Enable the "pgaudit" extension
|
||||
create extension pgaudit;
|
||||
@@ -42,20 +41,20 @@ drop extension if exists pgaudit;
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
## Configure the Extension
|
||||
## Configure the extension
|
||||
|
||||
PGAudit can be configured with multiple different levels of precision.
|
||||
PGAudit can be configured with different levels of precision.
|
||||
|
||||
**PGAudit Logging Precision:**
|
||||
**PGAudit logging precision:**
|
||||
|
||||
- **[Session](#session-logging):** Logs activity within a connection, such as a [PSQL](https://supabase.com/docs/guides/database/connecting-to-postgres#connecting-with-psql) connection.
|
||||
- **[User](#user-logging):** Logs activity by a particular database user (e.g., anon or postgres).
|
||||
- **[Session](#session-logging):** Logs activity within a connection, such as a [psql](https://supabase.com/docs/guides/database/connecting-to-postgres#connecting-with-psql) connection.
|
||||
- **[User](#user-logging):** Logs activity by a particular database user (for example, anon or postgres).
|
||||
- **[Global](#global-logging):** Logs activity across the entire database.
|
||||
- **[Object](#object-mode-settings):** Logs events related to specific database objects (for example, the auth.users table).
|
||||
- **[Object](#object-logging):** Logs events related to specific database objects (for example, the auth.users table).
|
||||
|
||||
Although Session, User, and Global modes differ in their precision, they are all considered variants of **Session Mode** and are configured with the same input categories.
|
||||
Although Session, User, and Global modes differ in their precision, they're all considered variants of **Session Mode** and are configured with the same input categories.
|
||||
|
||||
### Session Mode Categories
|
||||
### Session mode categories
|
||||
|
||||
These modes can monitor predefined categories of database operations:
|
||||
|
||||
@@ -69,7 +68,7 @@ These modes can monitor predefined categories of database operations:
|
||||
| misc | Less common commands (FETCH, CHECKPOINT) | Captures obscure actions for deeper analysis if needed. |
|
||||
| all | Everything above | Comprehensive logging for complete audit trails. |
|
||||
|
||||
Below is a limited example of how to assign pgaudit to monitor specific categories.
|
||||
Below is a limited example of how to assign PGAudit to monitor specific categories.
|
||||
|
||||
```sql
|
||||
-- log all CREATE, ALTER, and DROP events
|
||||
@@ -82,24 +81,24 @@ Below is a limited example of how to assign pgaudit to monitor specific categori
|
||||
... pgaudit.log = 'none';
|
||||
```
|
||||
|
||||
### Session Logging
|
||||
### Session logging
|
||||
|
||||
When you are connecting in a session environment, such as a PSQL connection, you can configure pgaudit to record events initiated within the session.
|
||||
When you are connecting in a session environment, such as a [psql](https://supabase.com/docs/guides/database/connecting-to-postgres#connecting-with-psql) connection, you can configure PGAudit to record events initiated within the session.
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
The [Dashboard](https://supabase.com/dashboard/project/_) is a transactional environment and will not sustain a session.
|
||||
The [Dashboard](https://supabase.com/dashboard/project/_) is a transactional environment and won't sustain a session.
|
||||
|
||||
</Admonition>
|
||||
|
||||
Inside a session, by default, pgaudit will log nothing:
|
||||
Inside a session, by default, PGAudit will log nothing:
|
||||
|
||||
```sql
|
||||
-- returns 'none'
|
||||
show pgaudit.log;
|
||||
```
|
||||
|
||||
In the session, you can `SET` the `pgaudit.log` variable to record events:
|
||||
In the session, you can `set` the `pgaudit.log` variable to record events:
|
||||
|
||||
```sql
|
||||
-- log CREATE, ALTER, and DROP events
|
||||
@@ -112,7 +111,7 @@ set pgaudit.log = 'read, ddl';
|
||||
set pgaudit.log = 'none';
|
||||
```
|
||||
|
||||
### User Logging
|
||||
### User logging
|
||||
|
||||
There are some cases where you may want to monitor a database user's actions. For instance, let's say you connected your database to [Zapier](https://supabase.com/partners/integrations/zapier) and created a custom role for it to use:
|
||||
|
||||
@@ -130,25 +129,26 @@ To remove the settings, execute the following code:
|
||||
|
||||
```sql
|
||||
-- disables role's log
|
||||
alter role "zapier" set pgaudit.log TO 'none';
|
||||
alter role "zapier" set pgaudit.log to 'none';
|
||||
|
||||
-- check to make sure the changes are finalized:
|
||||
select rolname,
|
||||
select
|
||||
rolname,
|
||||
rolconfig
|
||||
from pg_roles
|
||||
where rolname = 'zapier';
|
||||
-- should return a rolconfig path with "pgaudit.log=none" present
|
||||
```
|
||||
|
||||
### Global Logging
|
||||
### Global logging
|
||||
|
||||
<Admonition type="caution">
|
||||
|
||||
Use global logging sparingly as it can generate many logs and make it difficult to find important events. Consider limiting the scope of what is logged by using user or object logging where possible.
|
||||
Use global logging cautiously. It can generate many logs and make it difficult to find important events. Consider limiting the scope of what is logged by using session, user, or object logging where possible.
|
||||
|
||||
</Admonition>
|
||||
|
||||
The below SQL configures pgaudit to record all events associated with the "postgres" role. Since postgres has extensive privileges, this effectively monitors all database activity.
|
||||
The below SQL configures PGAudit to record all events associated with the "postgres" role. Since it has extensive privileges, this effectively monitors all database activity.
|
||||
|
||||
```sql
|
||||
alter role "postgres" set pgaudit.log to 'all';
|
||||
@@ -171,35 +171,31 @@ To remove the settings, execute the following code:
|
||||
alter role "postgres" set pgaudit.log to 'none';
|
||||
```
|
||||
|
||||
## Object Mode Settings
|
||||
### Object logging
|
||||
|
||||
Assigning a role to pgaudit will result in the logging of **any actions** that role is **capable** of performing, _regardless of who or what actually initiated the event_.
|
||||
|
||||
### Object Logging
|
||||
|
||||
To fine-tune what pgaudit will record, you must create a custom database role with limited permissions:
|
||||
To fine-tune what object events PGAudit will record, you must create a custom database role with limited permissions:
|
||||
|
||||
```sql
|
||||
create role "some_audit_role" noinherit;
|
||||
```
|
||||
|
||||
No other Postgres user can assume or login via this role. It solely exists to securely define what pgaudit will record.
|
||||
No other Postgres user can assume or login via this role. It solely exists to securely define what PGAudit will record.
|
||||
|
||||
Once the role is created, you can direct pgaudit to log by assigning it to the `pgaudit.role` variable:
|
||||
Once the role is created, you can direct PGAudit to log by assigning it to the `pgaudit.role` variable:
|
||||
|
||||
```sql
|
||||
alter role "postgres" set pgaudit.role to 'some_audit_role';
|
||||
```
|
||||
|
||||
You can then assign the role to monitor only approved events, such as `select` statements that include a specific table:
|
||||
You can then assign the role to monitor only approved object events, such as `select` statements that include a specific table:
|
||||
|
||||
```sql
|
||||
grant select on random_table to "some_audit_role";
|
||||
```
|
||||
|
||||
With this privilege granted, pgaudit will record all select statements against the `random_table`. Note, that this does include joins that feature the table, too. All assignignable priveleges can be viewed in the [PostgreSQL documentation](https://www.postgresql.org/docs/current/ddl-priv.html).
|
||||
With this privilege granted, PGAudit will record all select statements that reference the `random_table`, regardless of _who_ or _what_ actually initiated the event. All assignable privileges can be viewed in the [Postgres documentation](https://www.postgresql.org/docs/current/ddl-priv.html).
|
||||
|
||||
If you would no longer like to use object logging, you will need to unnasign the `pgaudit.role` variable:
|
||||
If you would no longer like to use object logging, you will need to unassign the `pgaudit.role` variable:
|
||||
|
||||
```sql
|
||||
-- change pgaudit.role to no longer reference some_audit_role
|
||||
@@ -214,27 +210,21 @@ where rolname = 'postgres';
|
||||
-- should return a rolconfig path with "pgaudit.role="
|
||||
```
|
||||
|
||||
If you are no longer using a `ROLE`, it is best to `DROP`it:
|
||||
|
||||
```sql
|
||||
drop role "some_audit_role";
|
||||
```
|
||||
|
||||
## Interpreting Audit Logs
|
||||
|
||||
pgaudit was designed for storing logs as CSV files with the following headers:
|
||||
PGAudit was designed for storing logs as CSV files with the following headers:
|
||||
|
||||
> Referenced from the [pgaudit official docs](https://github.com/pgaudit/pgaudit/blob/master/README.md#format)
|
||||
> Referenced from the [PGAudit official docs](https://github.com/pgaudit/pgaudit/blob/master/README.md#format)
|
||||
|
||||
| header | Description |
|
||||
| --------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| AUDIT_TYPE | SESSION or OBJECT |
|
||||
| STATEMENT_ID | Unique statement ID for this session. Sequential even if some statements are not logged. |
|
||||
| SUBSTATEMENT_ID | Sequential ID for each sub-statement within the main statement. Continuous even if some are not logged. |
|
||||
| CLASS | e.g., READ, ROLE (see pgaudit.log). |
|
||||
| COMMAND | e.g., ALTER TABLE, SELECT. |
|
||||
| CLASS | ..., READ, ROLE (see pgaudit.log). |
|
||||
| COMMAND | ..., ALTER TABLE, SELECT. |
|
||||
| OBJECT_TYPE | TABLE, INDEX, VIEW, etc. Available for SELECT, DML, and most DDL statements. |
|
||||
| OBJECT_NAME | The fully qualified object name (e.g., public.account). Available for SELECT, DML, and most DDL. |
|
||||
| OBJECT_NAME | The fully qualified object name (for example, public.account). Available for SELECT, DML, and most DDL. |
|
||||
| STATEMENT | Statement executed on the backend. |
|
||||
| PARAMETER | If pgaudit.log_parameter is set, this field contains the statement parameters as quoted CSV, or \<none\>. Otherwise, it's \<not logged\>. |
|
||||
|
||||
@@ -258,9 +248,9 @@ Generates the following log in the [Dashboard's Postgres Logs](https://supabase.
|
||||
); <not logged>
|
||||
```
|
||||
|
||||
## Finding and Filtering Audit Logs
|
||||
## Finding and filtering audit logs
|
||||
|
||||
Logs generated by pgaudit can be found in [Postgres Logs](https://supabase.com/dashboard/project/_/logs/postgres-logs?s=AUDIT). To find a specific log, you can use the log explorer. Below is a basic example to extract logs referencing `CREATE TABLE` events
|
||||
Logs generated by PGAudit can be found in [Postgres Logs](https://supabase.com/dashboard/project/_/logs/postgres-logs?s=AUDIT). To find a specific log, you can use the log explorer. Below is a basic example to extract logs referencing `CREATE TABLE` events
|
||||
|
||||
```sql
|
||||
select
|
||||
@@ -275,7 +265,7 @@ order by timestamp desc
|
||||
limit 100;
|
||||
```
|
||||
|
||||
## Practical Examples
|
||||
## Practical examples
|
||||
|
||||
### Monitoring API events
|
||||
|
||||
@@ -296,9 +286,9 @@ alter role "authenticator" set pgaudit.log to 'write';
|
||||
-- alter role "service_role" set pgaudit.log TO 'write';
|
||||
```
|
||||
|
||||
### Monitoring the `auth.users` Table
|
||||
### Monitoring the `auth.users` table
|
||||
|
||||
In the worst case scenario, where a privileged roles' password is exposed, you can use pgaudit to monitor if the `auth.users` table was targeted. It should be stated that API requests are already monitored in the [API Edge Network](https://supabase.com/dashboard/project/_/logs/edge-logs) and this is more about providing greater clarity about what is happening at the database level.
|
||||
In the worst case scenario, where a privileged roles' password is exposed, you can use PGAudit to monitor if the `auth.users` table was targeted. It should be stated that API requests are already monitored in the [API Edge Network](https://supabase.com/dashboard/project/_/logs/edge-logs) and this is more about providing greater clarity about what is happening at the database level.
|
||||
|
||||
Logging `auth.user` should be done in Object Mode and requires a custom role:
|
||||
|
||||
@@ -312,18 +302,17 @@ grant delete on auth.users to "auth_auditor";
|
||||
|
||||
-- assign auth_auditor to pgaudit.role
|
||||
alter role "postgres" set pgaudit.role to 'auth_auditor';
|
||||
|
||||
```
|
||||
|
||||
With the above code, any query involving reading or deleting the auth.users email column will be logged.
|
||||
With the above code, any query involving reading or deleting from the auth.users table will be logged.
|
||||
|
||||
## Best Practices
|
||||
## Best practices
|
||||
|
||||
### Over-logging: Help! Too many logs!
|
||||
### Disabling excess logging
|
||||
|
||||
pgaudit, if not configured mindfully, can log all database events, including background tasks. This can generate an undesirablely large amount of logs in a few hours.
|
||||
PGAudit, if not configured mindfully, can log all database events, including background tasks. This can generate an undesirably large amount of logs in a few hours.
|
||||
|
||||
The first step to solve this problem is to identify which database users pgaudit is observing:
|
||||
The first step to solve this problem is to identify which database users PGAudit is observing:
|
||||
|
||||
```sql
|
||||
-- find all users monitored by pgaudit
|
||||
@@ -340,7 +329,7 @@ where
|
||||
);
|
||||
```
|
||||
|
||||
To prevent pgaudit from monitoring the problematic roles, you'll want to change their `pgaudit.log` values to `none` and `pgaudit.role` values to `empty quotes ''`
|
||||
To prevent PGAudit from monitoring the problematic roles, you'll want to change their `pgaudit.log` values to `none` and `pgaudit.role` values to `empty quotes ''`
|
||||
|
||||
```sql
|
||||
-- Use to disable object level logging
|
||||
@@ -352,7 +341,7 @@ To prevent pgaudit from monitoring the problematic roles, you'll want to change
|
||||
|
||||
## FAQ
|
||||
|
||||
#### How can I use pgaudit to debug database functions?
|
||||
#### Can I use PGAudit to debug database functions?
|
||||
|
||||
Technically yes, but it is not the best approach. It is better to check out our [function debugging guide](https://supabase.com/docs/guides/database/functions#general-logging) instead.
|
||||
|
||||
@@ -362,7 +351,7 @@ In the [Logs Dashboard](https://supabase.com/dashboard/project/_/logs/postgres-l
|
||||
|
||||
#### Can I log observed table rows?
|
||||
|
||||
By default, pgaudit records queries, but not the returned rows. You can modify this behavior with the `pgaudit.log_rows` variable:
|
||||
By default, PGAudit records queries, but not the returned rows. You can modify this behavior with the `pgaudit.log_rows` variable:
|
||||
|
||||
```sql
|
||||
--enable
|
||||
@@ -372,31 +361,31 @@ alter role "postgres" set pgaudit.log_rows to 'on';
|
||||
alter role "postgres" set pgaudit.log_rows to 'off';
|
||||
```
|
||||
|
||||
You should not do this unless you are _absolutely_ certain it is necessary for your use case. It can expose sensitive values to your logs that ideally should not be preserved. Furthermore, if done in excess, it can noticably reduce database performance.
|
||||
You should not do this unless you are _absolutely_ certain it is necessary for your use case. It can expose sensitive values to your logs that ideally should not be preserved. Furthermore, if done in excess, it can noticeably reduce database performance.
|
||||
|
||||
#### Can I log function parameters?
|
||||
|
||||
We don't currently support configuring pgaudit.log_parameter because it may log secrets in encrypted columns if you are using [pgsodium](https://supabase.com/docs/guides/database/extensions/pgsodium) or[Vault](https://supabase.com/docs/guides/database/vault).
|
||||
We don't currently support configuring `pgaudit.log_parameter` because it may log secrets in encrypted columns if you are using [pgsodium](https://supabase.com/docs/guides/database/extensions/pgsodium) or[Vault](https://supabase.com/docs/guides/database/vault).
|
||||
|
||||
You can upvote this [feature request](https://github.com/orgs/supabase/discussions/20183) with your use-case if you'd like this restriction lifted.
|
||||
|
||||
#### Why do the official pgaudit docs differ from the Supabase one?
|
||||
#### Does PGAudit support system wide configurations?
|
||||
|
||||
pgaudit allows settings to be applied at 3 different database scopes:
|
||||
PGAudit allows settings to be applied to 3 different database scopes:
|
||||
|
||||
| Scope | Definition | Configuration File/Command |
|
||||
| Scope | Description | Configuration File/Command |
|
||||
| -------- | ------------------ | -------------------------- |
|
||||
| System | Entire server | ALTER SYSTEM commands |
|
||||
| Database | Specific database | ALTER DATABASE commands |
|
||||
| Role | Specific user/role | ALTER ROLE commands |
|
||||
|
||||
Supabase limits full privileges for file system and database variables, meaning pgaudit modifications can only occur at the ROLE level. Assigning pgaudit to the 'postgres' role provides it with the role's database visibility, allowing `ROLE` level adjustments a practical alternative to DATABASE/SYSTEM level configurations.
|
||||
Supabase limits full privileges for file system and database variables, meaning PGAudit modifications can only occur at the role level. Assigning PGAudit to the 'postgres' role grants it nearly complete visibility into the database, making role-level adjustments a practical alternative to configuring at the database or system level.
|
||||
|
||||
pgaudit's [official documentation](https://www.pgaudit.org) focuses on system and database level configs, but its docs officially supports `ROLE` level configs, too:
|
||||
PGAudit's [official documentation](https://www.pgaudit.org) focuses on system and database level configs, but its docs officially supports role level configs, too.
|
||||
|
||||
## Resources
|
||||
|
||||
- Official [`PGAudit` documentation](https://www.pgaudit.org)
|
||||
- [Official `PGAudit` documentation](https://www.pgaudit.org)
|
||||
- [Database Function Logging](https://supabase.com/docs/guides/database/functions#general-logging)
|
||||
- [Supabase Logging](https://supabase.com/docs/guides/platform/logs)
|
||||
- [Self-Hosting Logs](https://supabase.com/docs/reference/self-hosting-analytics/introduction)
|
||||
@@ -347,7 +347,7 @@ Good targets to log include:
|
||||
|
||||
#### General logging
|
||||
|
||||
To create custom logs in the [Dashboard's Postgres Logs](/apps/docs/public/img/database/postgres-logs.png), you can use the `raise` keyword. By default, there are 3 observed severity levels:
|
||||
To create custom logs in the [Dashboard's Postgres Logs](https://supabase.com/dashboard/project/_/logs/postgres-logs), you can use the `raise` keyword. By default, there are 3 observed severity levels:
|
||||
|
||||
- `log`
|
||||
- `warning`
|
||||
|
||||
@@ -177,6 +177,6 @@ import { Sheets } from 'npm:@googleapis/sheets'
|
||||
- Outgoing connections to ports `25` and `587` are not allowed.
|
||||
- Serving of HTML content is not supported (`GET` requests that return `text/html` will be rewritten to `text/plain`).
|
||||
- Deno and Node file system APIs are not available.
|
||||
- Memory limit for the function: 150 MB (heap) + 150 MB (external - array buffers/ WASM)
|
||||
- Memory limit for the function: 256MB
|
||||
- Wall clock limit: 400 s
|
||||
- CPU Time: 2 s
|
||||
@@ -78,7 +78,7 @@ Deno.serve(async (req) => {
|
||||
|
||||
```bash
|
||||
supabase functions deploy cloudflare-turnstile
|
||||
supabase secrets set CLOUDFLARE_TURNSTILE_SECRET_KEY=your_secret_key
|
||||
supabase secrets set CLOUDFLARE_SECRET_KEY=your_secret_key
|
||||
```
|
||||
|
||||
## Invoke the function from your site
|
||||
|
||||
@@ -117,7 +117,7 @@ hideToc: true
|
||||
|
||||
```dart lib/main.dart
|
||||
class MyApp extends StatelessWidget {
|
||||
const MyApp({Key? key}) : super(key: key);
|
||||
const MyApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
@@ -171,6 +171,6 @@ hideToc: true
|
||||
|
||||
## Next steps
|
||||
|
||||
- Set up [Auth](/docs/guides/auth/server-side/overview) for your app
|
||||
- Set up [Auth](/docs/guides/auth) for your app
|
||||
- [Insert more data](/docs/guides/database/import-data) into your database
|
||||
- Upload and serve static files using [Storage](/docs/guides/storage)
|
||||
@@ -139,6 +139,6 @@ hideToc: true
|
||||
|
||||
## Next steps
|
||||
|
||||
- Set up [Auth](/docs/guides/auth/server-side/overview) for your app
|
||||
- Set up [Auth](/docs/guides/auth) for your app
|
||||
- [Insert more data](/docs/guides/database/import-data) into your database
|
||||
- Upload and serve static files using [Storage](/docs/guides/storage)
|
||||
@@ -161,6 +161,6 @@ hideToc: true
|
||||
|
||||
## Next steps
|
||||
|
||||
- Set up [Auth](/docs/guides/auth/server-side/overview) for your app
|
||||
- Set up [Auth](/docs/guides/auth) for your app
|
||||
- [Insert more data](/docs/guides/database/import-data) into your database
|
||||
- Upload and serve static files using [Storage](/docs/guides/storage)
|
||||
@@ -85,13 +85,23 @@ export const handle: Handle = async ({ event, resolve }) => {
|
||||
})
|
||||
|
||||
/**
|
||||
* A convenience helper so we can just call await getSession() instead const { data: { session } } = await supabase.auth.getSession()
|
||||
* Unlike `supabase.auth.getSession`, which is unsafe on the server because it
|
||||
* doesn't validate the JWT, this function validates the JWT by first calling
|
||||
* `getUser` and aborts early if the JWT signature is invalid.
|
||||
*/
|
||||
event.locals.getSession = async () => {
|
||||
event.locals.safeGetSession = async () => {
|
||||
const {
|
||||
data: { user },
|
||||
error,
|
||||
} = await supabase.auth.getUser()
|
||||
if (error) {
|
||||
return { session: null, user: null }
|
||||
}
|
||||
|
||||
const {
|
||||
data: { session },
|
||||
} = await event.locals.supabase.auth.getSession()
|
||||
return session
|
||||
return { session, user }
|
||||
}
|
||||
|
||||
return resolve(event, {
|
||||
@@ -104,9 +114,7 @@ export const handle: Handle = async ({ event, resolve }) => {
|
||||
|
||||
<GetSessionWarning />
|
||||
|
||||
In this case, the session information from `getSession` is supplied to the Supabase client so it can retrieve the auth token. This is safe, since the auth token signature will be revalidated on the Auth server. But you shouldn't trust the unsigned data that is stored alongside the JWT.
|
||||
|
||||
If you are using TypeScript the compiler might complain about `event.locals.supabase` and `event.locals.getSession`, this can be fixed by updating your `src/app.d.ts` with the content below:
|
||||
If you are using TypeScript the compiler might complain about `event.locals.supabase` and `event.locals.safeGetSession`, this can be fixed by updating your `src/app.d.ts` with the content below:
|
||||
|
||||
```ts src/app.d.ts
|
||||
// src/app.d.ts
|
||||
@@ -117,10 +125,11 @@ declare global {
|
||||
namespace App {
|
||||
interface Locals {
|
||||
supabase: SupabaseClient
|
||||
getSession(): Promise<Session | null>
|
||||
safeGetSession(): Promise<{ session: Session | null; user: User | null }>
|
||||
}
|
||||
interface PageData {
|
||||
session: Session | null
|
||||
user: User | null
|
||||
}
|
||||
// interface Error {}
|
||||
// interface Platform {}
|
||||
@@ -134,9 +143,12 @@ Create a new `src/routes/+layout.server.ts` file to handle the session on the se
|
||||
// src/routes/+layout.server.ts
|
||||
import type { LayoutServerLoad } from './$types'
|
||||
|
||||
export const load: LayoutServerLoad = async ({ locals: { getSession } }) => {
|
||||
export const load: LayoutServerLoad = async ({ locals: { safeGetSession } }) => {
|
||||
const { session, user } = await safeGetSession()
|
||||
|
||||
return {
|
||||
session: await getSession(),
|
||||
session,
|
||||
user,
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -170,6 +182,11 @@ export const load: LayoutLoad = async ({ fetch, data, depends }) => {
|
||||
},
|
||||
})
|
||||
|
||||
/**
|
||||
* It's fine to use `getSession` here, because on the client, `getSession` is
|
||||
* safe, and on the server, it reads `session` from the `LayoutData`, which
|
||||
* safely checked the session using `safeGetSession`.
|
||||
*/
|
||||
const {
|
||||
data: { session },
|
||||
} = await supabase.auth.getSession()
|
||||
@@ -259,8 +276,8 @@ Create a `src/routes/+page.server.ts` file that will return our website URL to b
|
||||
import { redirect } from '@sveltejs/kit'
|
||||
import type { PageServerLoad } from './$types'
|
||||
|
||||
export const load: PageServerLoad = async ({ url, locals: { getSession } }) => {
|
||||
const session = await getSession()
|
||||
export const load: PageServerLoad = async ({ url, locals: { safeGetSession } }) => {
|
||||
const { session } = await safeGetSession()
|
||||
|
||||
// if the user is already logged in return them to the account page
|
||||
if (session) {
|
||||
@@ -419,8 +436,8 @@ and handle all our form actions through the `actions` object.
|
||||
import { fail, redirect } from '@sveltejs/kit'
|
||||
import type { Actions, PageServerLoad } from './$types'
|
||||
|
||||
export const load: PageServerLoad = async ({ locals: { supabase, getSession } }) => {
|
||||
const session = await getSession()
|
||||
export const load: PageServerLoad = async ({ locals: { supabase, safeGetSession } }) => {
|
||||
const { session } = await safeGetSession()
|
||||
|
||||
if (!session) {
|
||||
throw redirect(303, '/')
|
||||
@@ -436,14 +453,14 @@ export const load: PageServerLoad = async ({ locals: { supabase, getSession } })
|
||||
}
|
||||
|
||||
export const actions: Actions = {
|
||||
update: async ({ request, locals: { supabase, getSession } }) => {
|
||||
update: async ({ request, locals: { supabase, safeGetSession } }) => {
|
||||
const formData = await request.formData()
|
||||
const fullName = formData.get('fullName') as string
|
||||
const username = formData.get('username') as string
|
||||
const website = formData.get('website') as string
|
||||
const avatarUrl = formData.get('avatarUrl') as string
|
||||
|
||||
const session = await getSession()
|
||||
const { session } = await safeGetSession()
|
||||
|
||||
const { error } = await supabase.from('profiles').upsert({
|
||||
id: session?.user.id,
|
||||
@@ -470,8 +487,8 @@ export const actions: Actions = {
|
||||
avatarUrl,
|
||||
}
|
||||
},
|
||||
signout: async ({ locals: { supabase, getSession } }) => {
|
||||
const session = await getSession()
|
||||
signout: async ({ locals: { supabase, safeGetSession } }) => {
|
||||
const { session } = await safeGetSession()
|
||||
if (session) {
|
||||
await supabase.auth.signOut()
|
||||
throw redirect(303, '/')
|
||||
|
||||
@@ -64,7 +64,7 @@ Read more on [how billing for compute works](https://supabase.com/docs/guides/pl
|
||||
|
||||
#### What is unified egress and how is it billed?
|
||||
|
||||
Unified egress refers to the total egress quota available to each organization. This quota can be utilized for various purposes such as Storage, Runtime, Auth, Functions, and Database. Each plan includes a specific egress quota, and any additional usage beyond that quota is billed accordingly.
|
||||
Unified egress refers to the total egress quota available to each organization. This quota can be utilized for various purposes such as Storage, Realtime, Auth, Functions, Supavisor and Database. Each plan includes a specific egress quota, and any additional usage beyond that quota is billed accordingly.
|
||||
|
||||
To learn more about unified egress and its details, please refer to the information provided [here](https://supabase.com/docs/guides/platform/org-based-billing#:~:text=running%20any%20projects.-,Unified%20egress,-%23).
|
||||
|
||||
|
||||
@@ -18,15 +18,6 @@ When you're ready to ship your changes, merge your branch to update your product
|
||||
|
||||
If you understand Git, you already understand Supabase Branching.
|
||||
|
||||
<Admonition
|
||||
type="note"
|
||||
label="Branching is Early Access only, and only available on the Pro Plan and above."
|
||||
>
|
||||
|
||||
Branching will move to Public Beta in future, but only available for Pro plan and above.
|
||||
|
||||
</Admonition>
|
||||
|
||||
## How branching works
|
||||
|
||||
Supabase Branching works with Git. You can test changes in a separate, temporary environment without affecting your production setup. When you're ready to ship your changes, merge your branch to update your production instance with the new changes.
|
||||
|
||||
@@ -6,18 +6,25 @@ description: "Learn about your project's instance and additional add-ons."
|
||||
|
||||
Every project on the Supabase Platform comes with its own dedicated Postgres instance running inside a virtual machine (VM). The following table describes the base instance with additional compute add-ons available if you need extra performance when scaling up Supabase.
|
||||
|
||||
| Plan | Hourly Price USD | Monthly Price USD | CPU | Memory | Connections: Direct | Connections: Pooler | Recommended Maximum DB Size |
|
||||
| ------ | ---------------- | ----------------- | ----------------------- | ------ | ------------------- | ------------------- | --------------------------- |
|
||||
| Micro | $0.01344 | ~$10 | 2-core ARM (shared) | 1 GB | 60 | 200 | 10GB |
|
||||
| Small | $0.0206 | ~$15 | 2-core ARM (shared) | 2 GB | 90 | 200 | 50GB |
|
||||
| Medium | $0.0822 | ~$60 | 2-core ARM (shared) | 4 GB | 120 | 200 | 100GB |
|
||||
| Large | $0.1517 | ~$110 | 2-core ARM (dedicated) | 8 GB | 160 | 300 | 200GB |
|
||||
| XL | $0.2877 | ~$210 | 4-core ARM (dedicated) | 16 GB | 240 | 700 | 500GB |
|
||||
| 2XL | $0.562 | ~$410 | 8-core ARM (dedicated) | 32 GB | 380 | 1500 | 1TB |
|
||||
| 4XL | $1.32 | ~$960 | 16-core ARM (dedicated) | 64 GB | 480 | 3000 | 2TB |
|
||||
| 8XL | $2.562 | ~$1,870 | 32-core ARM (dedicated) | 128 GB | 490 | 6000 | 4TB |
|
||||
| 12XL | $3.836 | ~$2,800 | 48-core ARM (dedicated) | 192 GB | 500 | 9000 | 6TB |
|
||||
| 16XL | $5.12 | ~$3,730 | 64-core ARM (dedicated) | 256 GB | 500 | 12,000 | 10TB |
|
||||
<Admonition type="note">
|
||||
|
||||
Free plan resources subject to changes.
|
||||
|
||||
</Admonition>
|
||||
|
||||
| Plan | Hourly Price USD | Monthly Price USD | CPU | Memory | Connections: Direct | Connections: Pooler | Recommended Maximum DB Size |
|
||||
| ----------- | ---------------- | ----------------- | ----------------------- | ------------ | ------------------- | ------------------- | --------------------------- |
|
||||
| Nano (free) | $0 | $0 | Shared | Up to 0.5 GB | 60 | 200 | 500MB |
|
||||
| Micro | $0.01344 | ~$10 | 2-core ARM (shared) | 1 GB | 60 | 200 | 10GB |
|
||||
| Small | $0.0206 | ~$15 | 2-core ARM (shared) | 2 GB | 90 | 400 | 50GB |
|
||||
| Medium | $0.0822 | ~$60 | 2-core ARM (shared) | 4 GB | 120 | 600 | 100GB |
|
||||
| Large | $0.1517 | ~$110 | 2-core ARM (dedicated) | 8 GB | 160 | 800 | 200GB |
|
||||
| XL | $0.2877 | ~$210 | 4-core ARM (dedicated) | 16 GB | 240 | 1,000 | 500GB |
|
||||
| 2XL | $0.562 | ~$410 | 8-core ARM (dedicated) | 32 GB | 380 | 1,500 | 1TB |
|
||||
| 4XL | $1.32 | ~$960 | 16-core ARM (dedicated) | 64 GB | 480 | 3,000 | 2TB |
|
||||
| 8XL | $2.562 | ~$1,870 | 32-core ARM (dedicated) | 128 GB | 490 | 6,000 | 4TB |
|
||||
| 12XL | $3.836 | ~$2,800 | 48-core ARM (dedicated) | 192 GB | 500 | 9,000 | 6TB |
|
||||
| 16XL | $5.12 | ~$3,730 | 64-core ARM (dedicated) | 256 GB | 500 | 12,000 | 10TB |
|
||||
|
||||
Number of connections above are recommended values.
|
||||
|
||||
@@ -45,18 +52,19 @@ When considering compute upgrades, assess whether your bottlenecks are hardware-
|
||||
|
||||
SSD Disks are attached to your servers and the disk performance depends on the compute add-on of your instance. Disk IO refers to two metrics: throughput (Megabits per Second) and IOPS (Input/Output Operations per Second). Disk throughput is capped at 1,048 Mbps and IOPS is capped at 3,000 IOPS for all compute add-ons.
|
||||
|
||||
| Plan | Baseline Disk Throughput | Baseline IOPS |
|
||||
| ------ | ------------------------ | ------------- |
|
||||
| Micro | 87 Mbps | 500 IOPS |
|
||||
| Small | 174 Mbps | 1,000 IOPS |
|
||||
| Medium | 347 Mbps | 2,000 IOPS |
|
||||
| Large | 630 Mbps | 3,000 IOPS |
|
||||
| XL | 1,048 Mbps | 3,000 IOPS |
|
||||
| 2XL | 1,048 Mbps | 3,000 IOPS |
|
||||
| 4XL | 1,048 Mbps | 3,000 IOPS |
|
||||
| 8XL | 1,048 Mbps | 3,000 IOPS |
|
||||
| 12XL | 1,048 Mbps | 3,000 IOPS |
|
||||
| 16XL | 1,048 Mbps | 3,000 IOPS |
|
||||
| Plan | Baseline Disk Throughput | Baseline IOPS |
|
||||
| ----------- | ------------------------ | ------------- |
|
||||
| Nano (free) | 43 Mbps | 250 IOPS |
|
||||
| Micro | 87 Mbps | 500 IOPS |
|
||||
| Small | 174 Mbps | 1,000 IOPS |
|
||||
| Medium | 347 Mbps | 2,000 IOPS |
|
||||
| Large | 630 Mbps | 3,000 IOPS |
|
||||
| XL | 1,048 Mbps | 3,000 IOPS |
|
||||
| 2XL | 1,048 Mbps | 3,000 IOPS |
|
||||
| 4XL | 1,048 Mbps | 3,000 IOPS |
|
||||
| 8XL | 1,048 Mbps | 3,000 IOPS |
|
||||
| 12XL | 1,048 Mbps | 3,000 IOPS |
|
||||
| 16XL | 1,048 Mbps | 3,000 IOPS |
|
||||
|
||||
[Contact us](https://supabase.com/contact/enterprise) if you require a plan with higher disk throughput and IOPS.
|
||||
|
||||
@@ -74,18 +82,19 @@ If you're unsure of how much throughput or IOPS your application requires, you c
|
||||
|
||||
The maximum number of replication slots and WAL senders depends on your compute add-on plan, as follows:
|
||||
|
||||
| Plan | Max Replication Slots | Max WAL Senders |
|
||||
| ------ | --------------------- | --------------- |
|
||||
| Micro | 5 | 5 |
|
||||
| Small | 5 | 5 |
|
||||
| Medium | 5 | 5 |
|
||||
| Large | 8 | 8 |
|
||||
| XL | 24 | 24 |
|
||||
| 2XL | 80 | 80 |
|
||||
| 4XL | 80 | 80 |
|
||||
| 8XL | 80 | 80 |
|
||||
| 12XL | 80 | 80 |
|
||||
| 16XL | 80 | 80 |
|
||||
| Plan | Max Replication Slots | Max WAL Senders |
|
||||
| ----------- | --------------------- | --------------- |
|
||||
| Nano (free) | 5 | 5 |
|
||||
| Micro | 5 | 5 |
|
||||
| Small | 5 | 5 |
|
||||
| Medium | 5 | 5 |
|
||||
| Large | 8 | 8 |
|
||||
| XL | 24 | 24 |
|
||||
| 2XL | 80 | 80 |
|
||||
| 4XL | 80 | 80 |
|
||||
| 8XL | 80 | 80 |
|
||||
| 12XL | 80 | 80 |
|
||||
| 16XL | 80 | 80 |
|
||||
|
||||
<Admonition type="caution">
|
||||
|
||||
|
||||
@@ -59,6 +59,12 @@ Projects on the Pro plan and above have auto-scaling Disk Storage.
|
||||
|
||||
Disk Storage expands automatically when the database reaches 90% of the disk size. The disk is expanded to be 50% larger (e.g., 8GB -> 12GB). Auto-scaling can only take place once every 6 hours. If within those 6 hours you reach 95% of the disk space, your project will enter read-only mode.
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
The automatic resize operation will add an additional 50% capped to a maximum of 200GB. If 50% of your current usage is more than 200GB then only 200GB will be added to your disk (e.g. a size of 1500GB will resize to 1700GB).
|
||||
|
||||
</Admonition>
|
||||
|
||||
The Disk Storage Size can also be manually expanded on the [Database settings page](https://supabase.com/dashboard/project/_/settings/database). The maximum default you can expand the Disk Storage to is 200GB. If you wish to manually expand it to any more than 200GB, please [contact us](https://supabase.com/dashboard/support/new) to discuss expanding the 200GB limit.
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
@@ -92,7 +92,7 @@ Additionally, Compute Credits are given to you not only for the first month but
|
||||
|
||||
## Unified egress
|
||||
|
||||
Each organization receives a total egress quota (eg. 250GB on the Pro plan) which can be used for anything - Storage, Realtime, Auth, Functions or Database.
|
||||
Each organization receives a total egress quota (eg. 250GB on the Pro plan) which can be used for anything - Storage, Realtime, Auth, Functions, Supavisor or Database.
|
||||
|
||||
### Included egress quota
|
||||
|
||||
|
||||
@@ -175,21 +175,7 @@ You can pair the information available from `pg_stat_statements` with the detail
|
||||
|
||||
## Optimizing the number of connections
|
||||
|
||||
By default, the number of connections allowed to Postgres and Supavisor is configured based on the resources available to the database.
|
||||
|
||||
| Compute Add-on | Postgresql connections | Supavisor connections |
|
||||
| -------------- | ---------------------- | --------------------- |
|
||||
| - (Free plan) | 30 | 200 |
|
||||
| Micro | 60 | 200 |
|
||||
| Small | 90 | 200 |
|
||||
| Medium | 120 | 200 |
|
||||
| Large | 160 | 300 |
|
||||
| XL | 240 | 700 |
|
||||
| 2XL | 380 | 1500 |
|
||||
| 4XL | 480 | 3000 |
|
||||
| 8XL | 490 | 6000 |
|
||||
| 12XL | 500 | 9000 |
|
||||
| 16XL | 500 | 12000 |
|
||||
The default connection limits for Postgres and Supavisor is based on your compute size. See the default connection numbers in the [Compute Add-ons](/docs/guides/platform/compute-add-ons) section.
|
||||
|
||||
If the number of connections is insufficient, you will receive the following error upon connecting to the DB:
|
||||
|
||||
|
||||
@@ -51,10 +51,11 @@ A project needs to satisfy these requirements to deploy a Read Replica:
|
||||
- Support for projects on Fly.io is coming.
|
||||
1. Running on at least a [Small compute add-on](/docs/guides/platform/compute-add-ons).
|
||||
- Read Replicas are currently placed on the same compute instance as the database to make sure they can keep up with the Primary's activities.
|
||||
1. Using [physical backups](/docs/guides/platform/backups#point-in-time-recovery)
|
||||
- Physical backups are automatically enabled if using [PITR](/docs/guides/platform/backups#point-in-time-recovery)
|
||||
1. Running on Postgres 15.
|
||||
- For projects running on older versions of PostgreSQL, you will need to [upgrade to the latest platform version](/docs/guides/platform/migrating-and-upgrading-projects#pgupgrade).
|
||||
1. Using [physical backups](/docs/guides/platform/backups#point-in-time-recovery)
|
||||
- Physical backups are automatically enabled if using [PITR](/docs/guides/platform/backups#point-in-time-recovery)
|
||||
- If you're not using PITR, you'll be able to switch to physical backups as part of the Read Replica setup process. Note that physical backups can't be downloaded from the dashboard in the way logical backups can.
|
||||
|
||||
## Getting started
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ description: 'General information about enabling single sign-on (SSO) for your o
|
||||
|
||||
<Admonition>
|
||||
|
||||
Looking for docs on how to add Single Sign-On support in your Supabase project? Head on over to [Single Sign-On with SAML 2.0 for Projects](/docs/guides/auth/sso/auth-sso-saml).
|
||||
Looking for docs on how to add Single Sign-On support in your Supabase project? Head on over to [Single Sign-On with SAML 2.0 for Projects](/docs/guides/auth/enterprise-sso/auth-sso-saml).
|
||||
|
||||
</Admonition>
|
||||
|
||||
|
||||
@@ -150,6 +150,7 @@ Build a basic Todo List with Supabase and your favorite frontend framework:
|
||||
- Supabase + Ory Cloud [Github](https://github.com/ory/examples/tree/master/supabase-ory-cloud)
|
||||
- Supabase Auth with React Native + Next.js (Monorepo) [Github](https://github.com/mateoguzmana/react-native-next-supabase-auth-monorepo)
|
||||
- Supabase Auth with Nuxt3 [Github](https://github.com/zackha/supaAuth)
|
||||
- Supabase Auth with Hono [Github](https://github.com/CarlosZiegler/hono-supabase)
|
||||
|
||||
### Blog posts
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ Supabase Storage implements the [TUS protocol](https://tus.io/) to enable resuma
|
||||
const projectId = ''
|
||||
|
||||
async function uploadFile(bucketName, fileName, file) {
|
||||
const { data: session } = await supabase.auth.session()
|
||||
const { data: { session } } = await supabase.auth.getSession()
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
var upload = new tus.Upload(file, {
|
||||
|
||||
@@ -23,10 +23,13 @@ export const primaryLinks = [
|
||||
]
|
||||
|
||||
export const secondaryLinks = [
|
||||
{ title: 'Contributing', url: '/handbook/contributing' },
|
||||
{
|
||||
title: 'Contributing',
|
||||
url: 'https://github.com/supabase/supabase/blob/master/apps/docs/DEVELOPERS.md',
|
||||
},
|
||||
{
|
||||
title: 'Author Styleguide',
|
||||
url: 'https://github.com/supabase/supabase/blob/master/DEVELOPERS.md',
|
||||
url: 'https://github.com/supabase/supabase/blob/master/apps/docs/CONTRIBUTING.md',
|
||||
},
|
||||
{ title: 'Open Source', url: 'https://supabase.com/open-source' },
|
||||
{ title: 'SupaSquad', url: 'https://supabase.com/supasquad' },
|
||||
|
||||
@@ -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,17 @@
|
||||
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(() => {
|
||||
const elem = listeningElem ?? window
|
||||
elem.addEventListener(event, rerender)
|
||||
return () => elem.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>
|
||||
|
||||
@@ -6,141 +6,142 @@ import { type CSSProperties, type PropsWithChildren, memo, useEffect } from 'rea
|
||||
import { cn } from 'ui'
|
||||
|
||||
import Footer from '~/components/Navigation/Footer'
|
||||
import HomeMenuIconPicker from '~/components/Navigation/NavigationMenu/HomeMenuIconPicker'
|
||||
import NavigationMenu, { type MenuId } from '~/components/Navigation/NavigationMenu/NavigationMenu'
|
||||
import TopNavBar from '~/components/Navigation/NavigationMenu/TopNavBar'
|
||||
import { menuState, useMenuMobileOpen } from '~/hooks/useMenuState'
|
||||
|
||||
const levelsData = {
|
||||
home: {
|
||||
icon: '/docs/img/icons/menu/home',
|
||||
icon: 'home',
|
||||
name: 'Home',
|
||||
},
|
||||
gettingstarted: {
|
||||
icon: '/docs/img/icons/menu/getting-started',
|
||||
icon: 'getting-started',
|
||||
name: 'Getting Started',
|
||||
},
|
||||
database: {
|
||||
icon: '/docs/img/icons/menu/database',
|
||||
icon: 'database',
|
||||
name: 'Database',
|
||||
},
|
||||
api: {
|
||||
icon: '/docs/img/icons/menu/rest',
|
||||
icon: 'rest',
|
||||
name: 'REST API',
|
||||
},
|
||||
graphql: {
|
||||
icon: '/docs/img/icons/menu/graphql',
|
||||
icon: 'graphql',
|
||||
name: 'GraphQL',
|
||||
},
|
||||
auth: {
|
||||
icon: '/docs/img/icons/menu/auth',
|
||||
icon: 'auth',
|
||||
name: 'Auth',
|
||||
},
|
||||
functions: {
|
||||
icon: '/docs/img/icons/menu/functions',
|
||||
icon: 'edge-functions',
|
||||
name: 'Edge Functions',
|
||||
},
|
||||
realtime: {
|
||||
icon: '/docs/img/icons/menu/realtime',
|
||||
icon: 'realtime',
|
||||
name: 'Realtime',
|
||||
},
|
||||
analytics: {
|
||||
icon: '/docs/img/icons/menu/analytics',
|
||||
icon: 'analytics',
|
||||
name: 'Analytics',
|
||||
},
|
||||
storage: {
|
||||
icon: '/docs/img/icons/menu/storage',
|
||||
icon: 'storage',
|
||||
name: 'Storage',
|
||||
},
|
||||
ai: {
|
||||
icon: '/docs/img/icons/menu/ai',
|
||||
icon: 'ai',
|
||||
name: 'AI & Vectors',
|
||||
},
|
||||
supabase_cli: {
|
||||
icon: '/docs/img/icons/menu/reference-cli',
|
||||
icon: 'reference-cli',
|
||||
name: 'Supabase CLI',
|
||||
},
|
||||
platform: {
|
||||
icon: '/docs/img/icons/menu/platform',
|
||||
icon: 'platform',
|
||||
name: 'Platform',
|
||||
},
|
||||
resources: {
|
||||
icon: '/docs/img/icons/menu/resources',
|
||||
icon: 'resources',
|
||||
name: 'Resources',
|
||||
},
|
||||
self_hosting: {
|
||||
icon: '/docs/img/icons/menu/self-hosting',
|
||||
icon: 'self-hosting',
|
||||
name: 'Self-Hosting',
|
||||
},
|
||||
integrations: {
|
||||
icon: '/docs/img/icons/menu/integrations',
|
||||
icon: 'integrations',
|
||||
name: 'Integrations',
|
||||
},
|
||||
reference_javascript_v1: {
|
||||
icon: '/docs/img/icons/menu/reference-javascript',
|
||||
icon: 'reference-javascript',
|
||||
name: 'Javascript Reference v1.0',
|
||||
},
|
||||
reference_javascript_v2: {
|
||||
icon: '/docs/img/icons/menu/reference-javascript',
|
||||
icon: 'reference-javascript',
|
||||
name: 'Javascript Reference v2.0',
|
||||
},
|
||||
reference_dart_v1: {
|
||||
icon: '/docs/img/icons/menu/reference-dart',
|
||||
icon: 'reference-dart',
|
||||
name: 'Dart Reference v1.0',
|
||||
},
|
||||
reference_dart_v2: {
|
||||
icon: '/docs/img/icons/menu/reference-dart',
|
||||
icon: 'reference-dart',
|
||||
name: 'Dart Reference v2.0',
|
||||
},
|
||||
reference_csharp_v0: {
|
||||
icon: '/docs/img/icons/menu/reference-csharp',
|
||||
icon: 'reference-csharp',
|
||||
name: 'C# Reference v0.0',
|
||||
},
|
||||
reference_python_v2: {
|
||||
icon: '/docs/img/icons/menu/reference-python',
|
||||
icon: 'reference-python',
|
||||
name: 'Python Reference v2.0',
|
||||
},
|
||||
reference_swift_v1: {
|
||||
icon: '/docs/img/icons/menu/reference-swift',
|
||||
icon: 'reference-swift',
|
||||
name: 'Swift Reference v1.0',
|
||||
},
|
||||
reference_swift_v2: {
|
||||
icon: '/docs/img/icons/menu/reference-swift',
|
||||
icon: 'reference-swift',
|
||||
name: 'Swift Reference v2.0',
|
||||
},
|
||||
reference_kotlin_v1: {
|
||||
icon: '/docs/img/icons/menu/reference-kotlin',
|
||||
icon: 'reference-kotlin',
|
||||
name: 'Kotlin Reference v1.0',
|
||||
},
|
||||
reference_kotlin_v2: {
|
||||
icon: '/docs/img/icons/menu/reference-kotlin',
|
||||
icon: 'reference-kotlin',
|
||||
name: 'Kotlin Reference v2.0',
|
||||
},
|
||||
reference_cli: {
|
||||
icon: '/docs/img/icons/menu/reference-cli',
|
||||
icon: 'reference-cli',
|
||||
name: 'CLI Reference',
|
||||
},
|
||||
reference_api: {
|
||||
icon: '/docs/img/icons/menu/reference-api',
|
||||
icon: 'reference-api',
|
||||
name: 'Management API Reference',
|
||||
},
|
||||
reference_self_hosting_auth: {
|
||||
icon: '/docs/img/icons/menu/reference-auth',
|
||||
icon: 'reference-auth',
|
||||
name: 'Auth Server Reference',
|
||||
},
|
||||
reference_self_hosting_storage: {
|
||||
icon: '/docs/img/icons/menu/reference-storage',
|
||||
icon: 'reference-storage',
|
||||
name: 'Storage Server Reference',
|
||||
},
|
||||
reference_self_hosting_realtime: {
|
||||
icon: '/docs/img/icons/menu/reference-realtime',
|
||||
icon: 'reference-realtime',
|
||||
name: 'Realtime Server Reference',
|
||||
},
|
||||
reference_self_hosting_analytics: {
|
||||
icon: '/docs/img/icons/menu/reference-analytics',
|
||||
icon: 'reference-analytics',
|
||||
name: 'Analytics Server Reference',
|
||||
},
|
||||
reference_self_hosting_functions: {
|
||||
icon: '/docs/img/icons/menu/reference-functions',
|
||||
icon: 'reference-functions',
|
||||
name: 'Functions Server Reference',
|
||||
},
|
||||
}
|
||||
@@ -187,8 +188,8 @@ const MobileHeader = memo(function MobileHeader({ menuId }: { menuId: MenuId })
|
||||
</div>
|
||||
</button>
|
||||
<div className={[].join(' ')}>
|
||||
<img
|
||||
src={menuLevel ? levelsData[menuLevel].icon + '.svg' : levelsData['home'].icon + '.svg'}
|
||||
<HomeMenuIconPicker
|
||||
icon={menuLevel ? levelsData[menuLevel].icon : 'home'}
|
||||
className={[
|
||||
'transition-all duration-200',
|
||||
mobileMenuOpen ? 'invisible w-0 h-0' : 'w-4 h-4',
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -134,12 +134,20 @@ const StickyHeader: FC<StickyHeader> = ({ icon, ...props }) => {
|
||||
}
|
||||
|
||||
const Details: FC<PropsWithChildren<ISectionDetails>> = (props) => {
|
||||
return <div className="relative w-full">{props.children}</div>
|
||||
/**
|
||||
* `min-w` is necessary because these are used as grid children, which have
|
||||
* default `min-w-auto`
|
||||
*/
|
||||
return <div className="relative w-full min-w-full">{props.children}</div>
|
||||
}
|
||||
|
||||
const Examples: FC<PropsWithChildren<ISectionExamples>> = (props) => {
|
||||
/**
|
||||
* `min-w` is necessary because these are used as grid children, which have
|
||||
* default `min-w-auto`
|
||||
*/
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="w-full min-w-full">
|
||||
<div className="sticky top-24">{props.children}</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -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
|
||||
@@ -4,17 +4,19 @@ import { type ResponseError } from '~/types/fetch'
|
||||
import { post } from './fetchWrappers'
|
||||
|
||||
type SendFeedbackVariables = {
|
||||
title: string
|
||||
message: string
|
||||
isHelpful: boolean
|
||||
pathname?: string
|
||||
}
|
||||
|
||||
export async function sendFeedback({ message, pathname }: SendFeedbackVariables) {
|
||||
const { data, error } = await post('/platform/feedback/send', {
|
||||
export async function sendFeedback({ message, pathname, title, isHelpful }: SendFeedbackVariables) {
|
||||
const { data, error } = await post('/platform/feedback/docs', {
|
||||
body: {
|
||||
message,
|
||||
category: 'Feedback',
|
||||
tags: ['docs-feedback'],
|
||||
pathname,
|
||||
page: pathname,
|
||||
isHelpful,
|
||||
title,
|
||||
feedback: message,
|
||||
},
|
||||
})
|
||||
if (error) throw Error(`Couldn't send feedback`, { cause: error })
|
||||
|
||||
@@ -22,15 +22,15 @@
|
||||
"@code-hike/mdx": "^0.9.0",
|
||||
"@mdx-js/loader": "^2.1.5",
|
||||
"@mdx-js/react": "^2.3.0",
|
||||
"@next/bundle-analyzer": "^13.5.6",
|
||||
"@next/mdx": "^13.5.6",
|
||||
"@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",
|
||||
"@supabase/auth-helpers-react": "^0.4.2",
|
||||
"@supabase/supabase-js": "^2.39.3",
|
||||
"@supabase/supabase-js": "^2.41.1",
|
||||
"@tailwindcss/container-queries": "^0.1.1",
|
||||
"@tanstack/react-query": "^5.13.4",
|
||||
"common": "*",
|
||||
@@ -54,11 +54,11 @@
|
||||
"mdx-mermaid": "2.0.0-rc3",
|
||||
"mermaid": "^10.0.2",
|
||||
"micromark-extension-mdxjs": "^1.0.0",
|
||||
"next": "^13.4.8",
|
||||
"next": "^14.1.4",
|
||||
"next-mdx-remote": "^4.4.1",
|
||||
"next-plugin-yaml": "^1.0.1",
|
||||
"next-seo": "^6.5.0",
|
||||
"next-themes": "^0.2.1",
|
||||
"next-themes": "^0.3.0",
|
||||
"openai": "^4.20.1",
|
||||
"openapi-fetch": "^0.8.2",
|
||||
"react": "^18.2.0",
|
||||
|
||||
@@ -11,7 +11,7 @@ import { AuthProvider, ThemeProvider, useTelemetryProps, useThemeSandbox } from
|
||||
import Head from 'next/head'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useCallback, useEffect, useState, type PropsWithChildren } from 'react'
|
||||
import { PortalToast, TabsProvider } from 'ui'
|
||||
import { PortalToast, PromoToast, TabsProvider } from 'ui'
|
||||
import { CommandMenuProvider } from 'ui-patterns/Cmdk'
|
||||
import { useConsent } from 'ui-patterns/ConsentToast'
|
||||
|
||||
@@ -229,6 +229,7 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) {
|
||||
<div className="h-screen flex flex-col">
|
||||
<SiteLayout>
|
||||
<PortalToast />
|
||||
<PromoToast />
|
||||
<Component {...pageProps} />
|
||||
</SiteLayout>
|
||||
</div>
|
||||
|
||||
@@ -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 Database 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've resolved issues.
|
||||
`.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
|
||||
@@ -35,11 +35,11 @@ const pageMap = [
|
||||
remoteFile: 'airtable.md',
|
||||
},
|
||||
{
|
||||
slug: 's3',
|
||||
slug: 'auth0',
|
||||
meta: {
|
||||
title: 'AWS S3',
|
||||
title: 'Auth0',
|
||||
},
|
||||
remoteFile: 's3.md',
|
||||
remoteFile: 'auth0.md',
|
||||
},
|
||||
{
|
||||
slug: 'bigquery',
|
||||
@@ -55,6 +55,13 @@ const pageMap = [
|
||||
},
|
||||
remoteFile: 'clickhouse.md',
|
||||
},
|
||||
{
|
||||
slug: 'cognito',
|
||||
meta: {
|
||||
title: 'AWS Cognito',
|
||||
},
|
||||
remoteFile: 'cognito.md',
|
||||
},
|
||||
{
|
||||
slug: 'firebase',
|
||||
meta: {
|
||||
@@ -69,6 +76,27 @@ const pageMap = [
|
||||
},
|
||||
remoteFile: 'logflare.md',
|
||||
},
|
||||
{
|
||||
slug: 'mssql',
|
||||
meta: {
|
||||
title: 'MSSQL',
|
||||
},
|
||||
remoteFile: 'mssql.md',
|
||||
},
|
||||
{
|
||||
slug: 'redis',
|
||||
meta: {
|
||||
title: 'Redis',
|
||||
},
|
||||
remoteFile: 'redis.md',
|
||||
},
|
||||
{
|
||||
slug: 's3',
|
||||
meta: {
|
||||
title: 'AWS S3',
|
||||
},
|
||||
remoteFile: 's3.md',
|
||||
},
|
||||
{
|
||||
slug: 'stripe',
|
||||
meta: {
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
import fs from 'fs'
|
||||
import matter from 'gray-matter'
|
||||
import components from '~/components/index'
|
||||
import { MDXRemote } from 'next-mdx-remote'
|
||||
import { serialize } from 'next-mdx-remote/serialize'
|
||||
|
||||
import specFile from '~/spec/realtime_v0_config.yaml' assert { type: 'yml' }
|
||||
import { Parameter } from '~/lib/refGenerator/refTypes'
|
||||
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
|
||||
// Parameters are grouped on the page by tag
|
||||
const TAGS = ['general', 'database']
|
||||
|
||||
export default function Config(props) {
|
||||
return (
|
||||
<div>
|
||||
<div className="flex my-16">
|
||||
<div className="w-full prose">
|
||||
<h1 className="text-4xl mb-16">{specFile.info.title} Configuration</h1>
|
||||
<ReactMarkdown>{specFile.info.description}</ReactMarkdown>
|
||||
<div>
|
||||
<div>
|
||||
{props.docs
|
||||
.filter((doc) => doc.introPage)
|
||||
.map((item) => (
|
||||
<MDXRemote {...item.content} components={components} />
|
||||
))}
|
||||
</div>
|
||||
{TAGS.map((tag) =>
|
||||
specFile.parameters
|
||||
.filter((param: Parameter) => param.tags[0] === tag)
|
||||
.map((parameter: Parameter, index) => (
|
||||
<div>
|
||||
{index === 0 && <h2 className="text-xl capitalize">{tag} Settings</h2>}
|
||||
<div className="mt-8">
|
||||
<div>
|
||||
<h2 className="text-xl font-medium text-foreground font-mono">
|
||||
<span className="mr-2">$</span>
|
||||
{parameter.title}
|
||||
</h2>
|
||||
<div className="grid" id={parameter.id}>
|
||||
<div className="border-b pb-8" key={parameter.id}>
|
||||
<div className=" mb-16">
|
||||
<p className="mb-4 scroll-mt-16 mt-0 text-foreground-light text-base">
|
||||
<ReactMarkdown>{parameter.description}</ReactMarkdown>
|
||||
</p>
|
||||
<div className="grid gap-2">
|
||||
<div className="flex gap-2">
|
||||
Required: <code>{parameter.required.toString()}</code>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
Default:
|
||||
<code>
|
||||
{parameter.default ? parameter.default.toString() : 'None'}
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{parameter.links &&
|
||||
parameter.links.map((link) => (
|
||||
<div>
|
||||
<h3>See also:</h3>
|
||||
<li>
|
||||
<a href={link.link}>{link.name}</a>
|
||||
</li>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export async function getServerSideProps() {
|
||||
// an array of ids of the intro sections for this library
|
||||
const introPages = ['realtime']
|
||||
|
||||
const pages = [...introPages]
|
||||
|
||||
// Grab custom markdown intro page files
|
||||
const allMarkdownDocs = await Promise.all(
|
||||
pages.map(async (x: any, i) => {
|
||||
const pathName = `docs/ref/realtime/${x}.mdx`
|
||||
|
||||
const markdownExists = fs.existsSync(pathName)
|
||||
|
||||
const fileContents = markdownExists ? fs.readFileSync(pathName, 'utf8') : ''
|
||||
const { data, content } = matter(fileContents)
|
||||
|
||||
return {
|
||||
id: x,
|
||||
title: x,
|
||||
// ...content,
|
||||
meta: data,
|
||||
introPage: introPages.includes(x),
|
||||
content: content ? await serialize(content || '') : null,
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
return {
|
||||
props: {
|
||||
docs: allMarkdownDocs,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,167 +0,0 @@
|
||||
import { CodeBlock, Tabs } from 'ui'
|
||||
import specFile from '~/spec/transforms/storage_v0_openapi_deparsed.json' assert { type: 'json' }
|
||||
import { gen_v3 } from '~/lib/refGenerator/helpers'
|
||||
|
||||
import RefSubLayout from '~/layouts/ref/RefSubLayout'
|
||||
import React from 'react'
|
||||
|
||||
export type AcceptedValue = {
|
||||
id: string
|
||||
name: string
|
||||
type: 'string' | 'boolean' | 'object'
|
||||
description?: string
|
||||
}
|
||||
|
||||
export type Flag = {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
default_value: string
|
||||
accepted_values: AcceptedValue[]
|
||||
}
|
||||
|
||||
export type ApiParameter = {
|
||||
example: string
|
||||
in: string
|
||||
name: string
|
||||
required: boolean
|
||||
schema: {
|
||||
type: string
|
||||
example: string
|
||||
}
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
const generatedSpec = gen_v3(specFile, 'wat', { apiUrl: 'apiv0' })
|
||||
|
||||
export default function Config() {
|
||||
return (
|
||||
<RefSubLayout>
|
||||
<div className="flex my-16">
|
||||
<div className="w-full">
|
||||
<div className="grid gap-16">
|
||||
<h1 className="text-4xl">{generatedSpec.info.title}</h1>
|
||||
<p>{generatedSpec.info.description}</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-32 mx-auto max-w-5xl mt-24">
|
||||
{generatedSpec.sections.map((section, i) => (
|
||||
<React.Fragment key={i}>
|
||||
<h2 className="text-3xl">{section.title}</h2>
|
||||
{section.operations.map((operation: any, index) => (
|
||||
<div key={index} className="border-b pb-8">
|
||||
<RefSubLayout.Section
|
||||
slug={operation.id}
|
||||
title={operation.summary}
|
||||
id={operation.operationId}
|
||||
monoFont={false}
|
||||
>
|
||||
<RefSubLayout.Details>
|
||||
<div className="mt-4">
|
||||
<code className="text-md flex gap-4 text-md text-foreground-lighter break-all">
|
||||
<span className="uppercase">{operation.operation}</span>
|
||||
{operation.fullPath}
|
||||
</code>
|
||||
</div>
|
||||
{/* Path Parameters */}
|
||||
{operation.parameters &&
|
||||
operation.parameters.filter((parameter) => parameter.in === 'path')
|
||||
.length > 0 && (
|
||||
<div className="mt-12">
|
||||
<h2 className="border-b pb-2 text-xl">Path parameters</h2>
|
||||
<ul className="mt-4">
|
||||
{operation.parameters &&
|
||||
operation.parameters
|
||||
.filter((parameter: any) => parameter.in === 'path')
|
||||
.map((parameter: any, indx: number) => (
|
||||
<li key={indx} className="mt-8 border-b pb-6">
|
||||
<div className="flex gap-4 items-center">
|
||||
<span className="font-bold">{parameter.name}</span>
|
||||
<span className="font-mono text-xs break-all">
|
||||
{parameter.required && (
|
||||
<div className="text-[10px] border border-amber-700 bg-amber-300 text-amber-900 px-2 tracking-wide font-mono py-0.25 rounded-full">
|
||||
REQUIRED
|
||||
</div>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-4 flex gap-4 items-center">
|
||||
<span>Example:</span>
|
||||
<span className="font-mono text-xs break-all">
|
||||
{parameter.example}
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Header Parameters */}
|
||||
{operation.parameters &&
|
||||
operation.parameters.filter((parameter) => parameter.in === 'header')
|
||||
.length > 0 && (
|
||||
<div className="mt-12">
|
||||
<h2 className="border-b pb-2 text-xl">Header parameters</h2>
|
||||
<ul className="mt-4">
|
||||
{operation.parameters &&
|
||||
operation.parameters
|
||||
.filter((parameter: any) => parameter.in === 'header')
|
||||
.map((parameter: any, i: number) => (
|
||||
<li key={i} className="mt-8 border-b pb-6">
|
||||
<div className="flex gap-4 items-center">
|
||||
<span className="font-bold">{parameter.name}</span>
|
||||
<span className="font-mono text-xs break-all">
|
||||
{parameter.required && 'required'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-4 flex gap-4 items-center">
|
||||
<span>Example:</span>
|
||||
<span className="font-mono text-xs break-all">
|
||||
{parameter.example}
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</RefSubLayout.Details>
|
||||
<RefSubLayout.Examples>
|
||||
<h2 className="text-xl">Responses</h2>
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
type="underlined"
|
||||
defaultActiveId={operation.responseList[0].responseCode}
|
||||
queryGroup="response-status"
|
||||
>
|
||||
{operation.responseList.map((response: any, i: number) => (
|
||||
<Tabs.Panel
|
||||
key={i}
|
||||
id={response.responseCode}
|
||||
label={response.responseCode}
|
||||
>
|
||||
<p>{response.description}</p>
|
||||
{response?.content && response?.content['application/json'] && (
|
||||
<div className="mt-8">
|
||||
<CodeBlock language="bash" className="relative">
|
||||
{JSON.stringify(response.content['application/json'], null, 2)}
|
||||
</CodeBlock>
|
||||
</div>
|
||||
)}
|
||||
</Tabs.Panel>
|
||||
))}
|
||||
</Tabs>
|
||||
</RefSubLayout.Examples>
|
||||
</RefSubLayout.Section>
|
||||
</div>
|
||||
))}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</RefSubLayout>
|
||||
)
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
import specFile from '~/spec/storage_v0_config.yaml' assert { type: 'yml' }
|
||||
import { Parameter } from '~/lib/refGenerator/refTypes'
|
||||
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
|
||||
// Parameters are grouped on the page by tag
|
||||
const TAGS = ['general', 'multitenant']
|
||||
// console.log(specFile)
|
||||
export default function Config() {
|
||||
return (
|
||||
<div>
|
||||
<div className="flex my-16">
|
||||
<div className="w-full prose">
|
||||
<h1 className="text-4xl mb-16">{specFile.info.title} Configuration</h1>
|
||||
<ReactMarkdown>{specFile.info.description}</ReactMarkdown>
|
||||
<div>
|
||||
{TAGS.map((tag) =>
|
||||
specFile.parameters
|
||||
.filter((param: Parameter) => param.tags[0] === tag)
|
||||
.map((parameter: Parameter, index) => (
|
||||
<div>
|
||||
{index === 0 && <h2 className="text-xl capitalize">{tag} Settings</h2>}
|
||||
<div className="mt-8">
|
||||
<div>
|
||||
<h2 className="text-xl font-medium text-foreground font-mono">
|
||||
<span className="mr-2">$</span>
|
||||
{parameter.title}
|
||||
</h2>
|
||||
<div className="grid" id={parameter.id}>
|
||||
<div className="border-b pb-8" key={parameter.id}>
|
||||
<div className=" mb-16">
|
||||
<p className="mb-4 scroll-mt-16 mt-0 text-foreground-light text-base">
|
||||
<ReactMarkdown>{parameter.description}</ReactMarkdown>
|
||||
</p>
|
||||
<div className="grid gap-2">
|
||||
<div className="flex gap-2">
|
||||
Required: <code>{parameter.required.toString()}</code>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
Default:
|
||||
<code>
|
||||
{parameter.default ? parameter.default.toString() : 'None'}
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{parameter.links &&
|
||||
parameter.links.map((link) => (
|
||||
<div>
|
||||
<h3>See also:</h3>
|
||||
<li>
|
||||
<a href={link.link}>{link.name}</a>
|
||||
</li>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,2 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<browserconfig><msapplication><tile><square70x70logo src="/ms-icon-70x70.png"/><square150x150logo src="/ms-icon-150x150.png"/><square310x310logo src="/ms-icon-310x310.png"/><TileColor>#ffffff</TileColor></tile></msapplication></browserconfig>
|
||||
<browserconfig><msapplication><tile><square70x70logo src="/favicon/ms-icon-70x70.png"/><square150x150logo src="/favicon/ms-icon-150x150.png"/><square310x310logo src="/favicon/ms-icon-310x310.png"/><TileColor>#ffffff</TileColor></tile></msapplication></browserconfig>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 2.4 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 4.7 KiB |
@@ -7,37 +7,37 @@
|
||||
"background_color": "#1C1C1C",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/android-icon-36x36.png",
|
||||
"src": "/favicon/android-icon-36x36.png",
|
||||
"sizes": "36x36",
|
||||
"type": "image/png",
|
||||
"density": "0.75"
|
||||
},
|
||||
{
|
||||
"src": "/android-icon-48x48.png",
|
||||
"src": "/favicon/android-icon-48x48.png",
|
||||
"sizes": "48x48",
|
||||
"type": "image/png",
|
||||
"density": "1.0"
|
||||
},
|
||||
{
|
||||
"src": "/android-icon-72x72.png",
|
||||
"src": "/favicon/android-icon-72x72.png",
|
||||
"sizes": "72x72",
|
||||
"type": "image/png",
|
||||
"density": "1.5"
|
||||
},
|
||||
{
|
||||
"src": "/android-icon-96x96.png",
|
||||
"src": "/favicon/android-icon-96x96.png",
|
||||
"sizes": "96x96",
|
||||
"type": "image/png",
|
||||
"density": "2.0"
|
||||
},
|
||||
{
|
||||
"src": "/android-icon-144x144.png",
|
||||
"src": "/favicon/android-icon-144x144.png",
|
||||
"sizes": "144x144",
|
||||
"type": "image/png",
|
||||
"density": "3.0"
|
||||
},
|
||||
{
|
||||
"src": "/android-icon-192x192.png",
|
||||
"src": "/favicon/android-icon-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"density": "4.0"
|
||||
|
||||
@@ -1346,7 +1346,7 @@ functions:
|
||||
notes: |
|
||||
- Sends an invite link to the user's email address.
|
||||
- The `inviteUserByEmail()` method is typically used by administrators to invite users to join the application.
|
||||
- Note that PKCE is not supported when using `inviteUserByEmail`. This is because the browser initiating the invite is often different from the browser acecpting the invite which makes it difficult to provide the security guarantees required of the PKCE flow.
|
||||
- Note that PKCE is not supported when using `inviteUserByEmail`. This is because the browser initiating the invite is often different from the browser accepting the invite which makes it difficult to provide the security guarantees required of the PKCE flow.
|
||||
examples:
|
||||
- id: invite-a-user
|
||||
name: Invite a user
|
||||
|
||||
@@ -2903,6 +2903,37 @@ functions:
|
||||
password = "example-password"
|
||||
}
|
||||
```
|
||||
- id: sign-in-anonymously
|
||||
title: 'signInAnonymously()'
|
||||
notes: |
|
||||
- Creates an anonymous user.
|
||||
- The user can be retrieved by calling `supabase.auth.currentUserOrNull()`.
|
||||
- It is recommended to set up captcha for anonymous sign-ins to prevent abuse. You can pass in the captcha token in the `options` param.
|
||||
params:
|
||||
- name: captchaToken
|
||||
isOptional: true
|
||||
type: String?
|
||||
description: The captcha token when having captcha enabled.
|
||||
- name: data
|
||||
isOptional: true
|
||||
type: JsonObject? or T
|
||||
description: Extra user data to pass in.
|
||||
examples:
|
||||
- id: sign-in-anonymously
|
||||
name: Create an anonymous user
|
||||
isSpotlight: true
|
||||
code: |
|
||||
```kotlin
|
||||
supabase.auth.signInAnonymously(captchaToken = "token")
|
||||
```
|
||||
- id: sign-in-anonymously-with-user-metadata
|
||||
name: Create an anonymous user with custom user metadata
|
||||
isSpotlight: false
|
||||
code: |
|
||||
```kotlin
|
||||
supabase.auth.signInAnonymously(data = customUserData)
|
||||
```
|
||||
|
||||
- id: sign-in-with-password
|
||||
title: 'signInWith()'
|
||||
$ref: '@supabase/gotrue-js.GoTrueClient.signInWithPassword'
|
||||
|
||||
@@ -40,7 +40,8 @@ functions:
|
||||
code: |
|
||||
```
|
||||
import os
|
||||
from supabase import create_client, Client, ClientOptions
|
||||
from supabase import create_client, Client
|
||||
from supabase.client import ClientOptions
|
||||
|
||||
url: str = os.environ.get("SUPABASE_URL")
|
||||
key: str = os.environ.get("SUPABASE_KEY")
|
||||
|
||||
@@ -129,6 +129,29 @@ functions:
|
||||
redirectTo: URL(string: "https://example.com/welcome")!
|
||||
)
|
||||
```
|
||||
- id: sign-in-anonymously
|
||||
title: "signInAnonymously()"
|
||||
notes: |
|
||||
- Returns an anonymous user
|
||||
- It is recommended to set up captcha for anonymous sign-ins to prevent abuse. You can pass in the captcha token in the `options` param.
|
||||
examples:
|
||||
- id: sign-in-anonymously
|
||||
name: Create an anonymous user
|
||||
isSpotlight: true
|
||||
code: |
|
||||
```swift
|
||||
let session = try await supabase.auth.signInAnonymously(captchaToken: captchaToken)
|
||||
```
|
||||
- id: sign-in-anonymously-with-user-metadata
|
||||
name: Create an anonymous user with custom user metadata
|
||||
isSpotlight: false
|
||||
code: |
|
||||
```swift
|
||||
let session = try await supabase.auth.signInAnonymously(
|
||||
data: customData,
|
||||
captchaToken: captchaToken
|
||||
)
|
||||
```
|
||||
- id: sign-in-with-password
|
||||
title: "signInWithPassword()"
|
||||
notes: |
|
||||
@@ -376,7 +399,7 @@ functions:
|
||||
)
|
||||
```
|
||||
- id: get-user-identities
|
||||
title: 'userIdentities()'
|
||||
title: "userIdentities()"
|
||||
notes: |
|
||||
- The user needs to be signed in to call `userIdentities()`.
|
||||
examples:
|
||||
@@ -388,7 +411,7 @@ functions:
|
||||
let identities = try await supabase.auth.userIdentities()
|
||||
```
|
||||
- id: send-password-reauthentication
|
||||
title: 'reauthenticate()'
|
||||
title: "reauthenticate()"
|
||||
notes: |
|
||||
- This method is used together with `update(user:)` when a user's password needs to be updated.
|
||||
- If you require your user to reauthenticate before updating their password, you need to enable the **Secure password change** option in your [project's email provider settings](/dashboard/project/_/auth/providers).
|
||||
|
||||
Vendored
+286
-61
@@ -41,6 +41,14 @@ export interface paths {
|
||||
/** Send exit survey to HubSpot */
|
||||
post: operations['SendExitSurveyController_sendExitSurvey']
|
||||
}
|
||||
'/platform/feedback/upgrade': {
|
||||
/** Send upgrade survey to survey_responses table */
|
||||
post: operations['SendUpgradeSurveyController_sendUpgradeSurvey']
|
||||
}
|
||||
'/platform/feedback/docs': {
|
||||
/** Send feedback on docs */
|
||||
post: operations['SendDocsFeedbackController_sendDocsFeedback']
|
||||
}
|
||||
'/platform/signup': {
|
||||
/** Sign up with email and password */
|
||||
post: operations['SignUpController_signUp']
|
||||
@@ -100,6 +108,10 @@ export interface paths {
|
||||
/** Gets project backups */
|
||||
get: operations['BackupsController_getBackups']
|
||||
}
|
||||
'/platform/database/{ref}/backups/enable-physical-backups': {
|
||||
/** Enable usage of physical backups */
|
||||
post: operations['BackupsController_enablePhysicalBackup']
|
||||
}
|
||||
'/platform/database/{ref}/backups/download': {
|
||||
/** Download project backup */
|
||||
post: operations['BackupsController_downloadBackup']
|
||||
@@ -822,14 +834,6 @@ export interface paths {
|
||||
/** Get GitHub connection branch */
|
||||
get: operations['GitHubBranchesController_getConnectionBranch']
|
||||
}
|
||||
'/platform/integrations/github/pull-requests/{connectionId}': {
|
||||
/** List GitHub connection pull requests */
|
||||
get: operations['GitHubPullRequestsController_getConnectionPullRequests']
|
||||
}
|
||||
'/platform/integrations/github/pull-requests/{connectionId}/{branchName}': {
|
||||
/** List GitHub pull requests for a specific branch */
|
||||
get: operations['GitHubPullRequestsController_validateConnectionBranch']
|
||||
}
|
||||
'/platform/integrations/github/repositories': {
|
||||
/** Gets GitHub repositories for user */
|
||||
get: operations['GitHubRepositoriesController_listRepositories']
|
||||
@@ -1020,6 +1024,10 @@ export interface paths {
|
||||
/** Gets project backups */
|
||||
get: operations['BackupsController_getBackups']
|
||||
}
|
||||
'/v0/database/{ref}/backups/enable-physical-backups': {
|
||||
/** Enable usage of physical backups */
|
||||
post: operations['BackupsController_enablePhysicalBackup']
|
||||
}
|
||||
'/v0/database/{ref}/backups/download': {
|
||||
/** Download project backup */
|
||||
post: operations['BackupsController_downloadBackup']
|
||||
@@ -1468,6 +1476,13 @@ export interface paths {
|
||||
*/
|
||||
patch: operations['BranchController_updateBranch']
|
||||
}
|
||||
'/v1/branches/{branch_id}/reset': {
|
||||
/**
|
||||
* Resets a database branch
|
||||
* @description Resets the specified database branch
|
||||
*/
|
||||
post: operations['BranchController_resetBranch']
|
||||
}
|
||||
'/v1/projects': {
|
||||
/**
|
||||
* List all projects
|
||||
@@ -1640,6 +1655,18 @@ export interface paths {
|
||||
/** Updates a project's auth config */
|
||||
patch: operations['V1AuthConfigController_updateV1AuthConfig']
|
||||
}
|
||||
'/v1/projects/{ref}/config/auth/third-party-auth': {
|
||||
/** Lists all third-party auth integrations */
|
||||
get: operations['ThirdPartyAuthController_listTPAForProject']
|
||||
/** Creates a new third-party auth integration */
|
||||
post: operations['ThirdPartyAuthController_createTPAForProject']
|
||||
}
|
||||
'/v1/projects/{ref}/config/auth/third-party-auth/{tpa_id}': {
|
||||
/** Get a third-party integration */
|
||||
get: operations['ThirdPartyAuthController_getTPAForProject']
|
||||
/** Removes a third-party auth integration */
|
||||
delete: operations['ThirdPartyAuthController_deleteTPAForProject']
|
||||
}
|
||||
'/v1/projects/{ref}/config/auth/sso/providers': {
|
||||
/** Lists all SSO providers */
|
||||
get: operations['ProvidersController_listAllProviders']
|
||||
@@ -1860,6 +1887,19 @@ export interface components {
|
||||
additionalFeedback?: string
|
||||
exitAction?: string
|
||||
}
|
||||
SendUpgradeSurveyBody: {
|
||||
orgSlug?: string
|
||||
prevPlan?: string
|
||||
currentPlan?: string
|
||||
reasons: string[]
|
||||
additionalFeedback?: string
|
||||
}
|
||||
SendDocsFeedbackBody: {
|
||||
page: string
|
||||
isHelpful: boolean
|
||||
title: string
|
||||
feedback: string
|
||||
}
|
||||
SignUpBody: {
|
||||
email: string
|
||||
password: string
|
||||
@@ -1899,11 +1939,13 @@ export interface components {
|
||||
MAILER_SUBJECTS_RECOVERY: string
|
||||
MAILER_SUBJECTS_EMAIL_CHANGE: string
|
||||
MAILER_SUBJECTS_MAGIC_LINK: string
|
||||
MAILER_SUBJECTS_REAUTHENTICATION: string
|
||||
MAILER_TEMPLATES_INVITE_CONTENT: string
|
||||
MAILER_TEMPLATES_CONFIRMATION_CONTENT: string
|
||||
MAILER_TEMPLATES_RECOVERY_CONTENT: string
|
||||
MAILER_TEMPLATES_EMAIL_CHANGE_CONTENT: string
|
||||
MAILER_TEMPLATES_MAGIC_LINK_CONTENT: string
|
||||
MAILER_TEMPLATES_REAUTHENTICATION_CONTENT: string
|
||||
MFA_MAX_ENROLLED_FACTORS: number
|
||||
URI_ALLOW_LIST: string
|
||||
EXTERNAL_ANONYMOUS_USERS_ENABLED: boolean
|
||||
@@ -1959,6 +2001,8 @@ export interface components {
|
||||
HOOK_PASSWORD_VERIFICATION_ATTEMPT_URI: string
|
||||
HOOK_CUSTOM_ACCESS_TOKEN_ENABLED: boolean
|
||||
HOOK_CUSTOM_ACCESS_TOKEN_URI: string
|
||||
HOOK_SEND_SMS_ENABLED: boolean
|
||||
HOOK_SEND_SMS_URI: string
|
||||
EXTERNAL_APPLE_ENABLED: boolean
|
||||
EXTERNAL_APPLE_CLIENT_ID: string
|
||||
EXTERNAL_APPLE_SECRET: string
|
||||
@@ -2042,7 +2086,9 @@ export interface components {
|
||||
MAILER_SUBJECTS_RECOVERY?: string
|
||||
MAILER_SUBJECTS_EMAIL_CHANGE?: string
|
||||
MAILER_SUBJECTS_MAGIC_LINK?: string
|
||||
MAILER_SUBJECTS_REAUTHENTICATION?: string
|
||||
MAILER_TEMPLATES_INVITE_CONTENT?: string
|
||||
MAILER_TEMPLATES_REAUTHENTICATION_CONTENT?: string
|
||||
MAILER_TEMPLATES_CONFIRMATION_CONTENT?: string
|
||||
MAILER_TEMPLATES_RECOVERY_CONTENT?: string
|
||||
MAILER_TEMPLATES_EMAIL_CHANGE_CONTENT?: string
|
||||
@@ -2107,6 +2153,8 @@ export interface components {
|
||||
HOOK_PASSWORD_VERIFICATION_ATTEMPT_URI?: string
|
||||
HOOK_CUSTOM_ACCESS_TOKEN_ENABLED?: boolean
|
||||
HOOK_CUSTOM_ACCESS_TOKEN_URI?: string
|
||||
HOOK_SEND_SMS_ENABLED?: boolean
|
||||
HOOK_SEND_SMS_URI?: string
|
||||
EXTERNAL_APPLE_ENABLED?: boolean
|
||||
EXTERNAL_APPLE_CLIENT_ID?: string
|
||||
EXTERNAL_APPLE_SECRET?: string
|
||||
@@ -2207,6 +2255,7 @@ export interface components {
|
||||
updated_at?: string
|
||||
is_sso_user?: boolean
|
||||
deleted_at?: string
|
||||
is_anonymous?: boolean
|
||||
}
|
||||
UsersResponse: {
|
||||
total: number
|
||||
@@ -2263,14 +2312,17 @@ export interface components {
|
||||
migrated_at: string | null
|
||||
}
|
||||
OrganizationResponse: {
|
||||
/** @enum {string|null} */
|
||||
restriction_status: 'grace_period' | 'grace_period_over' | 'restricted' | null
|
||||
id: number
|
||||
slug: string
|
||||
name: string
|
||||
billing_email: string
|
||||
billing_email: string | null
|
||||
is_owner: boolean
|
||||
stripe_customer_id: string
|
||||
subscription_id?: string
|
||||
stripe_customer_id: string | null
|
||||
subscription_id: string | null
|
||||
opt_in_tags: string[]
|
||||
restriction_data: Record<string, never>
|
||||
}
|
||||
GetOrganizationByFlyOrganizationIdResponse: {
|
||||
slug: string
|
||||
@@ -2788,7 +2840,7 @@ export interface components {
|
||||
payment_method_card_details?: components['schemas']['PaymentMethodCardDetails']
|
||||
billing_via_partner: boolean
|
||||
/** @enum {string} */
|
||||
billing_partner: 'fly'
|
||||
billing_partner: 'fly' | 'aws'
|
||||
scheduled_plan_change: components['schemas']['ScheduledPlanChange'] | null
|
||||
customer_balance: number
|
||||
nano_enabled: boolean
|
||||
@@ -4565,6 +4617,7 @@ export interface components {
|
||||
branch_name?: string
|
||||
git_branch?: string
|
||||
reset_on_push?: boolean
|
||||
persistent?: boolean
|
||||
}
|
||||
BranchResponse: {
|
||||
id: string
|
||||
@@ -4575,11 +4628,18 @@ export interface components {
|
||||
git_branch?: string
|
||||
pr_number?: number
|
||||
reset_on_push: boolean
|
||||
persistent: boolean
|
||||
/** @enum {string} */
|
||||
status: 'CREATING_PROJECT' | 'RUNNING_MIGRATIONS' | 'MIGRATIONS_PASSED' | 'MIGRATIONS_FAILED'
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
BranchDeleteResponse: {
|
||||
message: string
|
||||
}
|
||||
BranchResetResponse: {
|
||||
message: string
|
||||
}
|
||||
V1DatabaseResponse: {
|
||||
/** @description Database host */
|
||||
host: string
|
||||
@@ -4604,6 +4664,19 @@ export interface components {
|
||||
*/
|
||||
created_at: string
|
||||
database?: components['schemas']['V1DatabaseResponse']
|
||||
/** @enum {string} */
|
||||
status:
|
||||
| 'ACTIVE_HEALTHY'
|
||||
| 'ACTIVE_UNHEALTHY'
|
||||
| 'COMING_UP'
|
||||
| 'GOING_DOWN'
|
||||
| 'INACTIVE'
|
||||
| 'INIT_FAILED'
|
||||
| 'REMOVED'
|
||||
| 'RESTORING'
|
||||
| 'UNKNOWN'
|
||||
| 'UPGRADING'
|
||||
| 'PAUSING'
|
||||
}
|
||||
V1CreateProjectBody: {
|
||||
/** @description Database password */
|
||||
@@ -4925,11 +4998,13 @@ export interface components {
|
||||
mailer_subjects_invite: string | null
|
||||
mailer_subjects_magic_link: string | null
|
||||
mailer_subjects_recovery: string | null
|
||||
mailer_subjects_reauthentication: string | null
|
||||
mailer_templates_confirmation_content: string | null
|
||||
mailer_templates_email_change_content: string | null
|
||||
mailer_templates_invite_content: string | null
|
||||
mailer_templates_magic_link_content: string | null
|
||||
mailer_templates_recovery_content: string | null
|
||||
mailer_templates_reauthentication_content: string | null
|
||||
mfa_max_enrolled_factors: number | null
|
||||
password_hibp_enabled: boolean | null
|
||||
password_min_length: number | null
|
||||
@@ -5001,11 +5076,13 @@ export interface components {
|
||||
mailer_subjects_recovery?: string
|
||||
mailer_subjects_email_change?: string
|
||||
mailer_subjects_magic_link?: string
|
||||
mailer_subjects_reauthentication?: string
|
||||
mailer_templates_invite_content?: string
|
||||
mailer_templates_confirmation_content?: string
|
||||
mailer_templates_recovery_content?: string
|
||||
mailer_templates_email_change_content?: string
|
||||
mailer_templates_magic_link_content?: string
|
||||
mailer_templates_reauthentication_content?: string
|
||||
mfa_max_enrolled_factors?: number
|
||||
uri_allow_list?: string
|
||||
external_anonymous_users_enabled?: boolean
|
||||
@@ -5066,6 +5143,8 @@ export interface components {
|
||||
hook_password_verification_attempt_uri?: string
|
||||
hook_custom_access_token_enabled?: boolean
|
||||
hook_custom_access_token_uri?: string
|
||||
hook_custom_sms_provider_enabled?: boolean
|
||||
hook_custom_sms_provider_uri?: string
|
||||
external_apple_enabled?: boolean
|
||||
external_apple_client_id?: string
|
||||
external_apple_secret?: string
|
||||
@@ -5131,6 +5210,22 @@ export interface components {
|
||||
external_zoom_client_id?: string
|
||||
external_zoom_secret?: string
|
||||
}
|
||||
CreateThirdPartyAuthBody: {
|
||||
oidc_issuer_url?: string
|
||||
jwks_url?: string
|
||||
custom_jwks?: Record<string, never>
|
||||
}
|
||||
ThirdPartyAuth: {
|
||||
id: string
|
||||
type: string
|
||||
oidc_issuer_url?: string | null
|
||||
jwks_url?: string | null
|
||||
custom_jwks?: unknown
|
||||
resolved_jwks?: unknown
|
||||
inserted_at: string
|
||||
updated_at: string
|
||||
resolved_at?: string | null
|
||||
}
|
||||
AttributeValue: {
|
||||
default?: Record<string, never> | number | string | boolean
|
||||
name?: string
|
||||
@@ -5650,6 +5745,44 @@ export interface operations {
|
||||
}
|
||||
}
|
||||
}
|
||||
/** Send upgrade survey to survey_responses table */
|
||||
SendUpgradeSurveyController_sendUpgradeSurvey: {
|
||||
requestBody: {
|
||||
content: {
|
||||
'application/json': components['schemas']['SendUpgradeSurveyBody']
|
||||
}
|
||||
}
|
||||
responses: {
|
||||
201: {
|
||||
content: {
|
||||
'application/json': components['schemas']['SendFeedbackResponse']
|
||||
}
|
||||
}
|
||||
/** @description Failed to send upgrade survey */
|
||||
500: {
|
||||
content: never
|
||||
}
|
||||
}
|
||||
}
|
||||
/** Send feedback on docs */
|
||||
SendDocsFeedbackController_sendDocsFeedback: {
|
||||
requestBody: {
|
||||
content: {
|
||||
'application/json': components['schemas']['SendDocsFeedbackBody']
|
||||
}
|
||||
}
|
||||
responses: {
|
||||
201: {
|
||||
content: {
|
||||
'application/json': components['schemas']['SendFeedbackResponse']
|
||||
}
|
||||
}
|
||||
/** @description Failed to send feedback for docs */
|
||||
500: {
|
||||
content: never
|
||||
}
|
||||
}
|
||||
}
|
||||
/** Sign up with email and password */
|
||||
SignUpController_signUp: {
|
||||
requestBody: {
|
||||
@@ -5870,7 +6003,13 @@ export interface operations {
|
||||
parameters: {
|
||||
path: {
|
||||
ref: string
|
||||
template: 'confirmation' | 'email-change' | 'invite' | 'magic-link' | 'recovery'
|
||||
template:
|
||||
| 'confirmation'
|
||||
| 'email-change'
|
||||
| 'invite'
|
||||
| 'magic-link'
|
||||
| 'recovery'
|
||||
| 'reauthentication'
|
||||
}
|
||||
}
|
||||
responses: {
|
||||
@@ -5986,6 +6125,24 @@ export interface operations {
|
||||
}
|
||||
}
|
||||
}
|
||||
/** Enable usage of physical backups */
|
||||
BackupsController_enablePhysicalBackup: {
|
||||
parameters: {
|
||||
path: {
|
||||
/** @description Project ref */
|
||||
ref: string
|
||||
}
|
||||
}
|
||||
responses: {
|
||||
201: {
|
||||
content: never
|
||||
}
|
||||
/** @description Failed to enable usage of physical backups */
|
||||
500: {
|
||||
content: never
|
||||
}
|
||||
}
|
||||
}
|
||||
/** Download project backup */
|
||||
BackupsController_downloadBackup: {
|
||||
parameters: {
|
||||
@@ -11079,52 +11236,6 @@ export interface operations {
|
||||
}
|
||||
}
|
||||
}
|
||||
/** List GitHub connection pull requests */
|
||||
GitHubPullRequestsController_getConnectionPullRequests: {
|
||||
parameters: {
|
||||
query: {
|
||||
pr_number: number[]
|
||||
}
|
||||
path: {
|
||||
connectionId: number
|
||||
}
|
||||
}
|
||||
responses: {
|
||||
200: {
|
||||
content: {
|
||||
'application/json': Record<string, never>[]
|
||||
}
|
||||
}
|
||||
/** @description Failed to list GitHub connection pull requests */
|
||||
500: {
|
||||
content: never
|
||||
}
|
||||
}
|
||||
}
|
||||
/** List GitHub pull requests for a specific branch */
|
||||
GitHubPullRequestsController_validateConnectionBranch: {
|
||||
parameters: {
|
||||
query?: {
|
||||
per_page?: number
|
||||
page?: number
|
||||
}
|
||||
path: {
|
||||
connectionId: number
|
||||
branchName: string
|
||||
}
|
||||
}
|
||||
responses: {
|
||||
200: {
|
||||
content: {
|
||||
'application/json': Record<string, never>[]
|
||||
}
|
||||
}
|
||||
/** @description Failed to validate GitHub connection branch */
|
||||
500: {
|
||||
content: never
|
||||
}
|
||||
}
|
||||
}
|
||||
/** Gets GitHub repositories for user */
|
||||
GitHubRepositoriesController_listRepositories: {
|
||||
responses: {
|
||||
@@ -11176,7 +11287,13 @@ export interface operations {
|
||||
parameters: {
|
||||
path: {
|
||||
ref: string
|
||||
template: 'confirmation' | 'email-change' | 'invite' | 'magic-link' | 'recovery'
|
||||
template:
|
||||
| 'confirmation'
|
||||
| 'email-change'
|
||||
| 'invite'
|
||||
| 'magic-link'
|
||||
| 'recovery'
|
||||
| 'reauthentication'
|
||||
}
|
||||
}
|
||||
responses: {
|
||||
@@ -11845,7 +11962,9 @@ export interface operations {
|
||||
}
|
||||
responses: {
|
||||
200: {
|
||||
content: never
|
||||
content: {
|
||||
'application/json': components['schemas']['BranchDeleteResponse']
|
||||
}
|
||||
}
|
||||
/** @description Failed to delete database branch */
|
||||
500: {
|
||||
@@ -11881,6 +12000,29 @@ export interface operations {
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Resets a database branch
|
||||
* @description Resets the specified database branch
|
||||
*/
|
||||
BranchController_resetBranch: {
|
||||
parameters: {
|
||||
path: {
|
||||
/** @description Branch ID */
|
||||
branch_id: string
|
||||
}
|
||||
}
|
||||
responses: {
|
||||
201: {
|
||||
content: {
|
||||
'application/json': components['schemas']['BranchResetResponse']
|
||||
}
|
||||
}
|
||||
/** @description Failed to reset database branch */
|
||||
500: {
|
||||
content: never
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* List all projects
|
||||
* @description Returns a list of all projects you've previously created.
|
||||
@@ -12833,6 +12975,89 @@ export interface operations {
|
||||
}
|
||||
}
|
||||
}
|
||||
/** Lists all third-party auth integrations */
|
||||
ThirdPartyAuthController_listTPAForProject: {
|
||||
parameters: {
|
||||
path: {
|
||||
/** @description Project ref */
|
||||
ref: string
|
||||
}
|
||||
}
|
||||
responses: {
|
||||
200: {
|
||||
content: {
|
||||
'application/json': components['schemas']['ThirdPartyAuth'][]
|
||||
}
|
||||
}
|
||||
403: {
|
||||
content: never
|
||||
}
|
||||
}
|
||||
}
|
||||
/** Creates a new third-party auth integration */
|
||||
ThirdPartyAuthController_createTPAForProject: {
|
||||
parameters: {
|
||||
path: {
|
||||
/** @description Project ref */
|
||||
ref: string
|
||||
}
|
||||
}
|
||||
requestBody: {
|
||||
content: {
|
||||
'application/json': components['schemas']['CreateThirdPartyAuthBody']
|
||||
}
|
||||
}
|
||||
responses: {
|
||||
201: {
|
||||
content: {
|
||||
'application/json': components['schemas']['ThirdPartyAuth']
|
||||
}
|
||||
}
|
||||
403: {
|
||||
content: never
|
||||
}
|
||||
}
|
||||
}
|
||||
/** Get a third-party integration */
|
||||
ThirdPartyAuthController_getTPAForProject: {
|
||||
parameters: {
|
||||
path: {
|
||||
/** @description Project ref */
|
||||
ref: string
|
||||
tpa_id: string
|
||||
}
|
||||
}
|
||||
responses: {
|
||||
200: {
|
||||
content: {
|
||||
'application/json': components['schemas']['ThirdPartyAuth']
|
||||
}
|
||||
}
|
||||
403: {
|
||||
content: never
|
||||
}
|
||||
}
|
||||
}
|
||||
/** Removes a third-party auth integration */
|
||||
ThirdPartyAuthController_deleteTPAForProject: {
|
||||
parameters: {
|
||||
path: {
|
||||
/** @description Project ref */
|
||||
ref: string
|
||||
tpa_id: string
|
||||
}
|
||||
}
|
||||
responses: {
|
||||
200: {
|
||||
content: {
|
||||
'application/json': components['schemas']['ThirdPartyAuth']
|
||||
}
|
||||
}
|
||||
403: {
|
||||
content: never
|
||||
}
|
||||
}
|
||||
}
|
||||
/** Lists all SSO providers */
|
||||
ProvidersController_listAllProviders: {
|
||||
parameters: {
|
||||
|
||||
@@ -27,7 +27,7 @@ WORKDIR /app
|
||||
FROM base as turbo
|
||||
COPY . .
|
||||
|
||||
RUN npx turbo@1.30.0 prune --scope=studio --docker
|
||||
RUN npx turbo@1.13.2 prune --scope=studio --docker
|
||||
|
||||
# Install dev dependencies (only if needed)
|
||||
FROM base as deps
|
||||
|
||||
@@ -38,7 +38,7 @@ const Footer = ({ isLoading, isRefetching }: FooterProps) => {
|
||||
width={75}
|
||||
options={['definition', 'data']}
|
||||
activeOption={selectedView}
|
||||
borderOverride="border-gray-500"
|
||||
borderOverride="border"
|
||||
onClickOption={setSelectedView}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -190,34 +190,30 @@ const Pagination = ({ isLoading: isLoadingRows = false }: PaginationProps) => {
|
||||
|
||||
<ConfirmationModal
|
||||
visible={isConfirmPreviousModalOpen}
|
||||
header="Confirm moving to previous page"
|
||||
buttonLabel="Confirm"
|
||||
onSelectCancel={() => setIsConfirmPreviousModalOpen(false)}
|
||||
onSelectConfirm={() => {
|
||||
title="Confirm moving to previous page"
|
||||
confirmLabel="Confirm"
|
||||
onCancel={() => setIsConfirmPreviousModalOpen(false)}
|
||||
onConfirm={() => {
|
||||
onConfirmPreviousPage()
|
||||
}}
|
||||
>
|
||||
<Modal.Content>
|
||||
<p className="py-4 text-sm text-foreground-light">
|
||||
The currently selected lines will be deselected, do you want to proceed?
|
||||
</p>
|
||||
</Modal.Content>
|
||||
<p className="py-4 text-sm text-foreground-light">
|
||||
The currently selected lines will be deselected, do you want to proceed?
|
||||
</p>
|
||||
</ConfirmationModal>
|
||||
|
||||
<ConfirmationModal
|
||||
visible={isConfirmNextModalOpen}
|
||||
header="Confirm moving to next page"
|
||||
buttonLabel="Confirm"
|
||||
onSelectCancel={() => setIsConfirmNextModalOpen(false)}
|
||||
onSelectConfirm={() => {
|
||||
title="Confirm moving to next page"
|
||||
confirmLabel="Confirm"
|
||||
onCancel={() => setIsConfirmNextModalOpen(false)}
|
||||
onConfirm={() => {
|
||||
onConfirmNextPage()
|
||||
}}
|
||||
>
|
||||
<Modal.Content>
|
||||
<p className="py-4 text-sm text-foreground-light">
|
||||
The currently selected lines will be deselected, do you want to proceed?
|
||||
</p>
|
||||
</Modal.Content>
|
||||
<p className="py-4 text-sm text-foreground-light">
|
||||
The currently selected lines will be deselected, do you want to proceed?
|
||||
</p>
|
||||
</ConfirmationModal>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -377,7 +377,7 @@ const RowHeader = ({ table, sorts, filters }: RowHeaderProps) => {
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<div className="h-[20px] border-r border-gray-700" />
|
||||
<div className="h-[20px] border-r border-strong" />
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
type="primary"
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
IconLock,
|
||||
IconTrash,
|
||||
IconUnlock,
|
||||
Separator,
|
||||
} from 'ui'
|
||||
|
||||
import { useDispatch, useTrackedState } from '../../store'
|
||||
@@ -92,7 +93,7 @@ const ColumnMenu = ({ column, isEncrypted }: ColumnMenuProps) => {
|
||||
</DropdownMenuItem>
|
||||
{state.editable && onDeleteColumn !== undefined && (
|
||||
<>
|
||||
<Divider light />
|
||||
<Separator />
|
||||
<DropdownMenuItem className="space-x-2" onClick={onDeleteColumn}>
|
||||
<IconTrash size="tiny" stroke="red" />
|
||||
<p>Delete column</p>
|
||||
|
||||
@@ -77,20 +77,18 @@ const AccessTokenList = () => {
|
||||
</div>
|
||||
<ConfirmationModal
|
||||
visible={isOpen}
|
||||
danger={true}
|
||||
header="Confirm to delete"
|
||||
buttonLabel="Delete"
|
||||
buttonLoadingLabel="Deleting"
|
||||
onSelectCancel={() => setIsOpen(false)}
|
||||
onSelectConfirm={() => {
|
||||
variant={'destructive'}
|
||||
title="Confirm to delete"
|
||||
confirmLabel="Delete"
|
||||
confirmLabelLoading="Deleting"
|
||||
onCancel={() => setIsOpen(false)}
|
||||
onConfirm={() => {
|
||||
if (token) onDeleteToken(token.id)
|
||||
}}
|
||||
>
|
||||
<Modal.Content>
|
||||
<p className="py-4 text-sm text-foreground-light">
|
||||
{`This action cannot be undone. Are you sure you want to delete "${token?.name}" token?`}
|
||||
</p>
|
||||
</Modal.Content>
|
||||
<p className="py-4 text-sm text-foreground-light">
|
||||
{`This action cannot be undone. Are you sure you want to delete "${token?.name}" token?`}
|
||||
</p>
|
||||
</ConfirmationModal>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -3,10 +3,11 @@ import dayjs from 'dayjs'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Alert, Button, IconArrowDown, IconArrowUp, IconRefreshCw } from 'ui'
|
||||
|
||||
import { FilterPopover, LogDetailsPanel } from 'components/interfaces/AuditLogs'
|
||||
import { LogDetailsPanel } from 'components/interfaces/AuditLogs'
|
||||
import Table from 'components/to-be-cleaned/Table'
|
||||
import AlertError from 'components/ui/AlertError'
|
||||
import { DatePicker } from 'components/ui/DatePicker'
|
||||
import { FilterPopover } from 'components/ui/FilterPopover'
|
||||
import ShimmeringLoader from 'components/ui/ShimmeringLoader'
|
||||
import type { AuditLog } from 'data/organizations/organization-audit-logs-query'
|
||||
import { useOrganizationsQuery } from 'data/organizations/organizations-query'
|
||||
|
||||
@@ -64,29 +64,27 @@ const FirstStep = ({ visible, name, enroll, setName, isEnrolling, onClose }: Fir
|
||||
<ConfirmationModal
|
||||
size="medium"
|
||||
visible={visible}
|
||||
header="Add a new authenticator app as a factor"
|
||||
buttonLabel="Generate QR"
|
||||
buttonLoadingLabel="Generating QR"
|
||||
buttonDisabled={name.length === 0}
|
||||
title="Add a new authenticator app as a factor"
|
||||
confirmLabel="Generate QR"
|
||||
confirmLabelLoading="Generating QR"
|
||||
disabled={name.length === 0}
|
||||
loading={isEnrolling}
|
||||
onSelectCancel={onClose}
|
||||
onSelectConfirm={() => {
|
||||
onCancel={onClose}
|
||||
onConfirm={() => {
|
||||
enroll({
|
||||
factorType: 'totp',
|
||||
friendlyName: name,
|
||||
})
|
||||
}}
|
||||
>
|
||||
<Modal.Content>
|
||||
<div className="pt-6 pb-5">
|
||||
<Input
|
||||
label="Provide a name to identify this app"
|
||||
descriptionText="A string will be randomly generated if a name is not provided"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</Modal.Content>
|
||||
<div className="pt-6 pb-5">
|
||||
<Input
|
||||
label="Provide a name to identify this app"
|
||||
descriptionText="A string will be randomly generated if a name is not provided"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</ConfirmationModal>
|
||||
)
|
||||
}
|
||||
@@ -152,65 +150,63 @@ const SecondStep = ({
|
||||
<ConfirmationModal
|
||||
size="medium"
|
||||
visible={visible}
|
||||
header={`Verify new factor ${factorName}`}
|
||||
buttonLabel="Confirm"
|
||||
buttonLoadingLabel="Confirming"
|
||||
title={`Verify new factor ${factorName}`}
|
||||
confirmLabel="Confirm"
|
||||
confirmLabelLoading="Confirming"
|
||||
loading={isVerifying}
|
||||
onSelectCancel={() => {
|
||||
onCancel={() => {
|
||||
// If a factor has been created (but not verified), unenroll it. This will be run as a
|
||||
// side effect so that it's not confusing to the user why the modal stays open while
|
||||
// unenrolling.
|
||||
if (factor) unenroll({ factorId: factor.id })
|
||||
}}
|
||||
onSelectConfirm={() => factor && challengeAndVerify({ factorId: factor.id, code })}
|
||||
onConfirm={() => factor && challengeAndVerify({ factorId: factor.id, code })}
|
||||
>
|
||||
<Modal.Content>
|
||||
<>
|
||||
<div className="py-4 pb-0 text-sm">
|
||||
<span>
|
||||
Use an authenticator app to scan the following QR code, and provide the code from the
|
||||
app to complete the enrolment.
|
||||
</span>
|
||||
<>
|
||||
<div className="py-4 pb-0 text-sm">
|
||||
<span>
|
||||
Use an authenticator app to scan the following QR code, and provide the code from the
|
||||
app to complete the enrolment.
|
||||
</span>
|
||||
</div>
|
||||
{isLoading && (
|
||||
<div className="pb-4 px-4">
|
||||
<GenericSkeletonLoader />
|
||||
</div>
|
||||
{isLoading && (
|
||||
<div className="pb-4 px-4">
|
||||
<GenericSkeletonLoader />
|
||||
)}
|
||||
{factor && (
|
||||
<>
|
||||
<div className="flex justify-center py-6">
|
||||
<div className="h-48 w-48 bg-white rounded">
|
||||
<img width={190} height={190} src={factor.totp.qr_code} alt={factor.totp.uri} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{factor && (
|
||||
<>
|
||||
<div className="flex justify-center py-6">
|
||||
<div className="h-48 w-48 bg-white rounded">
|
||||
<img width={190} height={190} src={factor.totp.qr_code} alt={factor.totp.uri} />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<InformationBox
|
||||
title="Unable to scan?"
|
||||
description={
|
||||
<Input
|
||||
copy
|
||||
disabled
|
||||
id="ref"
|
||||
size="small"
|
||||
label="You can also enter this secret key into your authenticator app"
|
||||
value={factor.totp.secret}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="pt-2 pb-4">
|
||||
<Input
|
||||
label="Authentication code"
|
||||
value={code}
|
||||
placeholder="XXXXXX"
|
||||
onChange={(e) => setCode(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
</Modal.Content>
|
||||
<div>
|
||||
<InformationBox
|
||||
title="Unable to scan?"
|
||||
description={
|
||||
<Input
|
||||
copy
|
||||
disabled
|
||||
id="ref"
|
||||
size="small"
|
||||
label="You can also enter this secret key into your authenticator app"
|
||||
value={factor.totp.secret}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="pt-2 pb-4">
|
||||
<Input
|
||||
label="Authentication code"
|
||||
value={code}
|
||||
placeholder="XXXXXX"
|
||||
onChange={(e) => setCode(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
</ConfirmationModal>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -34,45 +34,36 @@ const DeleteFactorModal = ({
|
||||
<ConfirmationModal
|
||||
size="medium"
|
||||
visible={visible}
|
||||
danger
|
||||
header="Confirm to delete factor"
|
||||
buttonLabel="Delete"
|
||||
buttonLoadingLabel="Deleting"
|
||||
variant={'destructive'}
|
||||
title="Confirm to delete factor"
|
||||
confirmLabel="Delete"
|
||||
confirmLabelLoading="Deleting"
|
||||
loading={isLoading}
|
||||
onSelectCancel={onClose}
|
||||
onSelectConfirm={() => factorId && unenroll({ factorId })}
|
||||
onCancel={onClose}
|
||||
onConfirm={() => factorId && unenroll({ factorId })}
|
||||
alert={{
|
||||
title: lastFactorToBeDeleted
|
||||
? 'Multi-factor authentication will be disabled'
|
||||
: 'This action cannot be undone',
|
||||
description: lastFactorToBeDeleted
|
||||
? 'There are no other factors that are set up once you delete this factor, as such your account will no longer be guarded by multi-factor authentication'
|
||||
: 'You will no longer be able to use this authenticator app for multi-factor authentication when signing in to the dashboard',
|
||||
}}
|
||||
>
|
||||
<Modal.Content className="py-6">
|
||||
<Alert_Shadcn_ variant="warning">
|
||||
<IconAlertTriangle strokeWidth={2} />
|
||||
<AlertTitle_Shadcn_>
|
||||
{lastFactorToBeDeleted
|
||||
? 'Multi-factor authentication will be disabled'
|
||||
: 'This action cannot be undone'}
|
||||
</AlertTitle_Shadcn_>
|
||||
<AlertDescription_Shadcn_>
|
||||
{lastFactorToBeDeleted
|
||||
? 'There are no other factors that are set up once you delete this factor, as such your account will no longer be guarded by multi-factor authentication'
|
||||
: 'You will no longer be able to use this authenticator app for multi-factor authentication when signing in to the dashboard'}
|
||||
</AlertDescription_Shadcn_>
|
||||
</Alert_Shadcn_>
|
||||
<div className="text-sm px-1 pt-4">
|
||||
<p>Before deleting this factor, consider:</p>
|
||||
<ul className="text-foreground-light py-1 list-disc mx-4 space-y-1">
|
||||
{lastFactorToBeDeleted ? (
|
||||
<>
|
||||
<li>Adding another authenticator app as a factor prior to deleting</li>
|
||||
<li>Ensure that your account does not need multi-factor authentication</li>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<li>Your backup authenticator app is still available to use</li>
|
||||
<li>Adding another authenticator app thereafter as a backup</li>
|
||||
</>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
</Modal.Content>
|
||||
<p className="text-sm">Before deleting this factor, consider:</p>
|
||||
<ul className="text-sm text-foreground-light py-1 list-disc mx-4 space-y-1">
|
||||
{lastFactorToBeDeleted ? (
|
||||
<>
|
||||
<li>Adding another authenticator app as a factor prior to deleting</li>
|
||||
<li>Ensure that your account does not need multi-factor authentication</li>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<li>Your backup authenticator app is still available to use</li>
|
||||
<li>Adding another authenticator app thereafter as a backup</li>
|
||||
</>
|
||||
)}
|
||||
</ul>
|
||||
</ConfirmationModal>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { useMonaco } from '@monaco-editor/react'
|
||||
import { useTheme } from 'next-themes'
|
||||
import { PropsWithChildren, useEffect } from 'react'
|
||||
import { PropsWithChildren, useMemo } from 'react'
|
||||
|
||||
import IncidentBanner from 'components/layouts/AppLayout/IncidentBanner'
|
||||
import { NoticeBanner } from 'components/layouts/AppLayout/NoticeBanner'
|
||||
import { RestrictrionBanner } from 'components/layouts/AppLayout/RestrictionBanner'
|
||||
import { getTheme } from 'components/ui/CodeEditor'
|
||||
import { useFlag } from 'hooks'
|
||||
|
||||
@@ -14,7 +15,9 @@ const AppBannerWrapper = ({ children }: PropsWithChildren<{}>) => {
|
||||
const ongoingIncident = useFlag('ongoingIncident')
|
||||
const showNoticeBanner = useFlag('showNoticeBanner')
|
||||
|
||||
useEffect(() => {
|
||||
// Define the supabase theme for Monaco before anything is rendered. Using useEffect would sometime load the theme
|
||||
// after the editor was loaded, so it looked off. useMemo will always be run before rendering
|
||||
useMemo(() => {
|
||||
if (monaco && resolvedTheme) {
|
||||
const mode: any = getTheme(resolvedTheme)
|
||||
monaco.editor.defineTheme('supabase', mode)
|
||||
@@ -26,6 +29,7 @@ const AppBannerWrapper = ({ children }: PropsWithChildren<{}>) => {
|
||||
<div className="flex-none">
|
||||
{ongoingIncident && <IncidentBanner />}
|
||||
{showNoticeBanner && <NoticeBanner />}
|
||||
<RestrictrionBanner />
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
export { default as FilterPopover } from './FilterPopover'
|
||||
export { default as LogDetailsPanel } from './LogDetailsPanel'
|
||||
@@ -24,6 +24,7 @@ const CONFIRMATION: FormSchema = {
|
||||
- \`{{ .SiteURL }}\` : The URL of the site
|
||||
- \`{{ .Email }}\` : The user's email address
|
||||
- \`{{ .Data }}\` : The user's \`user_metadata\`
|
||||
- \`{{ .RedirectTo }}\` : The URL of \`emailRedirectTo\` passed in options
|
||||
`,
|
||||
},
|
||||
},
|
||||
@@ -58,6 +59,7 @@ const INVITE: FormSchema = {
|
||||
- \`{{ .SiteURL }}\` : The URL of the site
|
||||
- \`{{ .Email }}\` : The user's email address
|
||||
- \`{{ .Data }}\` : The user's \`user_metadata\`
|
||||
- \`{{ .RedirectTo }}\` : The URL of \`redirectTo\` passed in options
|
||||
`,
|
||||
},
|
||||
},
|
||||
@@ -92,6 +94,7 @@ const MAGIC_LINK: FormSchema = {
|
||||
- \`{{ .SiteURL }}\` : The URL of the site
|
||||
- \`{{ .Email }}\` : The user's email address
|
||||
- \`{{ .Data }}\` : The user's \`user_metadata\`
|
||||
- \`{{ .RedirectTo }}\` : The URL of \`emailRedirectTo\` passed in options
|
||||
`,
|
||||
},
|
||||
},
|
||||
@@ -127,6 +130,7 @@ const EMAIL_CHANGE: FormSchema = {
|
||||
- \`{{ .Email }}\` : The original user's email address
|
||||
- \`{{ .NewEmail }}\` : The user's new email address
|
||||
- \`{{ .Data }}\` : The user's \`user_metadata\`
|
||||
- \`{{ .RedirectTo }}\` : The URL of \`emailRedirectTo\` passed in options
|
||||
`,
|
||||
},
|
||||
},
|
||||
@@ -161,6 +165,7 @@ const RECOVERY: FormSchema = {
|
||||
- \`{{ .SiteURL }}\` : The URL of the site
|
||||
- \`{{ .Email }}\` : The user's email address
|
||||
- \`{{ .Data }}\` : The user's \`user_metadata\`
|
||||
- \`{{ .RedirectTo }}\` : The URL of \`redirectTo\` passed in options
|
||||
`,
|
||||
},
|
||||
},
|
||||
|
||||
+7
-2
@@ -83,10 +83,15 @@ export const PolicyTemplates = ({
|
||||
icon={
|
||||
<div className="min-w-16">
|
||||
<Badge
|
||||
className="!rounded font-mono"
|
||||
className={cn(
|
||||
'!rounded font-mono',
|
||||
template.command === 'UPDATE'
|
||||
? 'bg-blue-400 text-blue-900 border border-blue-800'
|
||||
: ''
|
||||
)}
|
||||
variant={
|
||||
template.command === 'ALL'
|
||||
? 'default'
|
||||
? 'outline'
|
||||
: template.command === 'SELECT'
|
||||
? 'brand'
|
||||
: template.command === 'UPDATE'
|
||||
|
||||
+36
-21
@@ -22,6 +22,7 @@ interface RLSCodeEditorProps {
|
||||
placeholder?: string
|
||||
readOnly?: boolean
|
||||
|
||||
disableTabToUsePlaceholder?: boolean
|
||||
lineNumberStart?: number
|
||||
onChange?: () => void
|
||||
onMount?: () => void
|
||||
@@ -39,6 +40,7 @@ const RLSCodeEditor = ({
|
||||
placeholder,
|
||||
readOnly = false,
|
||||
|
||||
disableTabToUsePlaceholder = false,
|
||||
lineNumberStart,
|
||||
onChange = noop,
|
||||
onMount: _onMount = noop,
|
||||
@@ -50,6 +52,7 @@ const RLSCodeEditor = ({
|
||||
const monaco = useMonaco()
|
||||
const { project } = useProjectContext()
|
||||
|
||||
const placeholderId = `monaco-placeholder-${id}`
|
||||
const options: editor.IStandaloneEditorConstructionOptions = {
|
||||
tabSize: 2,
|
||||
fontSize: 13,
|
||||
@@ -70,27 +73,31 @@ const RLSCodeEditor = ({
|
||||
if (monacoRef !== undefined) monacoRef.current = monaco
|
||||
|
||||
hasValue.current = editor.createContextKey('hasValue', false)
|
||||
const placeholderEl = document.querySelector('.monaco-placeholder') as HTMLElement | null
|
||||
if (placeholderEl && placeholder !== undefined) placeholderEl.style.display = 'block'
|
||||
const placeholderEl = document.getElementById(placeholderId) as HTMLElement | null
|
||||
if (placeholderEl && placeholder !== undefined && (value ?? '').trim().length === 0) {
|
||||
placeholderEl.style.display = 'block'
|
||||
}
|
||||
|
||||
editor.addCommand(
|
||||
monaco.KeyCode.Tab,
|
||||
() => {
|
||||
editor.executeEdits('source', [
|
||||
{
|
||||
// @ts-ignore
|
||||
identifier: 'add-placeholder',
|
||||
range: new monaco.Range(1, 1, 1, 1),
|
||||
text: (placeholder ?? '')
|
||||
.split('\n\n')
|
||||
.join('\n')
|
||||
.replaceAll('*', '')
|
||||
.replaceAll(' ', ''),
|
||||
},
|
||||
])
|
||||
},
|
||||
'!hasValue'
|
||||
)
|
||||
if (!disableTabToUsePlaceholder) {
|
||||
editor.addCommand(
|
||||
monaco.KeyCode.Tab,
|
||||
() => {
|
||||
editor.executeEdits('source', [
|
||||
{
|
||||
// @ts-ignore
|
||||
identifier: 'add-placeholder',
|
||||
range: new monaco.Range(1, 1, 1, 1),
|
||||
text: (placeholder ?? '')
|
||||
.split('\n\n')
|
||||
.join('\n')
|
||||
.replaceAll('*', '')
|
||||
.replaceAll(' ', ''),
|
||||
},
|
||||
])
|
||||
},
|
||||
'!hasValue'
|
||||
)
|
||||
}
|
||||
|
||||
editor.focus()
|
||||
|
||||
@@ -100,7 +107,7 @@ const RLSCodeEditor = ({
|
||||
const onChangeContent: OnChange = (value) => {
|
||||
hasValue.current.set((value ?? '').length > 0)
|
||||
|
||||
const placeholderEl = document.querySelector('.monaco-placeholder') as HTMLElement | null
|
||||
const placeholderEl = document.getElementById(placeholderId) as HTMLElement | null
|
||||
if (placeholderEl) {
|
||||
if (!value) {
|
||||
placeholderEl.style.display = 'block'
|
||||
@@ -148,6 +155,13 @@ const RLSCodeEditor = ({
|
||||
}
|
||||
}, [monaco])
|
||||
|
||||
useEffect(() => {
|
||||
if (value !== undefined && value.trim().length > 0) {
|
||||
const placeholderEl = document.getElementById(placeholderId) as HTMLElement | null
|
||||
if (placeholderEl) placeholderEl.style.display = 'none'
|
||||
}
|
||||
}, [value])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Editor
|
||||
@@ -164,6 +178,7 @@ const RLSCodeEditor = ({
|
||||
/>
|
||||
{placeholder !== undefined && (
|
||||
<div
|
||||
id={placeholderId}
|
||||
className="monaco-placeholder absolute top-[0px] left-[57px] text-sm pointer-events-none font-mono [&>div>p]:text-foreground-lighter [&>div>p]:!m-0 tracking-tighter"
|
||||
style={{ display: 'none' }}
|
||||
>
|
||||
|
||||
@@ -551,14 +551,20 @@ export const AIPolicyEditorPanel = memo(function ({
|
||||
/>
|
||||
|
||||
<div
|
||||
className={`py-1 relative ${incomingChange ? 'hidden' : 'block'}`}
|
||||
className={`mt-1 relative ${incomingChange ? 'hidden' : 'block'}`}
|
||||
style={{
|
||||
height:
|
||||
expOneContentHeight <= 100 ? `${8 + expOneContentHeight}px` : '108px',
|
||||
}}
|
||||
>
|
||||
<RLSCodeEditor
|
||||
disableTabToUsePlaceholder
|
||||
id="rls-exp-one-editor"
|
||||
placeholder={
|
||||
command === 'insert'
|
||||
? '-- Provide a SQL expression for the with check statement'
|
||||
: '-- Provide a SQL expression for the using statement'
|
||||
}
|
||||
defaultValue={command === 'insert' ? check : using}
|
||||
value={command === 'insert' ? check : using}
|
||||
editorRef={editorOneRef}
|
||||
@@ -608,7 +614,7 @@ export const AIPolicyEditorPanel = memo(function ({
|
||||
{showCheckBlock && (
|
||||
<>
|
||||
<div
|
||||
className={`py-1 relative ${incomingChange ? 'hidden' : 'block'}`}
|
||||
className={`mt-1 relative ${incomingChange ? 'hidden' : 'block'}`}
|
||||
style={{
|
||||
height:
|
||||
expTwoContentHeight <= 100
|
||||
@@ -617,7 +623,9 @@ export const AIPolicyEditorPanel = memo(function ({
|
||||
}}
|
||||
>
|
||||
<RLSCodeEditor
|
||||
disableTabToUsePlaceholder
|
||||
id="rls-exp-two-editor"
|
||||
placeholder="-- Provide a SQL expression for the with check statement"
|
||||
defaultValue={check}
|
||||
value={check}
|
||||
editorRef={editorTwoRef}
|
||||
@@ -843,22 +851,20 @@ export const AIPolicyEditorPanel = memo(function ({
|
||||
|
||||
<ConfirmationModal
|
||||
visible={isClosingPolicyEditorPanel}
|
||||
header="Discard changes"
|
||||
buttonLabel="Discard"
|
||||
onSelectCancel={() => {
|
||||
title="Discard changes"
|
||||
confirmLabel="Discard"
|
||||
onCancel={() => {
|
||||
setIsClosingPolicyEditorPanel(false)
|
||||
}}
|
||||
onSelectConfirm={() => {
|
||||
onConfirm={() => {
|
||||
onSelectCancel()
|
||||
setIsClosingPolicyEditorPanel(false)
|
||||
}}
|
||||
>
|
||||
<Modal.Content>
|
||||
<p className="py-4 text-sm text-foreground-light">
|
||||
Are you sure you want to close the editor? Any unsaved changes on your policy and
|
||||
conversations with the Assistant will be lost.
|
||||
</p>
|
||||
</Modal.Content>
|
||||
<p className="text-sm text-foreground-light">
|
||||
Are you sure you want to close the editor? Any unsaved changes on your policy and
|
||||
conversations with the Assistant will be lost.
|
||||
</p>
|
||||
</ConfirmationModal>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -187,21 +187,19 @@ const PolicyEditorModal = ({
|
||||
<div>
|
||||
<ConfirmationModal
|
||||
visible={isClosingPolicyEditorModal}
|
||||
header="Discard changes"
|
||||
buttonLabel="Discard"
|
||||
onSelectCancel={() => setIsClosingPolicyEditorModal(false)}
|
||||
onSelectConfirm={() => {
|
||||
title="Discard changes"
|
||||
confirmLabel="Discard"
|
||||
onCancel={() => setIsClosingPolicyEditorModal(false)}
|
||||
onConfirm={() => {
|
||||
onSelectCancel()
|
||||
setIsClosingPolicyEditorModal(false)
|
||||
setIsDirty(false)
|
||||
}}
|
||||
>
|
||||
<Modal.Content>
|
||||
<p className="py-4 text-sm text-foreground-light">
|
||||
There are unsaved changes. Are you sure you want to close the editor? Your changes
|
||||
will be lost.
|
||||
</p>
|
||||
</Modal.Content>
|
||||
<p className="text-sm text-foreground-light">
|
||||
There are unsaved changes. Are you sure you want to close the editor? Your changes will
|
||||
be lost.
|
||||
</p>
|
||||
</ConfirmationModal>
|
||||
{view === POLICY_MODAL_VIEWS.SELECTION ? (
|
||||
<PolicySelection
|
||||
|
||||
@@ -277,6 +277,18 @@ const SmtpForm = () => {
|
||||
}
|
||||
>
|
||||
<FormSectionContent loading={isLoading}>
|
||||
{values['SMTP_HOST'] && values['SMTP_HOST'].endsWith('.gmail.com') && (
|
||||
<Alert_Shadcn_ variant="warning">
|
||||
<IconAlertTriangle strokeWidth={2} />
|
||||
<AlertTitle_Shadcn_>Check your SMTP provider</AlertTitle_Shadcn_>
|
||||
<AlertDescription_Shadcn_>
|
||||
Not all SMTP providers are designed for the email sending required by
|
||||
Supabase Auth. It looks like the SMTP provider you entered is designed for
|
||||
sending personal email messages and not for sending transactional messages.
|
||||
Although you can ignore this warning, email deliverability may be impacted.
|
||||
</AlertDescription_Shadcn_>
|
||||
</Alert_Shadcn_>
|
||||
)}
|
||||
<Input
|
||||
name="SMTP_HOST"
|
||||
placeholder="your.smtp.host.com"
|
||||
|
||||
@@ -121,7 +121,7 @@ const UserDropdown = ({
|
||||
<>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button type="text" loading={isLoading} className="hover:border-gray-500 flex">
|
||||
<Button type="text" loading={isLoading} className="hover:border-muted flex">
|
||||
<IconMoreVertical />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
@@ -224,35 +224,30 @@ const UserDropdown = ({
|
||||
|
||||
<ConfirmationModal
|
||||
visible={isDeleteModalOpen}
|
||||
header="Confirm to delete"
|
||||
buttonLabel="Delete"
|
||||
buttonLoadingLabel="Delete"
|
||||
onSelectCancel={() => setIsDeleteModalOpen(false)}
|
||||
onSelectConfirm={() => {
|
||||
title="Confirm to delete"
|
||||
confirmLabel="Delete"
|
||||
onCancel={() => setIsDeleteModalOpen(false)}
|
||||
onConfirm={() => {
|
||||
handleDelete()
|
||||
}}
|
||||
>
|
||||
<Modal.Content>
|
||||
<p className="py-4 text-sm text-foreground-light">
|
||||
This is permanent! Are you sure you want to delete user {user.email}?
|
||||
</p>
|
||||
</Modal.Content>
|
||||
<p className="text-sm text-foreground-light">
|
||||
This is permanent! Are you sure you want to delete user {user.email}?
|
||||
</p>
|
||||
</ConfirmationModal>
|
||||
|
||||
<ConfirmationModal
|
||||
visible={isDeleteFactorsModalOpen}
|
||||
header="Confirm to delete"
|
||||
buttonLabel="Delete"
|
||||
onSelectCancel={() => setIsDeleteFactorsModalOpen(false)}
|
||||
onSelectConfirm={() => {
|
||||
title="Confirm to delete"
|
||||
confirmLabel="Delete"
|
||||
onCancel={() => setIsDeleteFactorsModalOpen(false)}
|
||||
onConfirm={() => {
|
||||
handleDeleteFactors()
|
||||
}}
|
||||
>
|
||||
<Modal.Content>
|
||||
<p className="py-4 text-sm text-foreground-light">
|
||||
This is permanent! Are you sure you want to delete the user's MFA factors?
|
||||
</p>
|
||||
</Modal.Content>
|
||||
<p className="text-sm text-foreground-light">
|
||||
This is permanent! Are you sure you want to delete the user's MFA factors?
|
||||
</p>
|
||||
</ConfirmationModal>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -40,7 +40,9 @@ const UserListItem = ({
|
||||
<span className="text-foreground">{!user.phone ? '-' : user.phone}</span>
|
||||
</Table.td>
|
||||
<Table.td className="table-cell">
|
||||
<span className="capitalize text-foreground">{user?.raw_app_meta_data?.provider}</span>
|
||||
<span className="capitalize text-foreground">
|
||||
{user.is_anonymous ? 'Anonymous' : user?.raw_app_meta_data?.provider}
|
||||
</span>
|
||||
</Table.td>
|
||||
<Table.td className="table-cell">
|
||||
<span className="text-foreground">{createdAt?.format('DD MMM, YYYY HH:mm')}</span>
|
||||
|
||||
@@ -27,7 +27,7 @@ const UsersListItemSkeleton = () => {
|
||||
<ShimmeringLoader className="h-[20px] py-0 w-20" />
|
||||
</Table.td>
|
||||
<Table.td className="table-cell">
|
||||
<Button type="text" disabled={true} className="hover:border-gray-500 flex">
|
||||
<Button type="text" disabled={true} className="hover:border-muted flex">
|
||||
<IconMoreHorizontal />
|
||||
</Button>
|
||||
</Table.td>
|
||||
|
||||
@@ -344,44 +344,28 @@ const BranchManagement = () => {
|
||||
/>
|
||||
|
||||
<ConfirmationModal
|
||||
danger
|
||||
variant={'destructive'}
|
||||
size="medium"
|
||||
loading={isDisabling}
|
||||
visible={showDisableBranching}
|
||||
header="Confirm disable branching for project"
|
||||
buttonLabel="Confirm disable branching"
|
||||
buttonLoadingLabel="Disabling branching..."
|
||||
onSelectConfirm={() => onConfirmDisableBranching()}
|
||||
onSelectCancel={() => setShowDisableBranching(false)}
|
||||
title="Confirm disable branching for project"
|
||||
confirmLabel="Confirm disable branching"
|
||||
confirmLabelLoading="Disabling branching..."
|
||||
onConfirm={() => onConfirmDisableBranching()}
|
||||
onCancel={() => setShowDisableBranching(false)}
|
||||
alert={{
|
||||
title: 'This action cannot be undone',
|
||||
description:
|
||||
'All database preview branches will be removed upon disabling branching. You may still re-enable branching again thereafter, but your existing preview branches will not be restored.',
|
||||
}}
|
||||
>
|
||||
<Modal.Content>
|
||||
<div className="py-6">
|
||||
<Alert_Shadcn_ variant="warning">
|
||||
<IconAlertTriangle strokeWidth={2} />
|
||||
<AlertTitle_Shadcn_>This action cannot be undone</AlertTitle_Shadcn_>
|
||||
<AlertDescription_Shadcn_>
|
||||
All database preview branches will be removed upon disabling branching. You may
|
||||
still re-enable branching again thereafter, but your existing preview branches will
|
||||
not be restored.
|
||||
</AlertDescription_Shadcn_>
|
||||
</Alert_Shadcn_>
|
||||
<ul className="mt-4 space-y-5">
|
||||
<li className="flex gap-3">
|
||||
<div>
|
||||
<strong className="text-sm">Before you disable branching, consider:</strong>
|
||||
<ul className="space-y-2 mt-2 text-sm text-foreground-light">
|
||||
<li className="list-disc ml-6">
|
||||
Your project no longer requires database previews.
|
||||
</li>
|
||||
<li className="list-disc ml-6">
|
||||
None of your database previews are currently being used in any app.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</Modal.Content>
|
||||
<p className="text-sm">Before you disable branching, consider:</p>
|
||||
<ul className="space-y-2 mt-2 text-sm text-foreground-light">
|
||||
<li className="list-disc ml-6">Your project no longer requires database previews.</li>
|
||||
<li className="list-disc ml-6">
|
||||
None of your database previews are currently being used in any app.
|
||||
</li>
|
||||
</ul>
|
||||
</ConfirmationModal>
|
||||
|
||||
<CreateBranchModal visible={showCreateBranch} onClose={() => setShowCreateBranch(false)} />
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { useParams } from 'common'
|
||||
import dayjs from 'dayjs'
|
||||
import { GitPullRequest } from 'lucide-react'
|
||||
import { GitPullRequest, RefreshCw, Trash2 } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { PropsWithChildren, ReactNode } from 'react'
|
||||
import { PropsWithChildren, ReactNode, useState } from 'react'
|
||||
import toast from 'react-hot-toast'
|
||||
import { useInView } from 'react-intersection-observer'
|
||||
|
||||
import ShimmeringLoader from 'components/ui/ShimmeringLoader'
|
||||
import { useBranchQuery } from 'data/branches/branch-query'
|
||||
import { useBranchResetMutation } from 'data/branches/branch-reset-mutation'
|
||||
import type { Branch } from 'data/branches/branches-query'
|
||||
import {
|
||||
Badge,
|
||||
@@ -19,8 +21,9 @@ import {
|
||||
IconExternalLink,
|
||||
IconMoreVertical,
|
||||
IconShield,
|
||||
IconTrash,
|
||||
Modal,
|
||||
} from 'ui'
|
||||
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
|
||||
import BranchStatusBadge from './BranchStatusBadge'
|
||||
|
||||
interface BranchManagementSectionProps {
|
||||
@@ -111,6 +114,23 @@ export const BranchRow = ({
|
||||
}
|
||||
)
|
||||
|
||||
const [showConfirmResetModal, setShowConfirmResetModal] = useState(false)
|
||||
|
||||
const { mutate, isLoading: isResetting } = useBranchResetMutation({
|
||||
onSuccess() {
|
||||
toast.success('Success! Please allow a few seconds for the branch to reset.')
|
||||
setShowConfirmResetModal(false)
|
||||
},
|
||||
})
|
||||
|
||||
function onConfirmReset() {
|
||||
if (!projectRef) {
|
||||
throw new Error('Invalid project reference')
|
||||
}
|
||||
|
||||
mutate({ id: branch.id, projectRef })
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full flex items-center justify-between px-6 py-2.5" ref={ref}>
|
||||
<div className="flex items-center gap-x-4">
|
||||
@@ -204,16 +224,40 @@ export const BranchRow = ({
|
||||
<Button type="text" icon={<IconMoreVertical />} className="px-1" />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="p-0 w-56" side="bottom" align="end">
|
||||
<DropdownMenuItem
|
||||
className="gap-x-2"
|
||||
onSelect={() => setShowConfirmResetModal(true)}
|
||||
onClick={() => setShowConfirmResetModal(true)}
|
||||
>
|
||||
<RefreshCw size={14} />
|
||||
Reset Branch
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="gap-x-2"
|
||||
onSelect={() => onSelectDeleteBranch?.()}
|
||||
onClick={() => onSelectDeleteBranch?.()}
|
||||
>
|
||||
<IconTrash size="tiny" />
|
||||
<p>Delete branch</p>
|
||||
<Trash2 size={14} />
|
||||
Delete branch
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
<ConfirmationModal
|
||||
variant={'destructive'}
|
||||
visible={showConfirmResetModal}
|
||||
confirmLabel="Reset branch"
|
||||
title="Confirm branch reset"
|
||||
loading={isResetting}
|
||||
onCancel={() => {
|
||||
setShowConfirmResetModal(false)
|
||||
}}
|
||||
onConfirm={onConfirmReset}
|
||||
>
|
||||
<p className="text-sm text-foreground-light">
|
||||
Are you sure you want to reset the "{branch.name}" branch? All data will be deleted.
|
||||
</p>
|
||||
</ConfirmationModal>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { IconInfo } from 'ui'
|
||||
|
||||
const BackupsEmpty = () => {
|
||||
return (
|
||||
<div className="block w-full rounded border border-gray-400 border-opacity-50 bg-gray-300 p-3">
|
||||
<div className="block w-full rounded border border-muted border-opacity-50 bg-gray-300 p-3">
|
||||
<div className="flex space-x-3">
|
||||
<IconInfo size={20} strokeWidth={1.5} />
|
||||
<p className="text-sm">No backups created yet - check again tomorrow.</p>
|
||||
|
||||
@@ -107,26 +107,22 @@ const BackupsList = () => {
|
||||
</div>
|
||||
<ConfirmationModal
|
||||
size="medium"
|
||||
buttonLabel="Confirm restore"
|
||||
buttonLoadingLabel="Restoring"
|
||||
confirmLabel="Confirm restore"
|
||||
confirmLabelLoading="Restoring"
|
||||
visible={selectedBackup !== undefined}
|
||||
header="Confirm to restore from backup"
|
||||
title="Confirm to restore from backup"
|
||||
loading={isRestoring || isSuccessBackup}
|
||||
onSelectCancel={() => setSelectedBackup(undefined)}
|
||||
onSelectConfirm={() => {
|
||||
onCancel={() => setSelectedBackup(undefined)}
|
||||
onConfirm={() => {
|
||||
if (selectedBackup === undefined) return console.error('Backup required')
|
||||
restoreFromBackup({ ref: projectRef, backup: selectedBackup })
|
||||
}}
|
||||
>
|
||||
<Modal.Content>
|
||||
<div className="pt-6 pb-5">
|
||||
<p>
|
||||
Are you sure you want to restore from
|
||||
{dayjs(selectedBackup?.inserted_at).format('DD MMM YYYY')}? This will destroy any new
|
||||
data written since this backup was made.
|
||||
</p>
|
||||
</div>
|
||||
</Modal.Content>
|
||||
<p>
|
||||
Are you sure you want to restore from
|
||||
{dayjs(selectedBackup?.inserted_at).format('DD MMM YYYY')}? This will destroy any new data
|
||||
written since this backup was made.
|
||||
</p>
|
||||
</ConfirmationModal>
|
||||
</>
|
||||
)
|
||||
|
||||
+17
-25
@@ -46,40 +46,32 @@ const DeleteEnumeratedTypeModal = ({
|
||||
|
||||
return (
|
||||
<ConfirmationModal
|
||||
danger
|
||||
variant={'destructive'}
|
||||
size="medium"
|
||||
loading={isDeleting}
|
||||
visible={visible}
|
||||
header={
|
||||
title={
|
||||
<>
|
||||
Confirm to delete enumerated type{' '}
|
||||
<code className="text-sm">{selectedEnumeratedType?.name}</code>
|
||||
</>
|
||||
}
|
||||
buttonLabel="Confirm delete"
|
||||
buttonLoadingLabel="Deleting..."
|
||||
onSelectConfirm={() => onConfirmDeleteType()}
|
||||
onSelectCancel={onClose}
|
||||
confirmLabel="Confirm delete"
|
||||
confirmLabelLoading="Deleting..."
|
||||
onCancel={onClose}
|
||||
onConfirm={() => onConfirmDeleteType()}
|
||||
alert={{
|
||||
title: 'This action cannot be undone',
|
||||
description:
|
||||
'You will need to re-create the enumerated type if you want to revert the deletion.',
|
||||
}}
|
||||
>
|
||||
<Modal.Content>
|
||||
<div className="py-6">
|
||||
<Alert_Shadcn_ variant="warning">
|
||||
<IconAlertTriangle strokeWidth={2} />
|
||||
<AlertTitle_Shadcn_>This action cannot be undone</AlertTitle_Shadcn_>
|
||||
<AlertDescription_Shadcn_>
|
||||
You will need to re-create the enumerated type if you want to revert the deletion.
|
||||
</AlertDescription_Shadcn_>
|
||||
</Alert_Shadcn_>
|
||||
<div className="mt-4">
|
||||
<strong className="text-sm">Before deleting this enumerated type, consider:</strong>
|
||||
<ul className="space-y-2 mt-2 text-sm text-foreground-light">
|
||||
<li className="list-disc ml-6">
|
||||
This enumerated type is no longer in use in any tables or functions
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</Modal.Content>
|
||||
<p className="text-sm">Before deleting this enumerated type, consider:</p>
|
||||
<ul className="space-y-2 mt-2 text-sm text-foreground-light">
|
||||
<li className="list-disc ml-6">
|
||||
This enumerated type is no longer in use in any tables or functions
|
||||
</li>
|
||||
</ul>
|
||||
</ConfirmationModal>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -139,17 +139,15 @@ const ExtensionCard = ({ extension }: ExtensionCardProps) => {
|
||||
/>
|
||||
<ConfirmationModal
|
||||
visible={isDisableModalOpen}
|
||||
header="Confirm to disable extension"
|
||||
buttonLabel="Disable"
|
||||
buttonLoadingLabel="Disabling"
|
||||
onSelectCancel={() => setIsDisableModalOpen(false)}
|
||||
onSelectConfirm={() => onConfirmDisable()}
|
||||
title="Confirm to disable extension"
|
||||
confirmLabel="Disable"
|
||||
confirmLabelLoading="Disabling"
|
||||
onCancel={() => setIsDisableModalOpen(false)}
|
||||
onConfirm={() => onConfirmDisable()}
|
||||
>
|
||||
<Modal.Content>
|
||||
<p className="py-4 text-sm text-foreground-light">
|
||||
Are you sure you want to turn OFF the "{extension.name}" extension?
|
||||
</p>
|
||||
</Modal.Content>
|
||||
<p className="text-sm text-foreground-light">
|
||||
Are you sure you want to turn OFF the "{extension.name}" extension?
|
||||
</p>
|
||||
</ConfirmationModal>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -405,20 +405,18 @@ const CreateFunction = ({ func, visible, setVisible }: CreateFunctionProps) => {
|
||||
) : null}
|
||||
<ConfirmationModal
|
||||
visible={isClosingPanel}
|
||||
header="Discard changes"
|
||||
buttonLabel="Discard"
|
||||
onSelectCancel={() => setIsClosingPanel(false)}
|
||||
onSelectConfirm={() => {
|
||||
title="Discard changes"
|
||||
confirmLabel="Discard"
|
||||
onCancel={() => setIsClosingPanel(false)}
|
||||
onConfirm={() => {
|
||||
setIsClosingPanel(false)
|
||||
setVisible(!visible)
|
||||
}}
|
||||
>
|
||||
<Modal.Content>
|
||||
<p className="py-4 text-sm text-foreground-light">
|
||||
There are unsaved changes. Are you sure you want to close the panel? Your changes will
|
||||
be lost.
|
||||
</p>
|
||||
</Modal.Content>
|
||||
<p className="text-sm text-foreground-light">
|
||||
There are unsaved changes. Are you sure you want to close the panel? Your changes will
|
||||
be lost.
|
||||
</p>
|
||||
</ConfirmationModal>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
|
||||
@@ -310,21 +310,19 @@ const EditHookPanel = ({ visible, selectedHook, onClose }: EditHookPanelProps) =
|
||||
</SidePanel>
|
||||
<ConfirmationModal
|
||||
visible={isClosingPanel}
|
||||
header="Discard changes"
|
||||
buttonLabel="Discard"
|
||||
onSelectCancel={() => setIsClosingPanel(false)}
|
||||
onSelectConfirm={() => {
|
||||
title="Discard changes"
|
||||
confirmLabel="Discard"
|
||||
onCancel={() => setIsClosingPanel(false)}
|
||||
onConfirm={() => {
|
||||
setIsClosingPanel(false)
|
||||
setIsEdited(false)
|
||||
onClose()
|
||||
}}
|
||||
>
|
||||
<Modal.Content>
|
||||
<p className="py-4 text-sm text-foreground-light">
|
||||
There are unsaved changes. Are you sure you want to close the panel? Your changes will
|
||||
be lost.
|
||||
</p>
|
||||
</Modal.Content>
|
||||
<p className="text-sm text-foreground-light">
|
||||
There are unsaved changes. Are you sure you want to close the panel? Your changes will be
|
||||
lost.
|
||||
</p>
|
||||
</ConfirmationModal>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -7,7 +7,6 @@ import { Button, IconExternalLink, IconSearch, Input } from 'ui'
|
||||
|
||||
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
|
||||
import AlertError from 'components/ui/AlertError'
|
||||
import { FormHeader } from 'components/ui/Forms'
|
||||
import NoSearchResults from 'components/ui/NoSearchResults'
|
||||
import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader'
|
||||
import { useDatabaseHooksQuery } from 'data/database-triggers/database-triggers-query'
|
||||
@@ -45,95 +44,87 @@ const HooksList = ({ createHook = noop, editHook = noop, deleteHook = noop }: Ho
|
||||
const isPermissionsLoaded = usePermissionsLoaded()
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="w-full space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<FormHeader
|
||||
title="Database Webhooks"
|
||||
description="Send real-time data from your database to another system whenever a table event occurs"
|
||||
<Input
|
||||
placeholder="Search for a webhook"
|
||||
size="small"
|
||||
icon={<IconSearch size="tiny" />}
|
||||
value={filterString}
|
||||
className="w-64"
|
||||
onChange={(e) => setFilterString(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<Input
|
||||
placeholder="Search for a webhook"
|
||||
size="small"
|
||||
icon={<IconSearch size="tiny" />}
|
||||
value={filterString}
|
||||
className="w-64"
|
||||
onChange={(e) => setFilterString(e.target.value)}
|
||||
/>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button asChild type="default" icon={<IconExternalLink strokeWidth={1.5} />}>
|
||||
<Link
|
||||
href="https://supabase.com/docs/guides/database/webhooks"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button asChild type="default" icon={<IconExternalLink strokeWidth={1.5} />}>
|
||||
<Link
|
||||
href="https://supabase.com/docs/guides/database/webhooks"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Documentation
|
||||
</Link>
|
||||
</Button>
|
||||
<Tooltip.Root delayDuration={0}>
|
||||
<Tooltip.Trigger asChild>
|
||||
<Button
|
||||
disabled={!isPermissionsLoaded || !canCreateWebhooks}
|
||||
onClick={() => createHook()}
|
||||
>
|
||||
Documentation
|
||||
</Link>
|
||||
</Button>
|
||||
<Tooltip.Root delayDuration={0}>
|
||||
<Tooltip.Trigger asChild>
|
||||
<Button
|
||||
disabled={!isPermissionsLoaded || !canCreateWebhooks}
|
||||
onClick={() => createHook()}
|
||||
>
|
||||
Create a new hook
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
{isPermissionsLoaded && !canCreateWebhooks && (
|
||||
<Tooltip.Portal>
|
||||
<Tooltip.Content side="bottom">
|
||||
<Tooltip.Arrow className="radix-tooltip-arrow" />
|
||||
<div
|
||||
className={[
|
||||
'rounded bg-alternative py-1 px-2 leading-none shadow',
|
||||
'border border-background',
|
||||
].join(' ')}
|
||||
>
|
||||
<span className="text-xs text-foreground">
|
||||
You need additional permissions to create webhooks
|
||||
</span>
|
||||
</div>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Portal>
|
||||
)}
|
||||
</Tooltip.Root>
|
||||
</div>
|
||||
Create a new hook
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
{isPermissionsLoaded && !canCreateWebhooks && (
|
||||
<Tooltip.Portal>
|
||||
<Tooltip.Content side="bottom">
|
||||
<Tooltip.Arrow className="radix-tooltip-arrow" />
|
||||
<div
|
||||
className={[
|
||||
'rounded bg-alternative py-1 px-2 leading-none shadow',
|
||||
'border border-background',
|
||||
].join(' ')}
|
||||
>
|
||||
<span className="text-xs text-foreground">
|
||||
You need additional permissions to create webhooks
|
||||
</span>
|
||||
</div>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Portal>
|
||||
)}
|
||||
</Tooltip.Root>
|
||||
</div>
|
||||
|
||||
{isLoading && (
|
||||
<div className="py-4">
|
||||
<GenericSkeletonLoader />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isError && <AlertError error={error} subject="Failed to retrieve database webhooks" />}
|
||||
|
||||
{isSuccess &&
|
||||
(hooks.length <= 0 ? (
|
||||
<HooksListEmpty />
|
||||
) : (
|
||||
<>
|
||||
{filteredHooks.length <= 0 && (
|
||||
<NoSearchResults
|
||||
searchString={filterString}
|
||||
onResetFilter={() => setFilterString('')}
|
||||
/>
|
||||
)}
|
||||
{filteredHookSchemas.map((schema: any) => (
|
||||
<SchemaTable
|
||||
key={schema}
|
||||
filterString={filterString}
|
||||
schema={schema}
|
||||
editHook={editHook}
|
||||
deleteHook={deleteHook}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
|
||||
{isLoading && (
|
||||
<div className="py-4">
|
||||
<GenericSkeletonLoader />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isError && <AlertError error={error} subject="Failed to retrieve database webhooks" />}
|
||||
|
||||
{isSuccess &&
|
||||
(hooks.length <= 0 ? (
|
||||
<HooksListEmpty />
|
||||
) : (
|
||||
<>
|
||||
{filteredHooks.length <= 0 && (
|
||||
<NoSearchResults
|
||||
searchString={filterString}
|
||||
onResetFilter={() => setFilterString('')}
|
||||
/>
|
||||
)}
|
||||
{filteredHookSchemas.map((schema: any) => (
|
||||
<SchemaTable
|
||||
key={schema}
|
||||
filterString={filterString}
|
||||
schema={schema}
|
||||
editHook={editHook}
|
||||
deleteHook={deleteHook}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import Link from 'next/link'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import toast from 'react-hot-toast'
|
||||
|
||||
import { SelectValue } from '@ui/components/shadcn/ui/select'
|
||||
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
|
||||
import { CodeEditor } from 'components/ui/CodeEditor'
|
||||
import ShimmeringLoader from 'components/ui/ShimmeringLoader'
|
||||
@@ -10,8 +11,19 @@ import { useSchemasQuery } from 'data/database/schemas-query'
|
||||
import { useTableColumnsQuery } from 'data/database/table-columns-query'
|
||||
import { useEntityTypesQuery } from 'data/entity-types/entity-types-infinite-query'
|
||||
import { useExecuteSqlMutation } from 'data/sql/execute-sql-mutation'
|
||||
import { Button, Input, Listbox, SidePanel } from 'ui'
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
SelectContent_Shadcn_,
|
||||
SelectItem_Shadcn_,
|
||||
SelectSeparator_Shadcn_,
|
||||
SelectTrigger_Shadcn_,
|
||||
SelectValue_Shadcn_,
|
||||
Select_Shadcn_,
|
||||
SidePanel,
|
||||
} from 'ui'
|
||||
import MultiSelect, { MultiSelectOption } from 'ui-patterns/MultiSelect'
|
||||
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
|
||||
import { INDEX_TYPES } from './Indexes.constants'
|
||||
|
||||
interface CreateIndexSidePanelProps {
|
||||
@@ -22,7 +34,7 @@ interface CreateIndexSidePanelProps {
|
||||
const CreateIndexSidePanel = ({ visible, onClose }: CreateIndexSidePanelProps) => {
|
||||
const { project } = useProjectContext()
|
||||
const [selectedSchema, setSelectedSchema] = useState('public')
|
||||
const [selectedEntity, setSelectedEntity] = useState('---')
|
||||
const [selectedEntity, setSelectedEntity] = useState<string | undefined>(undefined)
|
||||
const [selectedColumns, setSelectedColumns] = useState<string[]>([])
|
||||
const [selectedIndexType, setSelectedIndexType] = useState<string>(INDEX_TYPES[0].value)
|
||||
|
||||
@@ -92,14 +104,14 @@ CREATE INDEX ON "${selectedSchema}"."${selectedEntity}" USING ${selectedIndexTyp
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
setSelectedSchema('public')
|
||||
setSelectedEntity('---')
|
||||
setSelectedEntity('')
|
||||
setSelectedColumns([])
|
||||
setSelectedIndexType(INDEX_TYPES[0].value)
|
||||
}
|
||||
}, [visible])
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedEntity('---')
|
||||
setSelectedEntity('')
|
||||
setSelectedColumns([])
|
||||
setSelectedIndexType(INDEX_TYPES[0].value)
|
||||
}, [selectedSchema])
|
||||
@@ -109,6 +121,8 @@ CREATE INDEX ON "${selectedSchema}"."${selectedEntity}" USING ${selectedIndexTyp
|
||||
setSelectedIndexType(INDEX_TYPES[0].value)
|
||||
}, [selectedEntity])
|
||||
|
||||
const isSelectEntityDisabled = entityTypes.length === 0
|
||||
|
||||
return (
|
||||
<SidePanel
|
||||
size="large"
|
||||
@@ -121,49 +135,61 @@ CREATE INDEX ON "${selectedSchema}"."${selectedEntity}" USING ${selectedIndexTyp
|
||||
>
|
||||
<div className="py-6 space-y-6">
|
||||
<SidePanel.Content className="space-y-6">
|
||||
<Listbox
|
||||
size="small"
|
||||
label="Select a schema"
|
||||
value={selectedSchema}
|
||||
onChange={setSelectedSchema}
|
||||
>
|
||||
{(schemas ?? []).map((schema) => (
|
||||
<Listbox.Option key={schema.name} value={schema.name} label={schema.name}>
|
||||
{schema.name}
|
||||
</Listbox.Option>
|
||||
))}
|
||||
</Listbox>
|
||||
|
||||
{entityTypes.length === 0 ? (
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm text-foreground-light leading-4">Select a table</p>
|
||||
<Input
|
||||
disabled
|
||||
placeholder="No tables available in schema"
|
||||
descriptionText="Create a table in this schema via the Table or SQL editor first"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<Listbox
|
||||
size="small"
|
||||
label="Select a table"
|
||||
value={selectedEntity}
|
||||
onChange={setSelectedEntity}
|
||||
<FormItemLayout label="Select a schema" name="select-schema" isReactForm={false}>
|
||||
<Select_Shadcn_
|
||||
value={selectedSchema}
|
||||
onValueChange={setSelectedSchema}
|
||||
name="select-schema"
|
||||
>
|
||||
<Listbox.Option key="default-option-table" value="---" label="---">
|
||||
---
|
||||
</Listbox.Option>
|
||||
{(entityTypes ?? []).map((entity) => (
|
||||
<Listbox.Option key={entity.name} value={entity.name} label={entity.name}>
|
||||
{entity.name}
|
||||
</Listbox.Option>
|
||||
))}
|
||||
</Listbox>
|
||||
)}
|
||||
<SelectTrigger_Shadcn_ size="small">
|
||||
<SelectValue_Shadcn_>{selectedSchema}</SelectValue_Shadcn_>
|
||||
</SelectTrigger_Shadcn_>
|
||||
<SelectContent_Shadcn_>
|
||||
{(schemas ?? []).map((schema) => (
|
||||
<SelectItem_Shadcn_ key={schema.name} value={schema.name}>
|
||||
{schema.name}
|
||||
</SelectItem_Shadcn_>
|
||||
))}
|
||||
</SelectContent_Shadcn_>
|
||||
</Select_Shadcn_>
|
||||
</FormItemLayout>
|
||||
|
||||
{selectedEntity !== '---' && (
|
||||
<div>
|
||||
<p className="text-sm text-foreground-light mb-2">Select up to 32 columns</p>
|
||||
<FormItemLayout
|
||||
label="Select a table"
|
||||
name="select-table"
|
||||
description={
|
||||
isSelectEntityDisabled &&
|
||||
'Create a table in this schema via the Table or SQL editor first'
|
||||
}
|
||||
isReactForm={false}
|
||||
>
|
||||
<Select_Shadcn_
|
||||
disabled={isSelectEntityDisabled}
|
||||
value={selectedEntity}
|
||||
onValueChange={setSelectedEntity}
|
||||
name="select-table"
|
||||
>
|
||||
<SelectTrigger_Shadcn_ size="small">
|
||||
<SelectValue
|
||||
placeholder={
|
||||
isSelectEntityDisabled ? 'No tables available in schema' : 'Choose a table'
|
||||
}
|
||||
>
|
||||
{selectedEntity}
|
||||
</SelectValue>
|
||||
</SelectTrigger_Shadcn_>
|
||||
<SelectContent_Shadcn_>
|
||||
{(entityTypes ?? []).map((entity) => (
|
||||
<SelectItem_Shadcn_ key={entity.name} value={entity.name}>
|
||||
{entity.name}
|
||||
</SelectItem_Shadcn_>
|
||||
))}
|
||||
</SelectContent_Shadcn_>
|
||||
</Select_Shadcn_>
|
||||
</FormItemLayout>
|
||||
|
||||
{selectedEntity && (
|
||||
<FormItemLayout label="Select up to 32 columns" isReactForm={false}>
|
||||
{isLoadingTableColumns && <ShimmeringLoader className="py-4" />}
|
||||
{isSuccessTableColumns && (
|
||||
<MultiSelect
|
||||
@@ -174,7 +200,7 @@ CREATE INDEX ON "${selectedSchema}"."${selectedEntity}" USING ${selectedIndexTyp
|
||||
onChange={setSelectedColumns}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</FormItemLayout>
|
||||
)}
|
||||
</SidePanel.Content>
|
||||
|
||||
@@ -182,21 +208,43 @@ CREATE INDEX ON "${selectedSchema}"."${selectedEntity}" USING ${selectedIndexTyp
|
||||
<>
|
||||
<SidePanel.Separator />
|
||||
<SidePanel.Content className="space-y-6">
|
||||
<Listbox
|
||||
size="small"
|
||||
<FormItemLayout
|
||||
label="Select an index type"
|
||||
value={selectedIndexType}
|
||||
onChange={setSelectedIndexType}
|
||||
name="selected-index-type"
|
||||
isReactForm={false}
|
||||
>
|
||||
{INDEX_TYPES.map((index) => (
|
||||
<Listbox.Option key={index.name} value={index.value} label={index.name}>
|
||||
<p>{index.name}</p>
|
||||
{index.description.split('\n').map((x, idx) => (
|
||||
<p key={`${index.value}-description-${idx}`}>{x}</p>
|
||||
<Select_Shadcn_
|
||||
value={selectedIndexType}
|
||||
onValueChange={setSelectedIndexType}
|
||||
name="selected-index-type"
|
||||
>
|
||||
<SelectTrigger_Shadcn_ size={'small'}>
|
||||
<SelectValue_Shadcn_ className="font-mono">
|
||||
{selectedIndexType}
|
||||
</SelectValue_Shadcn_>
|
||||
</SelectTrigger_Shadcn_>
|
||||
<SelectContent_Shadcn_>
|
||||
{INDEX_TYPES.map((index, i) => (
|
||||
<>
|
||||
<SelectItem_Shadcn_ key={index.name} value={index.value}>
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<span>{index.name}</span>
|
||||
{index.description.split('\n').map((x, idx) => (
|
||||
<span
|
||||
className="text-foreground-lighter group-focus:text-foreground-light group-data-[state=checked]:text-foreground-light"
|
||||
key={`${index.value}-description-${idx}`}
|
||||
>
|
||||
{x}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</SelectItem_Shadcn_>
|
||||
{i < INDEX_TYPES.length - 1 && <SelectSeparator_Shadcn_ />}
|
||||
</>
|
||||
))}
|
||||
</Listbox.Option>
|
||||
))}
|
||||
</Listbox>
|
||||
</SelectContent_Shadcn_>
|
||||
</Select_Shadcn_>
|
||||
</FormItemLayout>
|
||||
</SidePanel.Content>
|
||||
<SidePanel.Separator />
|
||||
<SidePanel.Content>
|
||||
|
||||
@@ -1,23 +1,10 @@
|
||||
import { partition, sortBy } from 'lodash'
|
||||
import Link from 'next/link'
|
||||
import { useState } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import toast from 'react-hot-toast'
|
||||
import {
|
||||
AlertDescription_Shadcn_,
|
||||
AlertTitle_Shadcn_,
|
||||
Alert_Shadcn_,
|
||||
Button,
|
||||
IconAlertCircle,
|
||||
IconAlertTriangle,
|
||||
IconExternalLink,
|
||||
IconSearch,
|
||||
IconTrash,
|
||||
Input,
|
||||
Modal,
|
||||
SidePanel,
|
||||
} from 'ui'
|
||||
import { Button, IconAlertCircle, IconSearch, IconTrash, Input, SidePanel } from 'ui'
|
||||
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
|
||||
|
||||
import { useParams } from 'common'
|
||||
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
|
||||
import Table from 'components/to-be-cleaned/Table'
|
||||
import AlertError from 'components/ui/AlertError'
|
||||
@@ -32,13 +19,15 @@ import ProtectedSchemaWarning from '../ProtectedSchemaWarning'
|
||||
import CreateIndexSidePanel from './CreateIndexSidePanel'
|
||||
|
||||
const Indexes = () => {
|
||||
const { project } = useProjectContext()
|
||||
const { schema: urlSchema, table } = useParams()
|
||||
|
||||
const [search, setSearch] = useState('')
|
||||
const [selectedSchema, setSelectedSchema] = useState('public')
|
||||
const [showCreateIndex, setShowCreateIndex] = useState(false)
|
||||
const [selectedIndex, setSelectedIndex] = useState<DatabaseIndex>()
|
||||
const [selectedIndexToDelete, setSelectedIndexToDelete] = useState<DatabaseIndex>()
|
||||
|
||||
const { project } = useProjectContext()
|
||||
const {
|
||||
data: allIndexes,
|
||||
refetch: refetchIndexes,
|
||||
@@ -93,38 +82,20 @@ const Indexes = () => {
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (urlSchema !== undefined) {
|
||||
const schema = schemas?.find((s) => s.name === urlSchema)
|
||||
if (schema !== undefined) setSelectedSchema(schema.name)
|
||||
}
|
||||
}, [urlSchema, isSuccessSchemas])
|
||||
|
||||
useEffect(() => {
|
||||
if (table !== undefined) setSearch(table)
|
||||
}, [table])
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="pb-8">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div className="space-y-1">
|
||||
<h3 className="text-xl text-foreground">Database Indexes</h3>
|
||||
<div className="text-sm text-foreground-lighter">
|
||||
Improve query performance against your database
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button asChild type="default" icon={<IconExternalLink strokeWidth={1.5} />}>
|
||||
<Link
|
||||
href="https://supabase.com/docs/guides/database/query-optimization"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Documentation
|
||||
</Link>
|
||||
</Button>
|
||||
<Button asChild type="default" icon={<IconExternalLink strokeWidth={1.5} />}>
|
||||
<Link
|
||||
href="https://supabase.com/docs/guides/database/extensions/index_advisor"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Optimization with index_advisor
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
@@ -264,48 +235,41 @@ const Indexes = () => {
|
||||
<CreateIndexSidePanel visible={showCreateIndex} onClose={() => setShowCreateIndex(false)} />
|
||||
|
||||
<ConfirmationModal
|
||||
danger
|
||||
variant="warning"
|
||||
size="medium"
|
||||
loading={isExecuting}
|
||||
visible={selectedIndexToDelete !== undefined}
|
||||
header={
|
||||
title={
|
||||
<>
|
||||
Confirm to delete index <code className="text-sm">{selectedIndexToDelete?.name}</code>
|
||||
</>
|
||||
}
|
||||
buttonLabel="Confirm delete"
|
||||
buttonLoadingLabel="Deleting..."
|
||||
onSelectConfirm={() =>
|
||||
confirmLabel="Confirm delete"
|
||||
confirmLabelLoading="Deleting..."
|
||||
onConfirm={() =>
|
||||
selectedIndexToDelete !== undefined ? onConfirmDeleteIndex(selectedIndexToDelete) : {}
|
||||
}
|
||||
onSelectCancel={() => setSelectedIndexToDelete(undefined)}
|
||||
onCancel={() => setSelectedIndexToDelete(undefined)}
|
||||
alert={{
|
||||
title: 'This action cannot be undone',
|
||||
description:
|
||||
'Deleting an index that is still in use will cause queries to slow down, and in some cases causing significant performance issues.',
|
||||
}}
|
||||
>
|
||||
<Modal.Content>
|
||||
<div className="py-6">
|
||||
<Alert_Shadcn_ variant="warning">
|
||||
<IconAlertTriangle strokeWidth={2} />
|
||||
<AlertTitle_Shadcn_>This action cannot be undone</AlertTitle_Shadcn_>
|
||||
<AlertDescription_Shadcn_>
|
||||
Deleting an index that is still in use will cause queries to slow down, and in some
|
||||
cases causing significant performance issues.
|
||||
</AlertDescription_Shadcn_>
|
||||
</Alert_Shadcn_>
|
||||
<ul className="mt-4 space-y-5">
|
||||
<li className="flex gap-3">
|
||||
<div>
|
||||
<strong className="text-sm">Before deleting this index, consider:</strong>
|
||||
<ul className="space-y-2 mt-2 text-sm text-foreground-light">
|
||||
<li className="list-disc ml-6">This index is no longer in use</li>
|
||||
<li className="list-disc ml-6">
|
||||
The table which the index is on is not currently in use, as dropping an index
|
||||
requires a short exclusive access lock on the table.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</Modal.Content>
|
||||
<ul className="mt-4 space-y-5">
|
||||
<li className="flex gap-3">
|
||||
<div>
|
||||
<strong className="text-sm">Before deleting this index, consider:</strong>
|
||||
<ul className="space-y-2 mt-2 text-sm text-foreground-light">
|
||||
<li className="list-disc ml-6">This index is no longer in use</li>
|
||||
<li className="list-disc ml-6">
|
||||
The table which the index is on is not currently in use, as dropping an index
|
||||
requires a short exclusive access lock on the table.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</ConfirmationModal>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import dayjs from 'dayjs'
|
||||
import Link from 'next/link'
|
||||
import { useState } from 'react'
|
||||
import { Alert, Button, IconExternalLink, IconSearch, Input, SidePanel } from 'ui'
|
||||
import { Alert, Button, IconSearch, Input, SidePanel } from 'ui'
|
||||
|
||||
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
|
||||
import Table from 'components/to-be-cleaned/Table'
|
||||
@@ -28,131 +28,109 @@ const Migrations = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h3 className="mb-1 text-xl text-foreground">Database Migrations</h3>
|
||||
<div className="text-sm text-foreground-lighter">
|
||||
History of migrations that have been run on your database
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button asChild type="default" icon={<IconExternalLink strokeWidth={1.5} />}>
|
||||
<Link
|
||||
href="https://supabase.com/docs/guides/getting-started/local-development#database-migrations"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Documentation
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
{isLoading && (
|
||||
<div className="space-y-2">
|
||||
<ShimmeringLoader />
|
||||
<ShimmeringLoader className="w-3/4" />
|
||||
<ShimmeringLoader className="w-1/2" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isLoading && (
|
||||
<div className="space-y-2">
|
||||
<ShimmeringLoader />
|
||||
<ShimmeringLoader className="w-3/4" />
|
||||
<ShimmeringLoader className="w-1/2" />
|
||||
<div>
|
||||
{isError && (
|
||||
<Alert
|
||||
withIcon
|
||||
variant="warning"
|
||||
title="Failed to retrieve migration history for database"
|
||||
actions={[
|
||||
<Button key="contact-support" asChild type="default" className="ml-4">
|
||||
<Link
|
||||
href={`/support/new?ref=${project?.ref}&category=dashboard_bug&subject=Unable%20to%20view%20database%20migrations`}
|
||||
>
|
||||
Contact support
|
||||
</Link>
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<p className="mb-1">
|
||||
Try refreshing your browser, but if the issue persists, please reach out to us via
|
||||
support.
|
||||
</p>
|
||||
<p>Error: {(error as any)?.message ?? 'Unknown'}</p>
|
||||
</Alert>
|
||||
)}
|
||||
{isSuccess && (
|
||||
<div>
|
||||
{data.result.length <= 0 && <MigrationsEmptyState />}
|
||||
|
||||
{data.result.length > 0 && (
|
||||
<>
|
||||
<div className="w-80 mb-4">
|
||||
<Input
|
||||
size="small"
|
||||
placeholder="Search for a migration"
|
||||
value={search}
|
||||
onChange={(e: any) => setSearch(e.target.value)}
|
||||
icon={<IconSearch size="tiny" />}
|
||||
/>
|
||||
</div>
|
||||
<Table
|
||||
head={[
|
||||
<Table.th key="version" style={{ width: '180px' }}>
|
||||
Version
|
||||
</Table.th>,
|
||||
<Table.th key="version">Name</Table.th>,
|
||||
<Table.th key="version">Inserted at (UTC)</Table.th>,
|
||||
<Table.th key="buttons"></Table.th>,
|
||||
]}
|
||||
body={
|
||||
migrations.length > 0 ? (
|
||||
migrations.map((migration) => {
|
||||
// [Joshen] LEFT OFF HERE
|
||||
const insertedAt = dayjs(migration.version, 'YYYYMMDDHHmmss').format(
|
||||
'DD MMM YYYY, HH:mm:ss'
|
||||
)
|
||||
|
||||
return (
|
||||
<Table.tr key={migration.version}>
|
||||
<Table.td>{migration.version}</Table.td>
|
||||
<Table.td
|
||||
className={
|
||||
(migration?.name ?? '').length === 0
|
||||
? '!text-foreground-lighter'
|
||||
: ''
|
||||
}
|
||||
>
|
||||
{migration?.name ?? 'Name not available'}
|
||||
</Table.td>
|
||||
<Table.td>{insertedAt}</Table.td>
|
||||
<Table.td align="right">
|
||||
<Button
|
||||
type="default"
|
||||
onClick={() => setSelectedMigration(migration)}
|
||||
>
|
||||
View migration SQL
|
||||
</Button>
|
||||
</Table.td>
|
||||
</Table.tr>
|
||||
)
|
||||
})
|
||||
) : (
|
||||
<Table.tr>
|
||||
<Table.td colSpan={3}>
|
||||
<p className="text-sm text-foreground">No results found</p>
|
||||
<p className="text-sm text-foreground-light">
|
||||
Your search for "{search}" did not return any results
|
||||
</p>
|
||||
</Table.td>
|
||||
</Table.tr>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
{isError && (
|
||||
<Alert
|
||||
withIcon
|
||||
variant="warning"
|
||||
title="Failed to retrieve migration history for database"
|
||||
actions={[
|
||||
<Button key="contact-support" asChild type="default" className="ml-4">
|
||||
<Link
|
||||
href={`/support/new?ref=${project?.ref}&category=dashboard_bug&subject=Unable%20to%20view%20database%20migrations`}
|
||||
>
|
||||
Contact support
|
||||
</Link>
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<p className="mb-1">
|
||||
Try refreshing your browser, but if the issue persists, please reach out to us via
|
||||
support.
|
||||
</p>
|
||||
<p>Error: {(error as any)?.message ?? 'Unknown'}</p>
|
||||
</Alert>
|
||||
)}
|
||||
{isSuccess && (
|
||||
<div>
|
||||
{data.result.length <= 0 && <MigrationsEmptyState />}
|
||||
|
||||
{data.result.length > 0 && (
|
||||
<>
|
||||
<div className="w-80 mb-4">
|
||||
<Input
|
||||
size="small"
|
||||
placeholder="Search for a migration"
|
||||
value={search}
|
||||
onChange={(e: any) => setSearch(e.target.value)}
|
||||
icon={<IconSearch size="tiny" />}
|
||||
/>
|
||||
</div>
|
||||
<Table
|
||||
head={[
|
||||
<Table.th key="version" style={{ width: '180px' }}>
|
||||
Version
|
||||
</Table.th>,
|
||||
<Table.th key="version">Name</Table.th>,
|
||||
<Table.th key="version">Inserted at (UTC)</Table.th>,
|
||||
<Table.th key="buttons"></Table.th>,
|
||||
]}
|
||||
body={
|
||||
migrations.length > 0 ? (
|
||||
migrations.map((migration) => {
|
||||
// [Joshen] LEFT OFF HERE
|
||||
const insertedAt = dayjs(migration.version, 'YYYYMMDDHHmmss').format(
|
||||
'DD MMM YYYY, HH:mm:ss'
|
||||
)
|
||||
|
||||
return (
|
||||
<Table.tr key={migration.version}>
|
||||
<Table.td>{migration.version}</Table.td>
|
||||
<Table.td
|
||||
className={
|
||||
(migration?.name ?? '').length === 0
|
||||
? '!text-foreground-lighter'
|
||||
: ''
|
||||
}
|
||||
>
|
||||
{migration?.name ?? 'Name not available'}
|
||||
</Table.td>
|
||||
<Table.td>{insertedAt}</Table.td>
|
||||
<Table.td align="right">
|
||||
<Button
|
||||
type="default"
|
||||
onClick={() => setSelectedMigration(migration)}
|
||||
>
|
||||
View migration SQL
|
||||
</Button>
|
||||
</Table.td>
|
||||
</Table.tr>
|
||||
)
|
||||
})
|
||||
) : (
|
||||
<Table.tr>
|
||||
<Table.td colSpan={3}>
|
||||
<p className="text-sm text-foreground">No results found</p>
|
||||
<p className="text-sm text-foreground-light">
|
||||
Your search for "{search}" did not return any results
|
||||
</p>
|
||||
</Table.td>
|
||||
</Table.tr>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SidePanel
|
||||
|
||||
@@ -173,21 +173,19 @@ const PublicationsList = ({ onSelectPublication = noop }: PublicationsListProps)
|
||||
|
||||
<ConfirmationModal
|
||||
visible={toggleListenEventValue !== null}
|
||||
header={`Confirm to toggle sending ${toggleListenEventValue?.event.event.toLowerCase()} events`}
|
||||
buttonLabel="Confirm"
|
||||
buttonLoadingLabel="Updating"
|
||||
onSelectCancel={() => setToggleListenEventValue(null)}
|
||||
onSelectConfirm={() => {
|
||||
title={`Confirm to toggle sending ${toggleListenEventValue?.event.event.toLowerCase()} events`}
|
||||
confirmLabel="Confirm"
|
||||
confirmLabelLoading="Updating"
|
||||
onCancel={() => setToggleListenEventValue(null)}
|
||||
onConfirm={() => {
|
||||
toggleListenEvent()
|
||||
}}
|
||||
>
|
||||
<Modal.Content>
|
||||
<p className="py-4 text-sm text-foreground-light">
|
||||
Are you sure you want to {toggleListenEventValue?.currentStatus ? 'stop' : 'start'}{' '}
|
||||
sending {toggleListenEventValue?.event.event.toLowerCase()} events for{' '}
|
||||
{toggleListenEventValue?.publication.name}?
|
||||
</p>
|
||||
</Modal.Content>
|
||||
<p className="text-sm text-foreground-light">
|
||||
Are you sure you want to {toggleListenEventValue?.currentStatus ? 'stop' : 'start'}{' '}
|
||||
sending {toggleListenEventValue?.event.event.toLowerCase()} events for{' '}
|
||||
{toggleListenEventValue?.publication.name}?
|
||||
</p>
|
||||
</ConfirmationModal>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import * as Tooltip from '@radix-ui/react-tooltip'
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
import { partition, sortBy } from 'lodash'
|
||||
import { Plus, Search, X } from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
import { Badge, Button, IconPlus, IconSearch, IconX, Input } from 'ui'
|
||||
import { Badge, Button, Input } from 'ui'
|
||||
|
||||
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
|
||||
import { FormHeader } from 'components/ui/Forms'
|
||||
import NoSearchResults from 'components/ui/NoSearchResults'
|
||||
import SparkBar from 'components/ui/SparkBar'
|
||||
import { useDatabaseRolesQuery } from 'data/database-roles/database-roles-query'
|
||||
@@ -59,135 +59,128 @@ const RolesList = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<div className="flex items-center justify-between">
|
||||
<FormHeader
|
||||
title="Database Roles"
|
||||
description="Manage access control to your database through users, groups, and permissions"
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div className="flex items-center space-x-4">
|
||||
<Input
|
||||
size="small"
|
||||
placeholder="Search for a role"
|
||||
icon={<Search size={12} />}
|
||||
value={filterString}
|
||||
onChange={(event: any) => setFilterString(event.target.value)}
|
||||
actions={
|
||||
filterString && (
|
||||
<Button
|
||||
size="tiny"
|
||||
type="text"
|
||||
onClick={() => setFilterString('')}
|
||||
className="px-1 mr-1"
|
||||
>
|
||||
<X size={12} strokeWidth={2} />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div className="flex items-center space-x-4">
|
||||
<Input
|
||||
size="small"
|
||||
placeholder="Search for a role"
|
||||
icon={<IconSearch size="tiny" />}
|
||||
value={filterString}
|
||||
onChange={(event: any) => setFilterString(event.target.value)}
|
||||
actions={
|
||||
filterString && (
|
||||
<Button
|
||||
size="tiny"
|
||||
type="text"
|
||||
onClick={() => setFilterString('')}
|
||||
className="px-1 mr-1"
|
||||
>
|
||||
<IconX size={12} strokeWidth={2} />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
/>
|
||||
<div className="flex items-center border border-strong rounded-full w-min h-[34px]">
|
||||
<button
|
||||
className={[
|
||||
'text-xs w-[90px] h-full text-center rounded-l-full flex items-center justify-center transition',
|
||||
filterType === 'all'
|
||||
? 'bg-overlay-hover text-foreground'
|
||||
: 'hover:bg-surface-200 text-foreground-light',
|
||||
].join(' ')}
|
||||
onClick={() => setFilterType('all')}
|
||||
>
|
||||
All roles
|
||||
</button>
|
||||
<div className="h-full w-[1px] border-r border-strong"></div>
|
||||
<button
|
||||
className={[
|
||||
'text-xs w-[90px] h-full text-center rounded-r-full flex items-center justify-center transition',
|
||||
filterType === 'active'
|
||||
? 'bg-overlay-hover text-foreground'
|
||||
: 'hover:bg-surface-200 text-foreground-light',
|
||||
].join(' ')}
|
||||
onClick={() => setFilterType('active')}
|
||||
>
|
||||
Active roles
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center border border-strong rounded-full w-min h-[34px]">
|
||||
<button
|
||||
className={[
|
||||
'text-xs w-[90px] h-full text-center rounded-l-full flex items-center justify-center transition',
|
||||
filterType === 'all'
|
||||
? 'bg-overlay-hover text-foreground'
|
||||
: 'hover:bg-surface-200 text-foreground-light',
|
||||
].join(' ')}
|
||||
onClick={() => setFilterType('all')}
|
||||
>
|
||||
All roles
|
||||
</button>
|
||||
<div className="h-full w-[1px] border-r border-strong"></div>
|
||||
<button
|
||||
className={[
|
||||
'text-xs w-[90px] h-full text-center rounded-r-full flex items-center justify-center transition',
|
||||
filterType === 'active'
|
||||
? 'bg-overlay-hover text-foreground'
|
||||
: 'hover:bg-surface-200 text-foreground-light',
|
||||
].join(' ')}
|
||||
onClick={() => setFilterType('active')}
|
||||
>
|
||||
Active roles
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center space-x-6">
|
||||
<Tooltip.Root delayDuration={0}>
|
||||
<Tooltip.Trigger>
|
||||
<div className="w-42">
|
||||
<SparkBar
|
||||
type="horizontal"
|
||||
// if the maxConnectionLimit is undefined, set totalActiveConnections so that
|
||||
// the width of the bar is set to 100%
|
||||
max={maxConnectionLimit || totalActiveConnections}
|
||||
value={totalActiveConnections}
|
||||
barClass={
|
||||
maxConnectionLimit === 0 || maxConnectionLimit === undefined
|
||||
? 'bg-control'
|
||||
: totalActiveConnections > 0.9 * maxConnectionLimit
|
||||
? 'bg-red-800'
|
||||
: totalActiveConnections > 0.75 * maxConnectionLimit
|
||||
? 'bg-amber-900'
|
||||
: 'bg-green-800'
|
||||
}
|
||||
labelTop={
|
||||
Number.isInteger(maxConnectionLimit)
|
||||
? `${totalActiveConnections}/${maxConnectionLimit}`
|
||||
: `${totalActiveConnections}`
|
||||
}
|
||||
labelBottom="Active connections"
|
||||
/>
|
||||
</div>
|
||||
</Tooltip.Trigger>
|
||||
</div>
|
||||
<div className="flex items-center space-x-6">
|
||||
<Tooltip.Root delayDuration={0}>
|
||||
<Tooltip.Trigger>
|
||||
<div className="w-42">
|
||||
<SparkBar
|
||||
type="horizontal"
|
||||
// if the maxConnectionLimit is undefined, set totalActiveConnections so that
|
||||
// the width of the bar is set to 100%
|
||||
max={maxConnectionLimit || totalActiveConnections}
|
||||
value={totalActiveConnections}
|
||||
barClass={
|
||||
maxConnectionLimit === 0 || maxConnectionLimit === undefined
|
||||
? 'bg-foreground'
|
||||
: totalActiveConnections > 0.9 * maxConnectionLimit
|
||||
? 'bg-destructive'
|
||||
: totalActiveConnections > 0.75 * maxConnectionLimit
|
||||
? 'bg-warning'
|
||||
: undefined
|
||||
}
|
||||
labelTop={
|
||||
Number.isInteger(maxConnectionLimit)
|
||||
? `${totalActiveConnections}/${maxConnectionLimit}`
|
||||
: `${totalActiveConnections}`
|
||||
}
|
||||
labelBottom="Active connections"
|
||||
/>
|
||||
</div>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content align="start" side="bottom">
|
||||
<Tooltip.Arrow className="radix-tooltip-arrow" />
|
||||
<div
|
||||
className={[
|
||||
'rounded bg-alternative py-1 px-2 leading-none shadow',
|
||||
'border border-background space-y-1',
|
||||
].join(' ')}
|
||||
>
|
||||
<p className="text-xs text-foreground-light pr-2">Connections by roles:</p>
|
||||
{rolesWithActiveConnections.map((role) => (
|
||||
<div key={role.id} className="text-xs text-foreground">
|
||||
{role.name}: {role.active_connections}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
<Tooltip.Root delayDuration={0}>
|
||||
<Tooltip.Trigger asChild>
|
||||
<Button
|
||||
type="primary"
|
||||
disabled={!canUpdateRoles}
|
||||
icon={<Plus size={12} />}
|
||||
onClick={() => setIsCreatingRole(true)}
|
||||
>
|
||||
Add role
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
{!canUpdateRoles && (
|
||||
<Tooltip.Content align="start" side="bottom">
|
||||
<Tooltip.Arrow className="radix-tooltip-arrow" />
|
||||
<div
|
||||
className={[
|
||||
'rounded bg-alternative py-1 px-2 leading-none shadow',
|
||||
'border border-background space-y-1',
|
||||
'border border-background text-xs',
|
||||
].join(' ')}
|
||||
>
|
||||
<p className="text-xs text-foreground-light pr-2">Connections by roles:</p>
|
||||
{rolesWithActiveConnections.map((role) => (
|
||||
<div key={role.id} className="text-xs text-foreground">
|
||||
{role.name}: {role.active_connections}
|
||||
</div>
|
||||
))}
|
||||
You need additional permissions to add a new role
|
||||
</div>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
<Tooltip.Root delayDuration={0}>
|
||||
<Tooltip.Trigger asChild>
|
||||
<Button
|
||||
type="primary"
|
||||
disabled={!canUpdateRoles}
|
||||
icon={<IconPlus size="tiny" />}
|
||||
onClick={() => setIsCreatingRole(true)}
|
||||
>
|
||||
Add role
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
{!canUpdateRoles && (
|
||||
<Tooltip.Content align="start" side="bottom">
|
||||
<Tooltip.Arrow className="radix-tooltip-arrow" />
|
||||
<div
|
||||
className={[
|
||||
'rounded bg-alternative py-1 px-2 leading-none shadow',
|
||||
'border border-background text-xs',
|
||||
].join(' ')}
|
||||
>
|
||||
You need additional permissions to add a new role
|
||||
</div>
|
||||
</Tooltip.Content>
|
||||
)}
|
||||
</Tooltip.Root>
|
||||
</div>
|
||||
)}
|
||||
</Tooltip.Root>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-4">
|
||||
{supabaseRoles.length > 0 && (
|
||||
<div>
|
||||
<div className="bg-surface-100 border border-default px-6 py-3 rounded-t flex items-center space-x-4">
|
||||
<p className="text-sm text-foreground-light">Roles managed by Supabase</p>
|
||||
@@ -196,7 +189,7 @@ const RolesList = () => {
|
||||
|
||||
{isLoading
|
||||
? Array.from({ length: 5 }).map((_, i) => <RoleRowSkeleton key={i} index={i} />)
|
||||
: supabaseRoles.map((role, i: number) => (
|
||||
: supabaseRoles.map((role) => (
|
||||
<RoleRow
|
||||
disabled
|
||||
key={role.id}
|
||||
@@ -205,7 +198,9 @@ const RolesList = () => {
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{otherRoles.length > 0 && (
|
||||
<div>
|
||||
<div className="bg-surface-100 border border-default px-6 py-3 rounded-t">
|
||||
<p className="text-sm text-foreground-light">Other database roles</p>
|
||||
@@ -213,7 +208,7 @@ const RolesList = () => {
|
||||
|
||||
{isLoading
|
||||
? Array.from({ length: 3 }).map((_, i) => <RoleRowSkeleton key={i} index={i} />)
|
||||
: otherRoles.map((role, i: number) => (
|
||||
: otherRoles.map((role) => (
|
||||
<RoleRow
|
||||
key={role.id}
|
||||
disabled={!canUpdateRoles}
|
||||
@@ -222,13 +217,13 @@ const RolesList = () => {
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{filterString.length > 0 && filteredRoles.length === 0 && (
|
||||
<NoSearchResults searchString={filterString} onResetFilter={() => setFilterString('')} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{filterString.length > 0 && filteredRoles.length === 0 && (
|
||||
<NoSearchResults searchString={filterString} onResetFilter={() => setFilterString('')} />
|
||||
)}
|
||||
|
||||
<CreateRolePanel visible={isCreatingRole} onClose={() => setIsCreatingRole(false)} />
|
||||
|
||||
<DeleteRoleModal
|
||||
|
||||
Loaded 100 of 561 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user