Merge remote-tracking branch 'supabase/master'

# Conflicts:
#	apps/docs/components/Navigation/NavigationMenu/HomeMenu.tsx
This commit is contained in:
Jan Tennert committed 2023-05-10 14:13:13 +02:00
commit d7332bd938
87 files changed
+2011 -463

No files matched your search

@@ -22,3 +22,14 @@ export interface References {
currentVersion?: string
}
}
type MenuItem = {
label: string
icon?: string
href?: string
level?: string
hasLightIcon?: boolean
community?: boolean
}
export type HomepageMenuItems = MenuItem[][]
@@ -4,153 +4,11 @@ import Link from 'next/link'
import { useRouter } from 'next/router'
import { Fragment } from 'react'
import { Badge } from '~/../../packages/ui'
const home = [
[
{
label: 'Home',
icon: '/img/icons/menu/home',
href: '/',
level: 'home',
},
{
label: 'Getting Started',
icon: '/img/icons/menu/getting-started',
href: '/guides/getting-started',
level: 'gettingstarted',
},
],
[
{
label: 'Database',
icon: '/img/icons/menu/database',
href: '/guides/database',
level: 'database',
},
{
label: 'Serverless APIs',
icon: '/img/icons/menu/reference',
href: '/guides/api',
level: 'api',
},
{
label: 'Auth',
icon: '/img/icons/menu/auth',
href: '/guides/auth',
level: 'auth',
},
{
label: 'Edge Functions',
icon: '/img/icons/menu/functions',
href: '/guides/functions',
level: 'functions',
},
{
label: 'Realtime',
icon: '/img/icons/menu/realtime',
href: '/guides/realtime',
level: 'realtime',
},
{
label: 'Storage',
icon: '/img/icons/menu/storage',
href: '/guides/storage',
level: 'storage',
},
],
[
{
label: 'Platform',
icon: '/img/icons/menu/platform',
href: '/guides/platform',
level: 'platform',
},
{
label: 'Resources',
icon: '/img/icons/menu/platform',
href: '/guides/resources',
level: 'resources',
},
{
label: 'Self-Hosting',
icon: '/img/icons/menu/platform',
href: '/guides/self-hosting',
level: 'self_hosting',
},
{
label: 'Integrations',
icon: '/img/icons/menu/integrations',
hasLightIcon: true,
href: '/guides/integrations',
level: 'integrations',
},
],
[
{
label: 'Client Library Reference',
},
{
label: 'JavaScript',
icon: '/img/icons/menu/reference-javascript',
href: '/reference/javascript/introduction',
level: 'reference_javascript',
},
{
label: 'Flutter',
icon: '/img/icons/menu/reference-dart',
href: '/reference/dart/introduction',
level: 'reference_dart',
},
{
label: 'Python',
icon: '/img/icons/menu/reference-python',
href: '/reference/python/introduction',
level: 'reference_python',
community: true,
},
{
label: 'C#',
icon: '/img/icons/menu/reference-csharp',
href: '/reference/csharp/introduction',
level: 'reference_csharp',
community: true,
},
{
label: 'Swift',
icon: '/img/icons/menu/reference-swift',
href: '/reference/swift/introduction',
level: 'reference_swift',
community: true,
},
{
label: 'Kotlin',
icon: '/img/icons/menu/reference-kotlin',
href: '/reference/kotlin/introduction',
level: 'reference_kotlin',
community: true,
},
{
label: 'Tools',
},
{
label: 'Management API',
icon: '/img/icons/menu/reference-api',
href: '/reference/api/introduction',
level: 'reference_javascript',
},
{
label: 'Supabase CLI',
icon: '/img/icons/menu/reference-cli',
href: '/guides/cli',
level: 'reference_javascript',
},
],
]
import { cn } from 'ui/src/utils/cn'
import { HOMEPAGE_MENU_ITEMS } from './NavigationMenu.constants'
import HomeMenuIconPicker from './HomeMenuIconPicker'
const NavigationMenuHome = ({ active }) => {
const router = useRouter()
const { isDarkMode } = useTheme()
return (
<div
className={[
@@ -158,8 +16,8 @@ const NavigationMenuHome = ({ active }) => {
active ? 'opacity-100 ml-0 delay-150' : 'opacity-0 -ml-8 invisible absolute',
].join(' ')}
>
<ul className="relative w-full flex flex-col gap-4">
{home.map((section, sectionIndex) => {
<ul className="relative w-full flex flex-col gap-6">
{HOMEPAGE_MENU_ITEMS.map((section, sectionIndex) => {
return (
<Fragment key={`section-container-${sectionIndex}-border`}>
{sectionIndex !== 0 && (
@@ -169,16 +27,19 @@ const NavigationMenuHome = ({ active }) => {
></div>
)}
<div key={`section-${sectionIndex}`}>
<div className="flex flex-col gap-3">
{section.map((link) => {
<div className="flex flex-col gap-4">
{section.map((link, i) => {
if (!link.href) {
return (
<span
<div
key={link.label}
className="font-mono uppercase text-xs text-scale-900"
className={cn(
'font-mono uppercase text-xs text-scale-900 ',
i !== 0 && 'mt-4'
)}
>
{link.label}
</span>
</div>
)
} else {
return (
@@ -186,19 +47,11 @@ const NavigationMenuHome = ({ active }) => {
<a>
<li
className={[
'group flex items-center gap-3',
'text-base transition-all duration-150 text-scale-1200 hover:text-brand-900 hover:cursor-pointer ',
'group flex items-center gap-2',
'text-sm transition-all duration-150 text-scale-1100 hover:text-scale-1200 hover:cursor-pointer ',
].join(' ')}
>
<Image
alt={link.label}
src={`${router.basePath}${
isDarkMode ? link.icon : `${link.icon}-light`
}${!link.icon.includes('png') ? '.svg' : ''}`}
width={17}
height={17}
className="w-4 h-4 group-hover:scale-110 ease-out transition-all"
/>
{link?.icon && <HomeMenuIconPicker icon={link.icon} />}
{link.label}
{link.community && <Badge size="small">Community</Badge>}
</li>
@@ -0,0 +1,84 @@
import React from 'react'
import {
IconMenuApi,
IconMenuAuth,
IconMenuCli,
IconMenuCsharp,
IconMenuDatabase,
IconMenuEdgeFunctions,
IconMenuFlutter,
IconMenuGettingStarted,
IconMenuHome,
IconMenuIntegrations,
IconMenuJavascript,
IconMenuPlatform,
IconMenuPython,
IconMenuRealtime,
IconMenuResources,
IconMenuSelfHosting,
IconMenuServerlessApis,
IconMenuStorage,
IconMenuSwift,
IconMenuStatus,
} from './HomeMenuIcons'
function getMenuIcon(menuKey: string, width: number = 16, height: number = 16) {
switch (menuKey) {
case 'home':
return <IconMenuHome width={width} height={height} />
case 'getting-started':
return <IconMenuGettingStarted width={width} height={height} />
case 'database':
return <IconMenuDatabase width={width} height={height} />
case 'serverless-apis':
return <IconMenuServerlessApis width={width} height={height} />
case 'auth':
return <IconMenuAuth width={width} height={height} />
case 'edge-functions':
return <IconMenuEdgeFunctions width={width} height={height} />
case 'realtime':
return <IconMenuRealtime width={width} height={height} />
case 'storage':
return <IconMenuStorage width={width} height={height} />
case 'platform':
return <IconMenuPlatform width={width} height={height} />
case 'resources':
return <IconMenuResources width={width} height={height} />
case 'self-hosting':
return <IconMenuSelfHosting width={width} height={height} />
case 'integrations':
return <IconMenuIntegrations width={width} height={height} />
case 'reference-javascript':
return <IconMenuJavascript width={width} height={height} />
case 'reference-dart':
return <IconMenuFlutter width={width} height={height} />
case 'reference-python':
return <IconMenuPython width={width} height={height} />
case 'reference-csharp':
return <IconMenuCsharp width={width} height={height} />
case 'reference-swift':
return <IconMenuSwift width={width} height={height} />
case 'reference-api':
return <IconMenuApi width={width} height={height} />
case 'reference-cli':
return <IconMenuCli width={width} height={height} />
case 'status':
return <IconMenuStatus width={width} height={height} />
default:
return <IconMenuPlatform width={width} height={height} />
}
}
type HomeMenuIconPickerProps = {
icon: string
width?: number
height?: number
}
export default function HomeMenuIconPicker({
icon,
width = 16,
height = 16,
}: HomeMenuIconPickerProps) {
return getMenuIcon(icon, width, height)
}
@@ -0,0 +1,402 @@
type HomeMenuIcon = {
width?: number
height?: number
}
export function IconMenuHome({ width = 16, height = 16 }: HomeMenuIcon) {
return (
<svg
viewBox="0 0 16 16"
width={width}
height={height}
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M5.889 13.83V8h4.222v5.83M3 5.494v8.338h10V5.54L7.994 2.02 3 5.493Z"
stroke="currentColor"
strokeMiterlimit="10"
strokeLinejoin="bevel"
/>
</svg>
)
}
export function IconMenuApi({ width = 16, height = 16 }: HomeMenuIcon) {
return (
<svg
viewBox="0 0 16 16"
width={width}
height={height}
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M2.923 10.927A5.833 5.833 0 0 1 2.14 8m2.877-5.046A5.833 5.833 0 0 1 8 2.14m5.13 3.025c.465.84.73 1.807.73 2.835m-2.883 5.049A5.832 5.832 0 0 1 8 13.859m6.172-10.027a2 2 0 1 1-4 0 2 2 0 0 1 4 0ZM5.81 12.148a2 2 0 1 1-4 0 2 2 0 0 1 4 0Zm8.362 0a2 2 0 1 1-4 0 2 2 0 0 1 4 0ZM5.81 3.832a2 2 0 1 1-4 0 2 2 0 0 1 4 0Z"
stroke="currentColor"
strokeMiterlimit="10"
strokeLinejoin="bevel"
/>
</svg>
)
}
export function IconMenuAuth({ width = 16, height = 16 }: HomeMenuIcon) {
return (
<svg
viewBox="0 0 16 16"
width={width}
height={height}
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M3.494 9.975h5m-5 0v2h5v-2m-5 0v-2h5v2M10 5V3a2 2 0 1 0-4 0v2m6.473 0v9h-9V5h9Z"
stroke="currentColor"
strokeMiterlimit="10"
strokeLinejoin="bevel"
/>
</svg>
)
}
export function IconMenuCli({ width = 16, height = 16 }: HomeMenuIcon) {
return (
<svg
viewBox="0 0 16 16"
width={width}
height={height}
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="m5.266 5.552 2.4 2.401-2.4 2.402M8 10.149h3M2 2h12v12H2V2Z"
stroke="currentColor"
strokeMiterlimit="10"
strokeLinejoin="bevel"
/>
</svg>
)
}
export function IconMenuCsharp({ width = 16, height = 16 }: HomeMenuIcon) {
return (
<svg
viewBox="0 0 16 16"
width={width}
height={height}
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="m2.242 11.3.004-.003a1.191 1.191 0 0 1-.134-.57V5.27c0-.442.185-.784.57-1.005.444-.259 1.393-.804 2.33-1.343 1.019-.587 2.024-1.165 2.35-1.356.42-.247.826-.254 1.25-.01l2.37 1.364 2.336 1.345c.189.109.33.247.425.414.098.167.145.366.145.587V10.73c-.001.216-.048.407-.138.573a1.089 1.089 0 0 1-.432.428l-1.786 1.028c-.973.56-1.947 1.12-2.92 1.682-.388.228-.767.236-1.155.042a1.65 1.65 0 0 1-.103-.056c-.382-.227-1.702-.986-2.881-1.664-.747-.429-1.437-.826-1.799-1.036a1.137 1.137 0 0 1-.432-.428Zm7.452-2.351a1.95 1.95 0 0 1-1.698.994 1.94 1.94 0 0 1-1.69-.983A1.946 1.946 0 0 1 9.68 7.02l1.7-.98a3.91 3.91 0 1 0-3.385 5.866 3.913 3.913 0 0 0 3.4-1.974l-1.701-.983Zm2.151-1.88h-.388v.316h-.312v.388h.312v.468h-.312v.388h.312v.316h.388v-.316h.472v.316h.388v-.316h.316v-.388h-.316v-.468h.316v-.388h-.316V7.07h-.388v.316h-.472V7.07Zm0 1.172v-.468h.472v.468h-.472Z"
fill="currentColor"
/>
</svg>
)
}
export function IconMenuDatabase({ width = 16, height = 16 }: HomeMenuIcon) {
return (
<svg
viewBox="0 0 16 16"
width={width}
height={height}
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M3.502 6h8.996v4H3.502V6ZM3 10.002h10v4H3v-4ZM3 2h10v4H3V2Z"
stroke="currentColor"
strokeMiterlimit="10"
strokeLinejoin="bevel"
/>
</svg>
)
}
export function IconMenuServerlessApis({ width = 16, height = 16 }: HomeMenuIcon) {
return (
<svg
viewBox="0 0 16 16"
width={width}
height={height}
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M1.923 9.927A5.833 5.833 0 0 1 1.14 7m2.877-5.046A5.833 5.833 0 0 1 7 1.14m5.13 3.025c.465.84.73 1.807.73 2.835m-2.883 5.049A5.832 5.832 0 0 1 7 12.859m6.172-10.027a2 2 0 1 1-4 0 2 2 0 0 1 4 0ZM4.81 11.148a2 2 0 1 1-4 0 2 2 0 0 1 4 0Zm8.362 0a2 2 0 1 1-4 0 2 2 0 0 1 4 0ZM4.81 2.832a2 2 0 1 1-4 0 2 2 0 0 1 4 0Z"
stroke="currentColor"
strokeMiterlimit="10"
strokeLinejoin="bevel"
/>
</svg>
)
}
export function IconMenuEdgeFunctions({ width = 16, height = 16 }: HomeMenuIcon) {
return (
<svg
viewBox="0 0 16 16"
width={width}
height={height}
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M1.857 11.36a7 7 0 0 1 9.41-9.551M4.774 14.212a7 7 0 0 0 9.41-9.497m-8.812 7.845a2 2 0 1 1-4 0 2 2 0 0 1 4 0Zm9.296-9.13a2 2 0 1 1-4 0 2 2 0 0 1 4 0ZM12.5 8a4.5 4.5 0 1 1-9 0 4.5 4.5 0 0 1 9 0Z"
stroke="currentColor"
strokeMiterlimit="10"
strokeLinejoin="bevel"
/>
</svg>
)
}
export function IconMenuExtensions({ width = 16, height = 16 }: HomeMenuIcon) {
return (
<svg
viewBox="0 0 16 16"
width={width}
height={height}
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M11.418 2.098v5m-2.5-2.5h5m-11.836-2.5h5v5h-5v-5Zm5.185 9.145a2.685 2.685 0 1 1-5.37 0 2.685 2.685 0 0 1 5.37 0Zm1.651-2.396h5v5h-5v-5Z"
stroke="currentColor"
strokeMiterlimit="10"
strokeLinejoin="bevel"
/>
</svg>
)
}
export function IconMenuFlutter({ width = 16, height = 16 }: HomeMenuIcon) {
return (
<svg
viewBox="0 0 16 16"
width={width}
height={height}
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="m11.857 2-3.718.004L2.143 8l1.85 1.852 1.626-1.617L11.857 2ZM8.245 7.531c-.052-.002-.107-.005-.14.04l-3.198 3.197 1.836 1.825-.002.002 1.315 1.316a.549.549 0 0 1 .026.025c.035.036.074.074.13.062.607-.002 1.214-.002 1.821-.001l1.822-.001-3.232-3.235 3.23-3.23H8.31a.39.39 0 0 1-.064 0Z"
fill="currentColor"
/>
</svg>
)
}
export function IconMenuGettingStarted({ width = 16, height = 16 }: HomeMenuIcon) {
return (
<svg
viewBox="0 0 16 16"
width={width}
height={height}
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="m13.429 8-4.681 2.702-4.681 2.703V2.594l4.68 2.703 4.682 2.702Z"
stroke="currentColor"
strokeMiterlimit="10"
strokeLinejoin="bevel"
/>
</svg>
)
}
export function IconMenuIntegrations({ width = 16, height = 16 }: HomeMenuIcon) {
return (
<svg
viewBox="0 0 16 16"
width={width}
height={height}
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M11.83 1.453v5.619m-2.809-2.81h5.62M1.338 1.454h5.62v5.619h-5.62V1.453Zm5.827 10.278a3.017 3.017 0 1 1-6.035 0 3.017 3.017 0 0 1 6.035 0ZM9.02 9.038h5.62v5.619H9.02V9.038Z"
stroke="currentColor"
strokeMiterlimit="10"
strokeLinejoin="bevel"
/>
</svg>
)
}
export function IconMenuJavascript({ width = 16, height = 16 }: HomeMenuIcon) {
return (
<svg
width={width}
height={height}
fill="none"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M2 2h12v12H2V2Zm9.173 10.06c-.556 0-.87-.29-1.112-.685l-.916.533c.33.654 1.007 1.153 2.054 1.153 1.071 0 1.869-.556 1.869-1.572 0-.941-.541-1.36-1.5-1.771l-.281-.12c-.484-.21-.693-.347-.693-.686 0-.273.209-.483.54-.483.323 0 .532.137.725.483l.878-.563c-.371-.654-.887-.903-1.604-.903-1.007 0-1.651.644-1.651 1.49 0 .917.54 1.352 1.354 1.698l.282.121c.514.225.821.362.821.749 0 .322-.299.556-.766.556Zm-4.37-.007c-.387 0-.548-.266-.726-.58l-.917.556c.265.562.788 1.03 1.691 1.03 1 0 1.684-.532 1.684-1.7V7.51H7.407v3.834c0 .564-.233.709-.604.709Z"
fill="currentColor"
/>
</svg>
)
}
export function IconMenuPlatform({ width = 16, height = 16 }: HomeMenuIcon) {
return (
<svg
viewBox="0 0 16 16"
width={width}
height={height}
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M11.556 5.554 13.77 7.14l-5.772 4.04-5.737-4.04 2.199-1.587M3.313 7.888.813 9.72l6.996 4.926 7.039-4.926-2.33-1.682M3.285 4.74 8 8.061l4.742-3.32L8 1.338 3.286 4.74Z"
stroke="currentColor"
strokeMiterlimit="10"
strokeLinejoin="bevel"
/>
</svg>
)
}
export function IconMenuPython({ width = 16, height = 16 }: HomeMenuIcon) {
return (
<svg
viewBox="0 0 16 16"
width={width}
height={height}
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M6.424 1.627A9.05 9.05 0 0 1 7.923 1.5a9.885 9.885 0 0 1 1.633.127c.851.14 1.568.771 1.568 1.612v2.953a1.57 1.57 0 0 1-1.568 1.576H6.424c-1.063 0-1.959.903-1.959 1.927v1.416H3.387c-.911 0-1.443-.654-1.666-1.572-.3-1.233-.288-1.97 0-3.152.25-1.03 1.047-1.572 1.959-1.572h4.312V4.42H4.856V3.239c0-.895.241-1.38 1.568-1.612Zm.391 1.417a.592.592 0 0 0-.588-.593.59.59 0 0 0 0 1.182.59.59 0 0 0 .588-.59Zm4.7 3.148V4.815h1.177c.912 0 1.342.675 1.568 1.572.313 1.246.327 2.18 0 3.152-.317.944-.657 1.572-1.568 1.572h-4.7v.394h3.132v1.182c0 .896-.778 1.35-1.568 1.577-1.187.34-2.14.288-3.132 0-.829-.242-1.568-.736-1.568-1.576V9.733c0-.85.71-1.576 1.568-1.576h3.132c1.044 0 1.96-.898 1.96-1.966Zm-1.173 6.69a.589.589 0 1 0-1.177 0c0 .328.265.594.589.594a.59.59 0 0 0 .588-.593Z"
fill="currentColor"
/>
</svg>
)
}
export function IconMenuRealtime({ width = 16, height = 16 }: HomeMenuIcon) {
return (
<svg
viewBox="0 0 16 16"
width={width}
height={height}
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M5.362.984v2.35m-1.866.144L1.365 1.282m2.052 3.92H1.052m8.023 9.653L4.557 4.523 15 9.115l-4.748 1.182-1.177 4.558Z"
stroke="currentColor"
strokeMiterlimit="10"
strokeLinejoin="bevel"
/>
</svg>
)
}
export function IconMenuResources({ width = 16, height = 16 }: HomeMenuIcon) {
return (
<svg
viewBox="0 0 16 16"
width={width}
height={height}
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M13 12V5l-.009-3H3v10.5m0 0A1.5 1.5 0 0 0 4.5 14H13v-1.65L12.991 11H4.5A1.5 1.5 0 0 0 3 12.5ZM5.005 2v4l1.5-1.695L8.005 6V2h-3Z"
stroke="currentColor"
strokeMiterlimit="10"
strokeLinejoin="bevel"
/>
</svg>
)
}
export function IconMenuSelfHosting({ width = 16, height = 16 }: HomeMenuIcon) {
return (
<svg
viewBox="0 0 16 16"
width={width}
height={height}
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M13.041 14.37v-2.285a3.245 3.245 0 0 0-3.244-3.244M7.317 10H3v4h3.552v-1.916a3.257 3.257 0 0 1 3.245-3.244m0 0a2 2 0 1 0 0-4 2 2 0 0 0 0 4ZM8 6H3V2h10v4h-1.43M3.502 6v4"
stroke="currentColor"
strokeMiterlimit="10"
strokeLinejoin="bevel"
/>
</svg>
)
}
export function IconMenuStorage({ width = 16, height = 16 }: HomeMenuIcon) {
return (
<svg
viewBox="0 0 16 16"
width={width}
height={height}
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M13 7.616V5.6l-3.618-3.6H3v4.03m9.964-.447L9.38 2v3.584h3.584ZM1.974 6v8h12V7.509h-7.59l-1.533-1.51H1.974Z"
stroke="currentColor"
strokeMiterlimit="10"
strokeLinejoin="bevel"
/>
</svg>
)
}
export function IconMenuSwift({ width = 16, height = 16 }: HomeMenuIcon) {
return (
<svg
viewBox="0 0 16 16"
width={width}
height={height}
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M14.364 4.774c.004.128.005.256.006.384l.001.439v5.245c-.002.128-.003.256-.007.384-.007.28-.024.561-.073.837-.05.28-.133.541-.263.796a2.673 2.673 0 0 1-1.17 1.17c-.254.13-.514.211-.795.262-.276.05-.558.066-.837.073-.128.004-.256.005-.384.006l-.408.001H5.565l-.407-.001c-.128 0-.256-.002-.384-.006a5.571 5.571 0 0 1-.837-.073c-.28-.05-.541-.133-.796-.262a2.674 2.674 0 0 1-1.17-1.17 2.824 2.824 0 0 1-.262-.796 5.582 5.582 0 0 1-.073-.837 18.375 18.375 0 0 1-.006-.384l-.001-.404V5.158c.002-.128.003-.256.007-.384.007-.28.024-.561.073-.837.05-.28.133-.541.262-.796a2.673 2.673 0 0 1 1.362-1.258c.194-.08.393-.136.604-.174.207-.037.417-.056.627-.066.07-.003.14-.006.21-.007.128-.004.256-.005.384-.006l.457-.001H10.842l.384.006c.28.008.561.024.837.074.28.05.541.133.796.262a2.671 2.671 0 0 1 1.17 1.17c.13.255.212.515.262.796.05.276.066.558.073.837Zm-2.64 4.72h.002c1.094 1.347.797 2.791.656 2.519-.572-1.114-1.638-.83-2.178-.55-.044.028-.09.053-.136.078l-.008.004a.306.306 0 0 0-.01.006l.002-.002c-1.124.597-2.632.642-4.149-.01A6.673 6.673 0 0 1 2.908 8.97c.345.255.718.48 1.114.665 1.603.75 3.213.697 4.352 0C6.753 8.386 5.4 6.762 4.361 5.446a5.644 5.644 0 0 1-.534-.736C5.07 5.85 7.033 7.277 7.737 7.672c-1.494-1.58-2.812-3.525-2.75-3.462 2.355 2.372 4.527 3.714 4.527 3.714.082.045.143.081.192.113.044-.114.084-.232.118-.355.376-1.374-.047-2.946-1.004-4.243 2.184 1.311 3.474 3.802 2.946 5.91a75.282 75.282 0 0 0-.041.145Z"
fill="currentColor"
/>
</svg>
)
}
export function IconMenuStatus({ width = 16, height = 16 }: HomeMenuIcon) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
width={width}
height={height}
fill="none"
>
<path
stroke="currentColor"
strokeLinejoin="bevel"
strokeMiterlimit="10"
d="M8.99 1.133a7.866 7.866 0 1 0 6.133 2.94l-6.77 6.77-1.956-1.958m2.67-4.688a4.789 4.789 0 1 0 3.98 2.124"
/>
</svg>
)
}
@@ -1,4 +1,146 @@
import { References } from '../Navigation.types'
import { References, HomepageMenuItems } from '../Navigation.types'
export const HOMEPAGE_MENU_ITEMS: HomepageMenuItems = [
[
{
label: 'Home',
icon: 'home',
href: '/',
level: 'home',
},
{
label: 'Getting Started',
icon: 'getting-started',
href: '/guides/getting-started',
level: 'gettingstarted',
},
],
[
{
label: 'Database',
icon: 'database',
href: '/guides/database',
level: 'database',
},
{
label: 'Serverless APIs',
icon: 'serverless-apis',
href: '/guides/api',
level: 'api',
},
{
label: 'Auth',
icon: 'auth',
href: '/guides/auth',
level: 'auth',
},
{
label: 'Edge Functions',
icon: 'edge-functions',
href: '/guides/functions',
level: 'functions',
},
{
label: 'Realtime',
icon: 'realtime',
href: '/guides/realtime',
level: 'realtime',
},
{
label: 'Storage',
icon: 'storage',
href: '/guides/storage',
level: 'storage',
},
],
[
{
label: 'Platform',
icon: 'platform',
href: '/guides/platform',
level: 'platform',
},
{
label: 'Resources',
icon: 'resources',
href: '/guides/resources',
level: 'resources',
},
{
label: 'Self-Hosting',
icon: 'self-hosting',
href: '/guides/self-hosting',
level: 'self_hosting',
},
{
label: 'Integrations',
icon: 'integrations',
hasLightIcon: true,
href: '/guides/integrations',
level: 'integrations',
},
],
[
{
label: 'Client Library Reference',
},
{
label: 'JavaScript',
icon: 'reference-javascript',
href: '/reference/javascript/introduction',
level: 'reference_javascript',
},
{
label: 'Flutter',
icon: 'reference-dart',
href: '/reference/dart/introduction',
level: 'reference_dart',
},
{
label: 'Python',
icon: 'reference-python',
href: '/reference/python/introduction',
level: 'reference_python',
community: true,
},
{
label: 'C#',
icon: 'reference-csharp',
href: '/reference/csharp/introduction',
level: 'reference_csharp',
community: true,
},
{
label: 'Swift',
icon: 'reference-swift',
href: '/reference/swift/introduction',
level: 'reference_swift',
community: true,
},
{
label: 'Tools reference',
},
{
label: 'Management API',
icon: 'reference-api',
href: '/reference/api/introduction',
level: 'reference_javascript',
},
{
label: 'Supabase CLI',
icon: 'reference-cli',
href: '/guides/cli',
level: 'reference_javascript',
},
],
[
{
label: 'Status',
icon: 'status',
href: 'https://status.supabase.com/',
},
],
]
export const REFERENCES: References = {
javascript: {
@@ -301,6 +443,7 @@ export const auth = {
},
],
},
{ name: 'Password Reset', url: '/guides/auth/auth-password-reset' },
{ name: 'Email Templates', url: '/guides/auth/auth-email-templates' },
],
},
@@ -509,7 +652,7 @@ export const database = {
}
export const api = {
icon: 'reference',
icon: 'serverless-apis',
title: 'Serverless APIs',
url: '/guides/api',
items: [
@@ -547,7 +690,7 @@ export const api = {
}
export const functions = {
icon: 'functions',
icon: 'edge-functions',
title: 'Edge Functions',
url: '/guides/functions',
items: [
@@ -834,7 +977,7 @@ export const resources = {
export const self_hosting = {
title: 'Self-Hosting',
icon: 'resources',
icon: 'self-hosting',
url: '/guides/self-hosting',
items: [
{ name: 'Overview', url: '/guides/self-hosting' },
@@ -22,7 +22,7 @@ const NavigationMenuCliList = ({ currentLevel, setLevel, id }) => {
icon?: string
}) => {
return (
<li key={id} className="function-link-item text-scale-1000">
<li key={id} className="function-link-item text-scale-1000 leading-3">
<Link href={`#${id}`} passHref>
<a className="cursor-pointer transition text-scale-1000 text-sm hover:text-brand-900 flex gap-3">
{icon && <img className="w-3" src={`${router.basePath}${icon}`} />}
@@ -2,23 +2,10 @@ import { useTheme } from 'common/Providers'
import Image from 'next/image'
import Link from 'next/link'
import { useRouter } from 'next/router'
import React from 'react'
import React, { useEffect, useRef } from 'react'
import { IconChevronLeft } from '~/../../packages/ui'
import * as Accordion from '@radix-ui/react-accordion'
const HeaderImage = React.memo(function HeaderImage(props: any) {
const router = useRouter()
const { isDarkMode } = useTheme()
return (
<Image
alt={props.icon}
width={15}
height={15}
src={`${router.basePath}` + `/img/icons/menu/${props.icon}${isDarkMode ? '' : '-light'}.svg`}
/>
)
})
import HomeMenuIconPicker from './HomeMenuIconPicker'
const HeaderLink = React.memo(function HeaderLink(props: {
title: string
@@ -43,6 +30,8 @@ const HeaderLink = React.memo(function HeaderLink(props: {
const ContentAccordionLink = React.memo(function ContentAccordionLink(props: any) {
const router = useRouter()
const { isDarkMode } = useTheme()
const activeItem = props.subItem.url === router.pathname
const activeItemRef = useRef(null)
const LinkContainer = (props) => {
return (
@@ -52,6 +41,15 @@ const ContentAccordionLink = React.memo(function ContentAccordionLink(props: any
)
}
useEffect(() => {
// scroll to active item
if (activeItem && activeItemRef.current) {
// this is a hack, but seems a common one on Stackoverflow
setTimeout(() => {
activeItemRef.current.scrollIntoView({ behavior: 'smooth', block: 'nearest' })
}, 0)
}
})
return (
<>
{props.subItemIndex === 0 && (
@@ -63,15 +61,15 @@ const ContentAccordionLink = React.memo(function ContentAccordionLink(props: any
</>
)}
<Accordion.Item key={props.subItem.label} value={props.subItem.url}>
<li key={props.subItem.name}>
<li key={props.subItem.name} ref={activeItem ? activeItemRef : null}>
<LinkContainer
url={props.subItem.url}
className={[
'flex items-center gap-2',
'cursor-pointer transition text-sm',
props.subItem.url === router.pathname
? 'text-brand-900'
: 'hover:text-brand-900 text-scale-1000',
activeItem
? 'text-brand-900 font-medium'
: 'hover:text-scale-1200 dark:hover:text-scale-1100 text-scale-1000',
].join(' ')}
parent={props.subItem.parent}
>
@@ -120,14 +118,14 @@ const ContentLink = React.memo(function ContentLink(props: any) {
const router = useRouter()
return (
<li>
<li className="mb-1.5">
<Link href={`${props.url}`} passHref>
<a
className={[
'cursor-pointer transition text-sm',
props.url === router.pathname
? 'text-brand-900'
: 'hover:text-brand-900 text-scale-1000',
: 'hover:text-scale-1200 dark:hover:text-scale-1100 text-scale-1000',
].join(' ')}
>
{props.icon && (
@@ -168,8 +166,8 @@ const Content = (props) => {
<Link href={menu.url ?? ''} passHref>
<a>
<div className="flex items-center gap-3 my-3">
<HeaderImage icon={menu.icon} />
<div className="flex items-center gap-3 my-3 text-brand-900">
<HomeMenuIconPicker icon={menu.icon} />
<HeaderLink title={menu.title} url={menu.url} id={id} />
</div>
</a>
@@ -179,15 +177,17 @@ const Content = (props) => {
return (
<div key={x.name}>
{x.items && x.items.length > 0 ? (
<div className="flex flex-col gap-1">
<div className="flex flex-col gap-2.5">
{x.items.map((subItem, subItemIndex) => {
return (
<ContentAccordionLink
key={subItem.name}
subItem={subItem}
subItemIndex={subItemIndex}
parent={x}
/>
<>
<ContentAccordionLink
key={subItem.name}
subItem={subItem}
subItemIndex={subItemIndex}
parent={x}
/>
</>
)
})}
</div>
@@ -1,7 +1,7 @@
import * as Accordion from '@radix-ui/react-accordion'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { IconChevronLeft } from 'ui'
import { IconChevronLeft, IconChevronUp } from 'ui'
import * as NavItems from './NavigationMenu.constants'
import { find } from 'lodash'
@@ -15,24 +15,10 @@ import { RefIdOptions, RefKeyOptions } from './NavigationMenu'
import React, { Fragment } from 'react'
import { generateAllowedClientLibKeys } from '~/lib/refGenerator/helpers'
import { isFuncNotInLibraryOrVersion } from './NavigationMenu.utils'
const HeaderImage = React.memo(function HeaderImage(props: any) {
const router = useRouter()
const { isDarkMode } = useTheme()
return (
<Image
alt={props.icon}
width={15}
height={15}
src={`${router.basePath}` + `/img/icons/menu/${props.icon}${isDarkMode ? '' : '-light'}.svg`}
/>
)
})
import { cn } from 'ui/src/utils/cn'
import HomeMenuIconPicker from './HomeMenuIconPicker'
const HeaderLink = React.memo(function HeaderLink(props: any) {
const router = useRouter()
return (
<span className={['text-base text-brand-1200 ', !props.title && 'capitalize'].join(' ')}>
{props.title ?? props.id}
@@ -46,6 +32,8 @@ const FunctionLink = React.memo(function FunctionLink({
icon,
library,
slug,
isParent = false,
isSubItem = false,
}: {
title: string
name?: string
@@ -54,6 +42,8 @@ const FunctionLink = React.memo(function FunctionLink({
product?: string
library: string
slug: string
isParent: boolean
isSubItem: boolean
}) {
const router = useRouter()
const activeAccordianItem = useMenuActiveRefId()
@@ -70,16 +60,29 @@ const FunctionLink = React.memo(function FunctionLink({
const active = activeAccordianItem === id
return (
<li key={id} className="function-link-item">
<li key={id} className="function-link-item leading-5">
<Link href={`/reference/${library}/${version ? version + '/' : ''}${slug}`} passHref>
<a
className={[
'cursor-pointer transition text-sm hover:text-brand-900 flex gap-3',
active ? 'text-brand-900' : 'text-scale-1000',
].join(' ')}
className={cn(
'cursor-pointer transition text-sm hover:text-scale-1200 gap-3 relative',
isParent ? 'flex justify-between' : 'leading-3',
active ? 'text-brand-900' : 'text-scale-1000'
)}
>
{icon && <Image width={16} height={16} alt={icon} src={`${router.basePath}${icon}`} />}
{title}
{active && !isSubItem && (
<div
aria-hidden="true"
className="absolute -left-[13px] top-0 bottom-0 w-[1px] bg-brand-1000"
></div>
)}
{isParent && (
<IconChevronUp
width={16}
className="data-open-parent:rotate-0 data-closed-parent:rotate-90 transition"
/>
)}
</a>
</Link>
</li>
@@ -121,26 +124,37 @@ const RenderLink = React.memo(function RenderLink(props: any) {
value={active ? props.id : ''}
>
<Accordion.Item key={props.id + '-accordian-item'} value={props.id}>
<FunctionLink library={props.lib} title={props.title} id={props.id} slug={props.slug} />
<Accordion.Content
key={props.id + '-sub-items-accordion-container'}
className="transition data-open:animate-slide-down data-closed:animate-slide-up ml-2"
>
{props.items &&
props.items
.filter((item) => props.allowedKeys.includes(item.id))
.map((item) => {
return (
<FunctionLink
key={item.title}
library={props.lib}
title={item.title}
id={item.id}
slug={item.slug}
/>
)
})}
</Accordion.Content>
<FunctionLink
library={props.lib}
title={props.title}
id={props.id}
slug={props.slug}
isParent={props.items}
isSubItem={true}
/>
{props.items && (
<Accordion.Content
key={props.id + '-sub-items-accordion-container'}
className="transition data-open:animate-slide-down data-closed:animate-slide-up border-l border-scale-600 pl-3 ml-1 data-open:mt-2 grid gap-2.5"
>
{props.items &&
props.items
.filter((item) => props.allowedKeys.includes(item.id))
.map((item) => {
return (
<FunctionLink
key={item.title}
library={props.lib}
title={item.title}
id={item.id}
slug={item.slug}
isParent={false}
isSubItem={false}
/>
)
})}
</Accordion.Content>
)}
</Accordion.Item>
</Accordion.Root>
)
@@ -239,12 +253,12 @@ const Content: React.FC<INavigationMenuRefList> = ({ id, lib, commonSections, sp
</Link>
<div className="flex items-center gap-3 my-3">
<HeaderImage icon={menu.icon} />
<HomeMenuIconPicker icon={menu.icon} width={21} height={21} />
<HeaderLink title={menu.title} url={menu.url} id={id} />
<RevVersionDropdown />
</div>
<ul className="function-link-list flex flex-col gap-1">
<ul className="function-link-list flex flex-col gap-2">
{sections.map((fn: any, fnIndex) => {
// run allow check
if (isFuncNotInLibraryOrVersion(fn.id, fn.type, allowedKeys)) {
+41 -1
View File
@@ -1,7 +1,6 @@
import Link from 'next/link'
import { Alert, Button, CodeBlock, GlassPanel, markdownComponents, Tabs } from 'ui'
import StepHikeCompact from '~/components/StepHikeCompact'
// Common components
import Admonition from './Admonition'
import ButtonCard from './ButtonCard'
@@ -31,6 +30,28 @@ import CliGlobalFlagsHandler from '~/components/reference/enrichments/cli/CliGlo
import Options from '~/components/Options'
import Param from '~/components/Params'
import {
IconMenuJavascript,
IconMenuHome,
IconMenuGettingStarted,
IconMenuDatabase,
IconMenuServerlessApis,
IconMenuAuth,
IconMenuEdgeFunctions,
IconMenuRealtime,
IconMenuStorage,
IconMenuPlatform,
IconMenuResources,
IconMenuSelfHosting,
IconMenuIntegrations,
IconMenuFlutter,
IconMenuPython,
IconMenuCsharp,
IconMenuSwift,
IconMenuApi,
IconMenuCli,
} from './Navigation/NavigationMenu/HomeMenuIcons'
const components = {
...markdownComponents,
Admonition,
@@ -73,6 +94,25 @@ const components = {
CliGlobalFlagsHandler: () => <CliGlobalFlagsHandler />,
Options,
Param,
IconMenuJavascript,
IconMenuHome,
IconMenuGettingStarted,
IconMenuDatabase,
IconMenuServerlessApis,
IconMenuAuth,
IconMenuEdgeFunctions,
IconMenuRealtime,
IconMenuStorage,
IconMenuPlatform,
IconMenuResources,
IconMenuSelfHosting,
IconMenuIntegrations,
IconMenuFlutter,
IconMenuPython,
IconMenuCsharp,
IconMenuSwift,
IconMenuApi,
IconMenuCli,
}
export default components
+1 -1
View File
@@ -5,6 +5,6 @@
"title": "Author Styleguide",
"url": "https://github.com/supabase/supabase/blob/master/DEVELOPERS.md"
},
{ "title": "Open Source", "url": "https://supabase.com/docs/oss" },
{ "title": "Open Source", "url": "https://supabase.com/open-source" },
{ "title": "SupaSquad", "url": "https://supabase.com/supasquad" }
]
+1 -1
View File
@@ -5,7 +5,7 @@ hideTitle: true
---
<div className="flex items-start gap-6 not-prose" id="introduction">
<img src="/docs/img/icons/menu/reference-csharp.svg" className="w-8 h-8 rounded" />
<IconMenuCsharp width={35} height={35} />
<div className="flex flex-col gap-2">
<h1 className="text-3xl text-scale-1200 m-0">C# Client Library</h1>
<h2 className="text-base font-mono text-scale-1100">@supabase-community/supabase-csharp</h2>
+1 -1
View File
@@ -5,7 +5,7 @@ hideTitle: true
---
<div className="flex items-start gap-6 not-prose" id="introduction">
<img src="/docs/img/icons/menu/reference-dart.svg" className="w-8 h-8 rounded" />
<IconMenuFlutter width={35} height={35} />
<div className="flex flex-col gap-2">
<h1 className="text-3xl text-scale-1200 m-0">Flutter Client Library</h1>
<h2 className="text-base font-mono text-scale-1100">supabase-flutter</h2>
+1 -1
View File
@@ -5,7 +5,7 @@ hideTitle: true
---
<div className="flex items-start gap-6 not-prose" id="introduction">
<img src="/docs/img/icons/menu/reference-dart.svg" className="w-8 h-8 rounded" />
<IconMenuFlutter width={35} height={35} />
<div className="flex flex-col gap-2">
<h1 className="text-3xl text-scale-1200 m-0">Flutter Client Library</h1>
<h2 className="text-base font-mono text-scale-1100">supabase-flutter</h2>
@@ -5,7 +5,7 @@ hideTitle: true
---
<div className="flex items-start gap-6 not-prose" id="introduction">
<img src="/docs/img/icons/menu/reference-javascript.svg" className="w-8 h-8 rounded" />
<IconMenuJavascript width={35} height={35} />
<div className="flex flex-col gap-2">
<h1 className="text-3xl text-scale-1200 m-0">Javascript Client Library</h1>
<h2 className="text-base font-mono text-scale-1100">@supabase/supabase-js</h2>
@@ -5,7 +5,7 @@ hideTitle: true
---
<div className="flex items-start gap-6 not-prose" id="introduction">
<img src="/docs/img/icons/javascript-icon.svg" className="w-8 h-8 rounded" />
<IconMenuJavascript width={35} height={35} />
<div className="flex flex-col gap-2">
<h1 className="text-3xl text-scale-1200 m-0">Javascript Client Library</h1>
<h2 className="text-base font-mono text-scale-1100">@supabase/supabase-js</h2>
+1 -1
View File
@@ -5,7 +5,7 @@ hideTitle: true
---
<div className="flex items-start gap-6 not-prose" id="introduction">
<img src="/docs/img/icons/menu/reference-python.svg" className="w-8 h-8 rounded" />
<IconMenuPython width={35} height={35} />
<div className="flex flex-col gap-2">
<h1 className="text-3xl text-scale-1200 m-0">Python Client Library</h1>
<h2 className="text-base font-mono text-scale-1100">@supabase-community/supabase-py</h2>
+3 -5
View File
@@ -5,7 +5,7 @@ hideTitle: true
---
<div className="flex items-start gap-6 not-prose" id="introduction">
<img src="/docs/img/icons/menu/reference-swift.svg" className="w-8 h-8 rounded" />
<IconMenuSwift width={35} height={35} />
<div className="flex flex-col gap-2">
<h1 className="text-3xl text-scale-1200 m-0">Swift Client Library</h1>
<h2 className="text-base font-mono text-scale-1100">@supabase-community/supabase-swift</h2>
@@ -16,14 +16,12 @@ hideTitle: true
This reference documents every object and method available in Supabase's Swift library, [supabase-swift](https://github.com/supabase-community/supabase-swift). You can use supabase-swift to interact with your Postgres database, listen to database changes, invoke Deno Edge Functions, build login and user management functionality, and manage large files.
We also provide a [supabase](https://pub.dev/packages/supabase) package for non-Swift projects.
</div>
<div className="max-w-xl bg-slate-300 px-4 py-2 rounded-md">
<p>The Swift client library is created and maintained by the Supabase community, and is not an official library. Please be tolerant of areas where the library is still being developed, and — as with all the libraries — feel free to contribute wherever you find issues.</p>
<p>
Huge thanks to official maintainer, [Maail](https://github.com/maail).
</p>
<p>Huge thanks to official maintainer, [Maail](https://github.com/maail).</p>
</div>
@@ -5,7 +5,7 @@ hideTitle: true
---
<div className="flex items-start gap-6 not-prose" id="introduction">
<img src="/docs/img/icons/menu/reference-csharp.svg" className="w-8 h-8 rounded" />
<IconMenuCsharp width={35} height={35} />
<div className="flex flex-col gap-2">
<h1 className="text-3xl text-scale-1200 m-0">C# Client Library</h1>
<h2 className="text-base font-mono text-scale-1100">supabase-csharp</h2>
+1
View File
@@ -1,2 +1,3 @@
export const IS_PLATFORM = process.env.NEXT_PUBLIC_IS_PLATFORM === 'true'
export const LOCAL_SUPABASE = process.env.NEXT_PUBLIC_LOCAL_SUPABASE === 'true'
export const API_URL = process.env.NEXT_PUBLIC_API_URL
+1 -3
View File
@@ -1,7 +1,5 @@
interface DataProps {
referrer?: string
title: string
route?: string
[prop: string]: any
}
export const post = (url: string, data: DataProps, options = {}) => {
+9 -9
View File
@@ -1,6 +1,6 @@
import { createBrowserSupabaseClient } from '@supabase/auth-helpers-nextjs'
import { SessionContextProvider } from '@supabase/auth-helpers-react'
import { AuthProvider, ThemeProvider } from 'common'
import { AuthProvider, ThemeProvider, useTelemetryProps } from 'common'
import { useRouter } from 'next/router'
import { useEffect, useState } from 'react'
import ReactMarkdown from 'react-markdown'
@@ -10,7 +10,7 @@ import { CommandMenuProvider } from 'ui'
import components from '~/components'
import Favicons from '~/components/Favicons'
import SiteLayout from '~/layouts/SiteLayout'
import { IS_PLATFORM, LOCAL_SUPABASE } from '~/lib/constants'
import { API_URL, IS_PLATFORM, LOCAL_SUPABASE } from '~/lib/constants'
import { post } from '~/lib/fetchWrappers'
import '../styles/ch.scss'
import '../styles/main.scss?v=1.0.0'
@@ -19,16 +19,21 @@ import '../styles/prism-okaidia.scss'
function MyApp({ Component, pageProps }: AppPropsWithLayout) {
const router = useRouter()
const telemetryProps = useTelemetryProps()
const [supabase] = useState(() =>
IS_PLATFORM || LOCAL_SUPABASE ? createBrowserSupabaseClient() : undefined
)
function handlePageTelemetry(route: string) {
return post(`https://api.supabase.io/platform/telemetry/page`, {
return post(`${API_URL}/telemetry/page`, {
referrer: document.referrer,
title: document.title,
route,
ga: {
screen_resolution: telemetryProps?.screenResolution,
language: telemetryProps?.language,
},
})
}
@@ -85,12 +90,7 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) {
<Favicons />
<AuthContainer>
<ThemeProvider>
<CommandMenuProvider
site="docs"
MarkdownHandler={(props) => (
<ReactMarkdown remarkPlugins={[remarkGfm]} components={components} {...props} />
)}
>
<CommandMenuProvider site="docs">
<SiteLayout>
<Component {...pageProps} />
</SiteLayout>
@@ -0,0 +1,109 @@
import Layout from '~/layouts/DefaultGuideLayout'
import { Accordion } from 'ui'
export const meta = {
id: 'auth-password-reset',
title: 'Password Reset',
description: 'How to reset password with Supabase Auth',
}
- Send email for password reset using [`.resetPasswordForEmail`](/docs/reference/javascript/auth-resetpasswordforemail) while providing a `redirectTo` parameter
- Email link will work as a magic link and log the user in then take them to the url specified in the `redirectTo` parameter
- Create form to update the password and call the [`.updateUser`](/docs/reference/javascript/auth-updateuser) method with the new password
## Single Page Application (SPA)
### Sending password reset email
Supabase provides a convenient method [`.resetPasswordForEmail`](/docs/reference/javascript/auth-resetpasswordforemail)
to reset a user password. This method takes a parameter of `redirectTo` which we will use to pass an absolute URL to
the update password page. This URL must be saved in your
allowed [Redirect URLs](https://app.supabase.com/project/_/auth/url-configuration) list found at
[Authentication > Redirect Configuration](https://app.supabase.com/project/_/auth/url-configuration) or it won't
redirect the user.
```ts
await supabase.auth.resetPasswordForEmail('hello@example.com', {
redirectTo: 'http://example.com/account/update-password',
})
```
### Email link
The email link you receive will work like a magic link. This way when you click the link you will be logged into
the website. Since we passed a redirect URL to the [`.resetPasswordForEmail`](https://supabase.com/docs/reference/javascript/auth-resetpasswordforemail)
method the user should be sent to the update password page.
### Update Password
To update the password we call the [`.updateUser`](/docs/reference/javascript/auth-updateuser) method and pass along the new password to this method.
```ts
await supabase.auth.updateUser({ password: new_password })
```
## Server-Side Rendering (SSR)
### Sending password reset email
Supabase provides a convenient method [`.resetPasswordForEmail`](/docs/reference/javascript/auth-resetpasswordforemail)
to reset a user password. This method takes a parameter of `redirectTo` which we will use to pass an absolute URL to
the callback page along with a query parameter to the update password page. This URL must be saved in your
allowed [Redirect URLs](https://app.supabase.com/project/_/auth/url-configuration) list found at
[Authentication > Redirect Configuration](https://app.supabase.com/project/_/auth/url-configuration) or it won't
redirect the user.
```ts
await supabase.auth.resetPasswordForEmail('hello@example.com', {
redirectTo: 'http://example.com/api/auth/callback?next=/account/update-password',
})
```
<Admonition type="note">
We are using `next` as our query parameter, but this can name whatever you like.
</Admonition>
### Email link
The email link you receive will behave like a magic link. When the link is clicked you will be sent to the `redirectTo` URL you specified that points to the path with the exchange code.
### Exchange authorization code
After redirecting to the server page, we need to retrieve the code from the query parameter called `code` and pass it to the `.exchangeAuthCodeForSession` function.
```ts
// api/auth/callback.ts
// The code is retrieved from the query parameter - use whichever mechanism is recommended
// for your app/framework. This is just an example.
const code = url.searchParams.get('code')
// call the Supabase API to exchange the code for a session
await supabase.auth.exchangeCodeForSession(code)
```
<Admonition type="note">
The query parameter is always `code` for the authorization code returned from the Supabase API
</Admonition>
We will also need to check for the `next` query parameter to redirect the user to the update password page.
```ts
// api/auth/callback.ts
// The password page path is retrieved from the query parameter - use whichever mechanism is recommended
// for your app/framework. This is just an example.
const next = url.searchParams.get('next')
// using NextJS API response object in this example
res.redirect(next)
```
### Update Password
To update the password we call the [`.updateUser`](/docs/reference/javascript/auth-updateuser) method and
pass along the new password to this method.
```ts
await supabase.auth.updateUser({ password: new_password })
```
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
@@ -99,7 +99,7 @@ service_role key: eyJh......
```
You can use the [supabase stop](/docs/reference/cli/usage#supabase-stop) command at any time to stop all services.
You can use the [`supabase stop`](/docs/reference/cli/usage#supabase-stop) command at any time to stop all services and reset your local database. Use `supabase stop --backup` to stop all services without resetting your local database.
## Access your project's services
@@ -463,6 +463,7 @@ The local development environment is not as feature-complete as the Supabase Pla
- The Functions interface is coming soon.
- You cannot update your project settings in the Dashboard. This must be done using the CLI.
- The CLI version determines the local version of Studio used, so make sure you keep your local [Supabase CLI up to date](https://github.com/supabase/cli#getting-started). We're constantly adding new features and bug fixes.
export const Page = ({ children }) => <Layout meta={meta} children={children} />
@@ -45,21 +45,21 @@ These variables will be exposed on the browser, and that's completely fine since
[Row Level Security](/docs/guides/auth#row-level-security) enabled on our Database.
```ts title=lib/supabase.ts
import * as SecureStore from "expo-secure-store";
import 'react-native-url-polyfill/auto'
import * as SecureStore from 'expo-secure-store'
import { createClient } from '@supabase/supabase-js'
const ExpoSecureStoreAdapter = {
getItem: (key: string) => {
return SecureStore.getItemAsync(key);
return SecureStore.getItemAsync(key)
},
setItem: (key: string, value: string) => {
SecureStore.setItemAsync(key, value);
SecureStore.setItemAsync(key, value)
},
removeItem: (key: string) => {
SecureStore.deleteItemAsync(key);
SecureStore.deleteItemAsync(key)
},
};
}
const supabaseUrl = YOUR_REACT_NATIVE_SUPABASE_URL
const supabaseAnonKey = YOUR_REACT_NATIVE_SUPABASE_ANON_KEY
@@ -80,7 +80,7 @@ We have a number of small improvements:
- supabase-js now has [UMD support](https://github.com/supabase/supabase/pull/156)
- We significantly [improved our docs](/docs). Try out the new search!
- All of our awesome sponsors are now listed [on our OSS page](/docs/oss).
- All of our awesome sponsors are now listed [on our OSS page](/open-source).
### From the community
@@ -0,0 +1,120 @@
---
title: Supabase Beta April 2023
description: A review of Launch Week 7 and more exciting updates from last month.
author: ant_wilson
image: 2023-05-09-beta-update-april/monthly-update-april-2023.jpg
thumb: 2023-05-09-beta-update-april/monthly-update-april-2023.jpg
tags:
- release-notes
date: '2023-05-09'
toc_depth: 3
---
Brace yourself, this is one of the most feature-packed Beta Updates we've published so far.
[Launch Week 7](https://supabase.com/launch-week) was a massive success with great feedback from the community. We also gave away lots of mechanical keyboards, including the Hackathon winner which we announce here!
## Day 1 - Supabase Logs: open source logging server
![Day 1 - Supabase Logs: open source logging server](/images/blog/2023-05-09-beta-update-april/newsletter-OG-day-1.jpg)
Logflare is the hub of analytics streams for Supabase. We are open sourcing it so that you can self-host your own Logging infrastructure.
[Blog Post](https://supabase.com/blog/supabase-logs-self-hosted)\
[Video overview](https://www.youtube.com/watch?v=Ai2BjHV36Ng)
## Day 2 - Supabase Edge Runtime: Self-hosted Deno Functions
![Day 2 - Supabase Edge Runtime: Self-hosted Deno Functions](/images/blog/2023-05-09-beta-update-april/newsletter-OG-day2.jpg)
You can now self-host Edge Functions and run them in local development using our new Edge Runtime. We published a guide showing how to self-host Edge Functions with Fly and what more is coming![⚡](https://fonts.gstatic.com/s/e/notoemoji/15.0/26a1/32.png)️
[Blog post](https://supabase.com/blog/edge-runtime-self-hosted-deno-functions)\
[Video overview](https://www.youtube.com/watch?v=cPGxPl1lx4Y)
## Day 3 - Storage v3: Resumable Uploads with support for 50GB files
![Day 3 - Storage v3: Resumable Uploads with support for 50GB files](/images/blog/2023-05-09-beta-update-april/newsletter-OG-day3.jpg)
Supabase Storage received many of the most requested features from our users: Resumable Uploads, Quality Filters, Next.js support, and WebP support.
[Blog post](https://supabase.com/blog/storage-v3-resumable-uploads)\
[Video overview](https://www.youtube.com/watch?v=pT2PcZFq_M0)
## Day 4 - Supabase Auth: SSO, Mobile, and Server-side support
![Day 4 - Supabase Auth: SSO, Mobile, and Server-side support](/images/blog/2023-05-09-beta-update-april/newsletter-OG-day4.jpg)
On day 4, we introduced SSO with SAML 2.0, PKCE, and Sign in with Apple for iOS. It felt like acronym day, but it was actually Auth day!
[Blog post](https://supabase.com/blog/supabase-auth-sso-pkce)\
[Video overview](https://www.youtube.com/watch?v=hAwJeR6mhB0)
## Day 5 - Supabase Studio 2.0 with new AI features
![Day 5 - Supabase Studio 2.0 with new AI features](/images/blog/2023-05-09-beta-update-april/newsletter-OG-day5-studio.jpg)
Supabase Studio got a major upgrade that goes from redesigns to improved developer experience, and new tools. We have the features people have been asking for and new capabilities that will change the way you work.
[Blog Post](https://supabase.com/blog/supabase-studio-2.0)\
[Video overview](https://www.youtube.com/watch?v=0rcNqHt5KWU)
## Community highlights
![Community highlights](/images/blog/2023-05-09-beta-update-april/newsletter-OG-day5-community.jpg)
Our community defines us. We're honored to work with, sponsor, and support incredible people and tools 💜. Our CEO wrote a highlight of the last 3 months.
[Blog post](https://supabase.com/blog/launch-week-7-community-highlights)
## Introducing dbdev: PostgreSQL Package Manager
![dbdev: PostgreSQL Package Manager](/images/blog/2023-05-09-beta-update-april/newsletter-OG-day5-dbdev.jpg)
[database.dev](https://database.dev/) fills the same role for PostgreSQL as `npm` for JavaScript or `pip` for Python, it enables publishing libraries and applications for repeatable deployment. Our goal is to create an open ecosystem for packaging and discovering SQL.
[Blog post](https://supabase.com/blog/dbdev)
## More product announcements
- Trusted Language Extensions for Postgres. [[Blog post]](https://supabase.com/blog/pg-tle)
- What's New in pg_graphql v1.2. [[Blog post]](https://supabase.com/blog/whats-new-in-pg-graphql-v1-2)
- GitHub Discussions are now a new knowledge source for search & AI (Troubleshooting category only for now). [[Check it out]](https://supabase.com/docs)
- New API report with routing information for each chart, making it easier to debug API calls.  [[PR]](https://github.com/supabase/supabase/pull/14063)
- Storage permission changes: the developer role is now allowed to update the storage settings (previously was only owner and admin). [[PR]](https://github.com/supabase/supabase/pull/13883)
## Launch Week 7 Hackathon winners
![Launch Week 7 Hackathon winners](/images/blog/2023-05-09-beta-update-april/newsletter-hackathon.jpg)
The community is loving `pgvector` to build AI apps so we decided to make it part of the traditional Launch Week Hackathon. The quality of the apps was out of this world, it wasn't easy, but in the end, we selected [Page Assist](https://github.com/n4ze3m/page-assist) - by [@n4ze3m ](https://twitter.com/n4ze3m)as the winner of the Best Overall Project. 
[Full list of Winners](https://supabase.com/blog/launch-week-7-hackathon-winners)
[See all the submissions](https://www.madewithsupabase.com/launch-week-7)
## Mendable.ai switches from Pinecone to Supabase for PostgreSQL vector embeddings.
![Mendable logo](/images/blog/2023-05-09-beta-update-april/customer-stories-mendable.png)
With Supabase's pg_vector, Mendable.ai could build a more cost-effective solution that is just as performant - if not more performant - than other vector databases.
[Read the full story](https://supabase.com/customers/mendableai)
## From the community
![Community](/images/blog/2022-june/community.jpg)
- FlutterFlow now supports Supabase Authentication. [Video guide](https://www.youtube.com/watch?v=tL-sLPfWzVE)
- Supabase + ClickHouse: Combining the Best of the OLTP and OLAP Worlds. [Webinar](https://www.youtube.com/watch?v=LDWEsw41Zko)
- Our friend Guillaume put together the most incredible course about Supabase, with the in and outs of the platform. [Full course](https://www.youtube.com/watch?v=8DTOTT7q0XA)
- Supabase + LangChain starter template for building full stack AI apps. [Template](https://github.com/langchain-ai/langchain-template-supabase)
- Creating a Books Tracker App with .NET MAUI and Supabase. [Article](https://hackernoon.com/creating-a-books-tracker-app-with-net-maui-and-supabase)
- Vanta Case Study - Supabase turns trust into a revenue-generating opportunity with Vanta. [Case Study](https://www.vanta.com/customers/supabase)
## Meme Zone
As always, one of our favorite memes from last month. [Follow us on Twitter](https://twitter.com/supabase) for more.
![Beta Update Meme](images/blog/2023-05-09-beta-update-april/beta-update-april-2023-meme.png)
+14 -10
View File
@@ -1,8 +1,8 @@
---
title: Mendable.ai switches from Pinecone to Supabase for PostgreSQL vector embeddings.
name: Mendable.ai
description: How Mendable.ai boosts efficiency and accuracy of chat powered search for documentation using Supabase with pg_vector.
author: rory_wilding
description: How Mendable.ai boosts efficiency and accuracy of chat powered search for documentation using Supabase with pg_vector.
author: paul_copplestone
author_title: Supabase
author_url: https://github.com/kiwicopple
author_image_url: https://avatars2.githubusercontent.com/u/10214025?s=400&u=c6775be2ae667e2acae3ccd347fed62bb3f5b3e7&v=4
@@ -24,31 +24,35 @@ misc: [{ label: 'Backed by', text: 'Y Combinator' }]
about: Mendable.ai is Chat Powered Search for Documentation.
---
[Mendable.ai](http://mendable.ai/) provides a chat-powered search engine for technical documentation. Their AI-powered search tool makes it easier for developers and other technical users to find relevant information in complex documentation. Users can simply ask questions in natural language, and the tool returns the most relevant answers. [Mendable.ai](http://mendable.ai/)'s search engine also provides detailed analytics, which helps teams identify knowledge gaps and areas for improvement in their documentation.
[Mendable.ai](http://mendable.ai/) provides a chat-powered search engine for technical documentation. Their AI-powered search tool makes it easier for developers and other technical users to find relevant information in complex documentation. Users can simply ask questions in natural language, and the tool returns the most relevant answers. Mendable.ai's search engine also provides detailed analytics, which helps teams identify knowledge gaps and areas for improvement in their documentation.
## The Challenge
[Mendable.ai](http://mendable.ai/) needed to store and search through large amounts of vector data to improve the efficiency and accuracy of their similarity search operations for their Chat Powered Search for Documentation. They tried Faiss, Weviate, and Pinecone., but found them to be expensive and not very intuitive, especially when it came to storing metadata along with the vectors.
Mendable.ai was experiencing tremendous success, with WAUs growing nearly 300% since March, and started looking for a tool to store and search through large amounts of vector data to improve the efficiency and accuracy of their similarity search operations for their Chat Powered Search for Documentation. They tried Faiss, Weviate, and Pinecone, but found them to be expensive and not very intuitive, especially when it came to storing metadata along with the vectors.
Why they chose Supabase:
[Mendable.ai](http://mendable.ai/) discovered that Supabase supports pg_vector and found it to be a simple and cost-effective solution. They were impressed with the open-source nature of Supabase, as well as its ability to store metadata alongside the vectors. They also appreciated the intuitive interface and ease of use.
Mendable.ai discovered that Supabase supports pgvector and found it to be a simple and cost-effective solution. They were impressed with the open source nature of Supabase, as well as its ability to store metadata alongside the vectors. They also appreciated the intuitive interface and ease of use.
<Quote img="caleb-peffer.jpg" caption="Caleb Peffer - CEO, Mendable.ai">
We tried other vector databases - we tried Faiss, we tried Weviate, we tried Pinecone. We found them to be incredibly expensive and not very intuitive. If you’re just doing vector search they’re great, but if you need to store a bunch of metadata that becomes a huge pain.
We tried other vector databases - we tried Faiss, we tried Weviate, we tried Pinecone. We found
them to be incredibly expensive and not very intuitive. If you’re just doing vector search they’re
great, but if you need to store a bunch of metadata that becomes a huge pain.
</Quote>
## What They Built
Using Supabase's pg_vector, [Mendable.ai](http://mendable.ai/) was able to build a more efficient and accurate search function for their AI chatbot. By storing vector data alongside metadata in Supabase, [Mendable.ai](http://mendable.ai/) was able to quickly and easily search through their customers documentation to find the most relevant responses to queries. They found that Supabase's solution was just as performant as dedicated vector databases, but without the high cost.
Using Supabase and pgvector, Mendable.ai was able to build a more efficient and accurate search function for their AI chatbot. By storing vector data alongside metadata in Supabase, Mendable.ai was able to quickly and easily search through their customers documentation to find the most relevant responses to queries. They found that Supabase's solution was just as performant as dedicated vector databases, but without the high cost.
## The Results
Thanks to Supabase's pg_vector, [Mendable.ai](http://mendable.ai/) was able to significantly improve the efficiency and accuracy of their Chat Powered Search for Documentation. They were able to build faster and more cost-effectively using Supabase’s open source stack.
Thanks to Supabase and pgvector, Mendable.ai was able to significantly improve the efficiency and accuracy of their Chat Powered Search for Documentation. They were able to build faster and more cost-effectively using Supabase’s open source stack.
## Tech stack
[Mendable.ai](http://mendable.ai/)'s tech stack includes React, Next.js, Express, Vercel, and Supabase.
Mendable.ai's tech stack includes React, Next.js, Express, Vercel, and Supabase.
<Quote img="caleb-peffer.jpg" caption="Caleb Peffer - CEO, Mendable.ai">
We looked at the alternatives and chose Supabase because it’s open source, it’s simpler, and, for all the ways we need use it, Supabase has been just as performant - if not more performant - than the other vector databases.
We looked at the alternatives and chose Supabase because it’s open source, it’s simpler, and, for
all the ways we need use it, Supabase has been just as performant - if not more performant - than
the other vector databases.
</Quote>
+50 -4
View File
@@ -1,12 +1,52 @@
import { Button, Badge, IconArrowRight } from 'ui'
import SectionHeader from 'components/UI/SectionHeader'
import { Badge } from 'ui'
import Solutions from 'data/Solutions.json'
import Link from 'next/link'
import Telemetry from '~/lib/telemetry'
import gaEvents from '~/lib/gaEvents'
import SectionContainer from '../Layouts/SectionContainer'
import ProductIcon from '../ProductIcon'
import TextLink from '../TextLink'
import { useTelemetryProps } from 'common/hooks/useTelemetryProps'
import { useRouter } from 'next/router'
const Features = () => {
const router = useRouter()
const telemetryProps = useTelemetryProps()
const sendTelemetryEvent = async (product: any) => {
switch (product) {
case 'Database':
return await Telemetry.sendEvent(
gaEvents['www_hp_subhero_products_database'],
telemetryProps,
router
)
case 'Authentication':
return await Telemetry.sendEvent(
gaEvents['www_hp_subhero_products_auth'],
telemetryProps,
router
)
case 'Storage':
return await Telemetry.sendEvent(
gaEvents['www_hp_subhero_products_storage'],
telemetryProps,
router
)
case 'Edge Functions':
return await Telemetry.sendEvent(
gaEvents['www_hp_subhero_products_edgeFunctions'],
telemetryProps,
router
)
case 'Realtime':
return await Telemetry.sendEvent(
gaEvents['www_hp_subhero_products_realtime'],
telemetryProps,
router
)
}
}
const IconSections = Object.values(Solutions).map((solution: any) => {
const { name, description, icon, label, url } = solution
if (solution.name === 'Realtime') return null
@@ -24,7 +64,13 @@ const Features = () => {
<Badge dot>{label}</Badge>
</div>
)}
{url && <TextLink label={label ? 'Get notified' : 'Learn more'} url={url} />}
{url && (
<TextLink
label={label ? 'Get notified' : 'Learn more'}
url={url}
onClick={() => sendTelemetryEvent(name)}
/>
)}
</div>
)
})
+16 -5
View File
@@ -1,10 +1,19 @@
import { Button, IconBookOpen, Space } from 'ui'
import Link from 'next/link'
import { Button, IconBookOpen } from 'ui'
import { useRouter } from 'next/router'
import Link from 'next/link'
import SectionContainer from './Layouts/SectionContainer'
import Telemetry, { TelemetryEvent } from '~/lib/telemetry'
import gaEvents from '~/lib/gaEvents'
import { useTelemetryProps } from 'common/hooks/useTelemetryProps'
const Hero = () => {
const { basePath } = useRouter()
const router = useRouter()
const { basePath } = router
const telemetryProps = useTelemetryProps()
const sendTelemetryEvent = async (event: TelemetryEvent) => {
await Telemetry.sendEvent(event, telemetryProps, router)
}
return (
<div className="overflow-hidden">
@@ -40,14 +49,16 @@ const Hero = () => {
</div>
<div className="flex items-center gap-2">
<Link href="https://app.supabase.com" as="https://app.supabase.com" passHref>
<a>
<a onClick={() => sendTelemetryEvent(gaEvents['www_hp_hero_startProject'])}>
<Button size="small" className="text-white">
Start your project
</Button>
</a>
</Link>
<Link href="/docs" as="/docs" passHref>
<a>
<a
onClick={() => sendTelemetryEvent(gaEvents['www_hp_hero_documentation'])}
>
<Button size="small" type="default" icon={<IconBookOpen />}>
Documentation
</Button>
+15 -13
View File
@@ -1,20 +1,22 @@
import { IconArrowRight, IconChevronRight } from 'ui'
import { IconChevronRight } from 'ui'
import Link from 'next/link'
function TextLink({ url, label }: any) {
function TextLink({ url = '', label, ...props }: any) {
return (
<a
href={url}
className="text-scale-1100 hover:text-scale-1200 mt-3 block cursor-pointer text-sm"
>
<div className="group flex items-center gap-1">
<span className="sr-only">{`${label} about ${url}`}</span>
<span>{label}</span>
<div className="transition-all group-hover:ml-0.5">
<IconChevronRight size={14} strokeWidth={2} />
<Link href={url} passHref>
<a
className="text-scale-1100 hover:text-scale-1200 mt-3 block cursor-pointer text-sm"
{...props}
>
<div className="group flex items-center gap-1">
<span className="sr-only">{`${label} about ${url}`}</span>
<span>{label}</span>
<div className="transition-all group-hover:ml-0.5">
<IconChevronRight size={14} strokeWidth={2} />
</div>
</div>
</div>
</a>
</a>
</Link>
)
}
+1 -1
View File
@@ -8,7 +8,7 @@
{
"text": "Open Source",
"description": "Supabase is an opensource company, supporting existing open source tools and communities wherever possible.",
"url": "/docs/oss",
"url": "/open-source",
"icon": "M7 8h10M7 12h4m1 8l-4-4H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-3l-4 4z"
},
{
+17
View File
@@ -1,4 +1,21 @@
[
{
"type": "Customer Story",
"title": "Mendable.ai switches from Pinecone to Supabase for PostgreSQL vector embeddings.",
"description": "How Mendable.ai boosts efficiency and accuracy of chat powered search for documentation using Supabase with pg_vector.",
"imgUrl": "images/customers/logos/mendableai.png",
"logoUrl": "images/customers/logos/mendableai.png",
"organization": "Mendable.ai",
"url": "/customers/mendableai",
"path": "/customers/mendableai",
"postMeta": {
"name": "Paul Copplestone",
"avatarUrl": "https://avatars0.githubusercontent.com/u/10214025?v=4",
"publishDate": "Feb 16, 2023",
"readLength": 6
},
"ctaText": "View story"
},
{
"type": "Customer Story",
"title": "HappyTeams unlocks better performance and reduces cost with Supabase.",
+1 -1
View File
@@ -20,7 +20,7 @@ export const links = [
{
text: 'Open Source',
description: 'We support existing open source tools and communities wherever possible.',
url: '/oss',
url: '/open-source',
icon: 'M10.7607 3.08859C15.7723 2.38592 20.0295 5.94789 20.8525 10.7642C21.5964 15.1122 19.4634 19.1692 15.8778 20.9967L14.0736 16.084C15.129 15.4346 15.8232 14.4223 16.0198 13.0819C16.3702 10.7219 14.7442 8.47426 12.3801 8.25648C10.2228 8.04047 8.33304 9.61601 7.99823 11.7702L7.99761 11.7743C7.73427 13.5096 8.47082 15.1847 9.92827 16.0841L8.12693 21C6.34571 20.1066 4.88125 18.6129 3.97963 16.7387L3.97879 16.7369C3.15872 15.0395 3.07406 13.7081 3.01524 12.783C3.01012 12.7024 3.0052 12.6249 3 12.5506C3.10941 7.30353 6.59243 3.6764 10.7607 3.08859Z',
},
]
+1 -1
View File
@@ -82,7 +82,7 @@
},
{
"text": "Open Source",
"url": "/oss"
"url": "/open-source"
},
{
"text": "SupaSquad",
-8
View File
@@ -23,14 +23,6 @@
"label": "",
"url": "/storage"
},
"realtime": {
"name": "Realtime",
"icon": "M15.042 21.672L13.684 16.6m0 0l-2.51 2.225.569-9.47 5.227 7.917-3.286-.672zM12 2.25V4.5m5.834.166l-1.591 1.591M20.25 10.5H18M7.757 14.743l-1.59 1.59M6 10.5H3.75m4.007-4.243l-1.59-1.59",
"description": "Store, organize, and serve large files. Any media, including videos and images.",
"description_short": "",
"label": "",
"url": "/storage"
},
"edge-functions": {
"name": "Edge Functions",
"icon": "M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4",
+3
View File
@@ -1,5 +1,8 @@
export const APP_NAME = 'Supabase'
export const DESCRIPTION = 'The Open Source Alternative to Firebase.'
export const IS_PROD = process.env.NEXT_PUBLIC_VERCEL_ENV === 'production'
export const IS_PREVIEW = process.env.NEXT_PUBLIC_VERCEL_ENV === 'preview'
export const API_URL = process.env.NEXT_PUBLIC_API_URL
// Launch Weeek
export const SAMPLE_TICKET_NUMBER = 1234
+1 -3
View File
@@ -1,7 +1,5 @@
interface DataProps {
referrer?: string
title: string
route?: string
[prop: string]: any
}
export const post = (url: string, data: DataProps, options = {}) => {
+45
View File
@@ -0,0 +1,45 @@
import { TelemetryEvent } from './telemetry'
interface TelemetryEventType {
[key: string]: TelemetryEvent
}
const TelemetryEvents: TelemetryEventType = {
www_hp_hero_startProject: {
action: 'www_hp_hero_startProject',
category: 'link',
label: '',
},
www_hp_hero_documentation: {
action: 'www_hp_hero_documentation',
category: 'link',
label: '',
},
www_hp_subhero_products_database: {
action: 'www_hp_subhero_products_database',
category: 'link',
label: '',
},
www_hp_subhero_products_auth: {
action: 'www_hp_subhero_products_auth',
category: 'link',
label: '',
},
www_hp_subhero_products_storage: {
action: 'www_hp_subhero_products_storage',
category: 'link',
label: '',
},
www_hp_subhero_products_edgeFunctions: {
action: 'www_hp_subhero_products_edgeFunctions',
category: 'link',
label: '',
},
www_hp_subhero_products_realtime: {
action: 'www_hp_subhero_products_realtime',
category: 'link',
label: '',
},
}
export default TelemetryEvents
+58
View File
@@ -1496,6 +1496,11 @@ module.exports = [
source: '/docs/oss',
destination: '/oss',
},
{
permanent: true,
source: '/oss',
destination: '/open-source',
},
{
permanent: true,
source: '/docs/company/aup',
@@ -1913,4 +1918,57 @@ module.exports = [
source: '/docs/guides/cli/local-development',
destination: '/docs/guides/getting-started/local-development',
},
// old case studies moved to /customers
{
permanent: true,
source: '/blog/case-study-monitoro',
destination: '/customers',
},
{
permanent: true,
source: '/blog/case-study-tayfa',
destination: '/customers',
},
{
permanent: true,
source: '/blog/case-study-xendit',
destination: '/customers/xendit',
},
{
permanent: true,
source: '/blog/case-study-roboflow',
destination: '/customers',
},
{
permanent: true,
source: '/blog/toad-a-link-shortener-with-simple-apis-for-low-coders',
destination: '/customers',
},
{
permanent: true,
source: '/blog/epsilon3-self-hosting',
destination: '/customers/epsilon3',
},
{
permanent: true,
source: '/blog/spot-flutter-with-postgres',
destination: '/customers',
},
{
permanent: true,
source: '/blog/mobbin-supabase-200000-users',
destination: '/customers/mobbin',
},
{
permanent: true,
source: '/blog/replenysh-time-to-value-in-less-than-24-hours',
destination: '/customers/replenysh',
},
{
permanent: true,
source: '/blog/how-supabase-accelerates-development-of-all-pull-together',
destination: '/customers',
},
]
+42
View File
@@ -0,0 +1,42 @@
import { post } from '~/lib/fetchWrapper'
import { API_URL, IS_PROD, IS_PREVIEW } from 'lib/constants'
import { NextRouter } from 'next/router'
export interface TelemetryEvent {
category: string
action: string
label: string
value?: string
}
export interface TelemetryProps {
screenResolution?: string
language: string
}
// This event is the same as in studio/lib/telemetry.tx
// but uses different ENV variables for www
const sendEvent = (event: TelemetryEvent, gaProps: TelemetryProps, router: NextRouter) => {
if (!IS_PROD && !IS_PREVIEW) return
const { category, action, label, value } = event
return post(`${API_URL}/telemetry/event`, {
action: action,
category: category,
label: label,
value: value,
page_referrer: document?.referrer,
page_title: document?.title,
page_location: router.asPath,
ga: {
screen_resolution: gaProps?.screenResolution,
language: gaProps?.language,
},
})
}
export default {
sendEvent,
}
+9 -4
View File
@@ -1,22 +1,27 @@
import { APP_NAME, DESCRIPTION } from 'lib/constants'
import { API_URL, APP_NAME, DESCRIPTION } from 'lib/constants'
import { DefaultSeo } from 'next-seo'
import { AppProps } from 'next/app'
import { useRouter } from 'next/router'
import { useEffect } from 'react'
import Meta from '~/components/Favicons'
import '../styles/index.css'
import { post } from './../lib/fetchWrapper'
import { AuthProvider, ThemeProvider } from 'common'
import { post } from '~/lib/fetchWrapper'
import { AuthProvider, ThemeProvider, useTelemetryProps } from 'common'
import Head from 'next/head'
export default function MyApp({ Component, pageProps }: AppProps) {
const router = useRouter()
const telemetryProps = useTelemetryProps()
function handlePageTelemetry(route: string) {
return post(`https://api.supabase.io/platform/telemetry/page`, {
return post(`${API_URL}/telemetry/page`, {
referrer: document.referrer,
title: document.title,
route,
ga: {
screen_resolution: telemetryProps?.screenResolution,
language: telemetryProps?.language,
},
})
}
@@ -3,12 +3,12 @@ import { useRouter } from 'next/router'
import { Octokit } from '@octokit/core'
import DefaultLayout from '~/components/Layouts/Default'
import SectionContainer from '~/components/Layouts/SectionContainer'
import maintainers from '../data/maintainers.json'
import Sponsors from '../components/Sponsors'
import maintainers from '~/data/maintainers.json'
import Sponsors from '~/components/Sponsors'
import Image from 'next/image'
import { NextSeo } from 'next-seo'
export default function Oss() {
const OpenSource = () => {
const octokit = new Octokit()
const [activePill, setActivePill] = useState('All')
@@ -157,3 +157,5 @@ export default function Oss() {
</>
)
}
export default OpenSource
+4 -7
View File
@@ -174,10 +174,7 @@ function StoragePage() {
content={ApiExamples}
text={[
<p key={0}>Built from the ground up for interoperable authentication.</p>,
<p key={1}>
Fast and easy to implement using our powerful library clients. Asset optimization
and image transformation coming soon!
</p>,
<p key={1}>Fast and easy to implement using our powerful library clients.</p>,
]}
footer={[
<div className="my-8 grid grid-cols-12 gap-8 lg:gap-0 xl:gap-16" key={0}>
@@ -185,7 +182,7 @@ function StoragePage() {
<FeatureColumn
icon={<IconWifi />}
title="CDN"
text="Serve from the edge to reduce latency."
text="Serve from over 285 cities globally to reduce latency."
/>
<Link href="/docs/guides/storage/cdn" passHref>
<a>
@@ -203,8 +200,8 @@ function StoragePage() {
<div className="col-span-6 lg:col-span-12 xl:col-span-4">
<FeatureColumn
icon={<IconShuffle />}
title="Transformations"
text="Resize and compress your media before you serve it."
title="Image Optimizations and Transformations"
text="Resize and compress your media files on the fly."
/>
<Link href="/docs/guides/storage/image-transformations" passHref>
<a>
+13 -6
View File
@@ -5,42 +5,49 @@
<link>https://supabase.com</link>
<description>Latest news from Supabase</description>
<language>en</language>
<lastBuildDate>Wed, 15 Feb 2023 03:03:30 +0000</lastBuildDate>
<lastBuildDate>Thu, 04 May 2023 22:10:00 +0000</lastBuildDate>
<atom:link href="https://supabase.com/rss.xml" rel="self" type="application/rss+xml"/>
<item>
<guid>https://supabase.com/customers/mendableai</guid>
<title>Mendable.ai switches from Pinecone to Supabase for PostgreSQL vector embeddings.</title>
<link>https://supabase.com/customers/mendableai</link>
<description>How Mendable.ai boosts efficiency and accuracy of chat powered search for documentation using Supabase with pg_vector.</description>
<pubDate>Thu, 04 May 2023 22:10:00 +0000</pubDate>
</item>
<item>
<guid>https://supabase.com/customers/happyteams</guid>
<title>HappyTeams unlocks better performance and reduces cost with Supabase.</title>
<link>https://supabase.com/customers/happyteams</link>
<description>How a bootstrapped startup migrated from Heroku to Supabase in 30 minutes and never looked back.</description>
<pubDate>Wed, 15 Feb 2023 03:03:30 +0000</pubDate>
<pubDate>Wed, 15 Feb 2023 23:11:00 +0000</pubDate>
</item>
<item>
<guid>https://supabase.com/customers/xendit</guid>
<title>Xendit use Supabase and create a full solution shipped to production in less than one week.</title>
<link>https://supabase.com/customers/xendit</link>
<description>As a payment processor, Xendit are responsible for verifying that all transactions are legal.</description>
<pubDate>Mon, 13 Feb 2023 03:03:30 +0000</pubDate>
<pubDate>Mon, 13 Feb 2023 23:11:00 +0000</pubDate>
</item>
<item>
<guid>https://supabase.com/customers/replenysh</guid>
<title>Replenysh uses Supabase to implement OTP in less than 24 hours.</title>
<link>https://supabase.com/customers/replenysh</link>
<description>With Supabase, Replenysh gets a slick auth experience, reduces DevOps overhead, and continues to scale with Postgres.</description>
<pubDate>Mon, 13 Feb 2023 03:03:30 +0000</pubDate>
<pubDate>Mon, 13 Feb 2023 23:11:00 +0000</pubDate>
</item>
<item>
<guid>https://supabase.com/customers/mobbin</guid>
<title>How Mobbin migrated 200,000 users from Firebase for a better authentication experience.</title>
<link>https://supabase.com/customers/mobbin</link>
<description>Mobbin helps over 200,000 creators globally search and view the latest design patterns from well-known apps.</description>
<pubDate>Mon, 13 Feb 2023 03:03:30 +0000</pubDate>
<pubDate>Mon, 13 Feb 2023 23:11:00 +0000</pubDate>
</item>
<item>
<guid>https://supabase.com/customers/epsilon3</guid>
<title>Epsilon3 digitize paper-based procedures in the space industry using telemetry data, simplifying testing and operations.</title>
<link>https://supabase.com/customers/epsilon3</link>
<description>Epsilon3 uses Supabase to help teams execute secure and reliable operations in an industry where project spend runs into the billions.</description>
<pubDate>Mon, 13 Feb 2023 03:03:30 +0000</pubDate>
<pubDate>Mon, 13 Feb 2023 23:11:00 +0000</pubDate>
</item>
</channel>
Binary file not shown.

After

Width:  |  Height:  |  Size: 628 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 311 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 316 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 281 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 250 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 322 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 524 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 346 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 355 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 240 KiB

+33 -33
View File
@@ -9621,9 +9621,9 @@
}
},
"node_modules/@supabase/realtime-js": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.7.1.tgz",
"integrity": "sha512-WC0yPArBF/wPXwxKWTrRSMWWnFQCbhhUsX0u42x4OqUdDJtow6rzvDIZHWFZLh85UUBYIQ2++AabSNgzd3ubQg==",
"version": "2.7.2",
"resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.7.2.tgz",
"integrity": "sha512-Fi6xAl5PUkqnjl3wo4rdcQIbMG3+yTRX1aUZe/yfvTG84RMvmCXJ1yN6MmafVLeZpU1xkaz5Vx4L0tnHcLiy6w==",
"dependencies": {
"@types/phoenix": "^1.5.4",
"@types/websocket": "^1.0.3",
@@ -9636,23 +9636,23 @@
"integrity": "sha512-qP1N39xRhi00L6g0FYJCWKAbPQbiVsMP/E3y5zdivOiL2VUFkbLKXYfvzzwhGj3eKdr7O/N/2VtZXWXm+DCx0g=="
},
"node_modules/@supabase/storage-js": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.3.1.tgz",
"integrity": "sha512-BaPIvyvjuZW1V0CnfGKUZyzpBUXnsh0XD8eqTOYd+MdiGPmIPI0vtwnT4fAoK8mipp1vpcN62EVQaqeUnWXPtQ==",
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.5.1.tgz",
"integrity": "sha512-nkR0fQA9ScAtIKA3vNoPEqbZv1k5B5HVRYEvRWdlP6mUpFphM9TwPL2jZ/ztNGMTG5xT6SrHr+H7Ykz8qzbhjw==",
"dependencies": {
"cross-fetch": "^3.1.5"
}
},
"node_modules/@supabase/supabase-js": {
"version": "2.14.0",
"resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.14.0.tgz",
"integrity": "sha512-pdcYDaxTUqv5A8NqDnLn731rTP0Wm469H+0ag6Jb8YpWR95rmusfpGY7vrykWXvdnvCMxoG6eEwSF9vQtxVrow==",
"version": "2.21.0",
"resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.21.0.tgz",
"integrity": "sha512-FW3ZzBoc4orSgfX0dXrmJoXAcI/hiekmqXTkN64vjtUF2Urp3UjyAf71UTtV9Jl6ejHoe3K++e0+Rg9zKUJh5w==",
"dependencies": {
"@supabase/functions-js": "^2.1.0",
"@supabase/gotrue-js": "^2.18.1",
"@supabase/gotrue-js": "^2.23.0",
"@supabase/postgrest-js": "^1.1.1",
"@supabase/realtime-js": "^2.7.1",
"@supabase/storage-js": "^2.3.1",
"@supabase/realtime-js": "^2.7.2",
"@supabase/storage-js": "^2.5.1",
"cross-fetch": "^3.1.5"
}
},
@@ -10264,9 +10264,9 @@
"integrity": "sha512-kUNnecmtkunAoQ3CnjmMkzNU/gtxG8guhi+Fk2U/kOpIKjIMKnXGp4IJCgQJrXSgMsWYimYG4TGjz/UzbGEBTw=="
},
"node_modules/@types/phoenix": {
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.5.5.tgz",
"integrity": "sha512-1eWWT19k0L4ZiTvdXjAvJ9KvW0B8SdiVftQmFPJGTEx78Q4PCSIQDpz+EfkFVR1N4U9gREjlW4JXL8YCIlY0bw=="
"version": "1.5.6",
"resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.5.6.tgz",
"integrity": "sha512-e7jZ6I9uyRGsg7MNwQcarmBvRlbGb9DibbocE9crVnxqsy6C23RMxLWbJ2CQ3vgCW7taoL1L+F02EcjA6ld7XA=="
},
"node_modules/@types/prettier": {
"version": "2.7.2",
@@ -39181,7 +39181,7 @@
"@supabase/auth-helpers-react": "^0.3.1",
"@supabase/react-data-grid": "^7.1.0-beta.7",
"@supabase/shared-types": "^0.1.40",
"@supabase/supabase-js": "^2.13.0",
"@supabase/supabase-js": "^2.21.0",
"@tanstack/react-query": "^4.22.0",
"@tanstack/react-query-devtools": "^4.22.0",
"@zip.js/zip.js": "^2.6.61",
@@ -46322,9 +46322,9 @@
}
},
"@supabase/realtime-js": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.7.1.tgz",
"integrity": "sha512-WC0yPArBF/wPXwxKWTrRSMWWnFQCbhhUsX0u42x4OqUdDJtow6rzvDIZHWFZLh85UUBYIQ2++AabSNgzd3ubQg==",
"version": "2.7.2",
"resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.7.2.tgz",
"integrity": "sha512-Fi6xAl5PUkqnjl3wo4rdcQIbMG3+yTRX1aUZe/yfvTG84RMvmCXJ1yN6MmafVLeZpU1xkaz5Vx4L0tnHcLiy6w==",
"requires": {
"@types/phoenix": "^1.5.4",
"@types/websocket": "^1.0.3",
@@ -46337,23 +46337,23 @@
"integrity": "sha512-qP1N39xRhi00L6g0FYJCWKAbPQbiVsMP/E3y5zdivOiL2VUFkbLKXYfvzzwhGj3eKdr7O/N/2VtZXWXm+DCx0g=="
},
"@supabase/storage-js": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.3.1.tgz",
"integrity": "sha512-BaPIvyvjuZW1V0CnfGKUZyzpBUXnsh0XD8eqTOYd+MdiGPmIPI0vtwnT4fAoK8mipp1vpcN62EVQaqeUnWXPtQ==",
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.5.1.tgz",
"integrity": "sha512-nkR0fQA9ScAtIKA3vNoPEqbZv1k5B5HVRYEvRWdlP6mUpFphM9TwPL2jZ/ztNGMTG5xT6SrHr+H7Ykz8qzbhjw==",
"requires": {
"cross-fetch": "^3.1.5"
}
},
"@supabase/supabase-js": {
"version": "2.14.0",
"resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.14.0.tgz",
"integrity": "sha512-pdcYDaxTUqv5A8NqDnLn731rTP0Wm469H+0ag6Jb8YpWR95rmusfpGY7vrykWXvdnvCMxoG6eEwSF9vQtxVrow==",
"version": "2.21.0",
"resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.21.0.tgz",
"integrity": "sha512-FW3ZzBoc4orSgfX0dXrmJoXAcI/hiekmqXTkN64vjtUF2Urp3UjyAf71UTtV9Jl6ejHoe3K++e0+Rg9zKUJh5w==",
"requires": {
"@supabase/functions-js": "^2.1.0",
"@supabase/gotrue-js": "^2.18.1",
"@supabase/gotrue-js": "^2.23.0",
"@supabase/postgrest-js": "^1.1.1",
"@supabase/realtime-js": "^2.7.1",
"@supabase/storage-js": "^2.3.1",
"@supabase/realtime-js": "^2.7.2",
"@supabase/storage-js": "^2.5.1",
"cross-fetch": "^3.1.5"
}
},
@@ -46910,9 +46910,9 @@
"integrity": "sha512-kUNnecmtkunAoQ3CnjmMkzNU/gtxG8guhi+Fk2U/kOpIKjIMKnXGp4IJCgQJrXSgMsWYimYG4TGjz/UzbGEBTw=="
},
"@types/phoenix": {
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.5.5.tgz",
"integrity": "sha512-1eWWT19k0L4ZiTvdXjAvJ9KvW0B8SdiVftQmFPJGTEx78Q4PCSIQDpz+EfkFVR1N4U9gREjlW4JXL8YCIlY0bw=="
"version": "1.5.6",
"resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.5.6.tgz",
"integrity": "sha512-e7jZ6I9uyRGsg7MNwQcarmBvRlbGb9DibbocE9crVnxqsy6C23RMxLWbJ2CQ3vgCW7taoL1L+F02EcjA6ld7XA=="
},
"@types/prettier": {
"version": "2.7.2",
@@ -66384,7 +66384,7 @@
"@supabase/postgres-meta": "^0.64.4",
"@supabase/react-data-grid": "^7.1.0-beta.7",
"@supabase/shared-types": "^0.1.40",
"@supabase/supabase-js": "^2.13.0",
"@supabase/supabase-js": "^2.21.0",
"@tailwindcss/typography": "^0.5.2",
"@tanstack/react-query": "^4.22.0",
"@tanstack/react-query-devtools": "^4.22.0",
@@ -68107,7 +68107,7 @@
"@types/react": "^17.0.37",
"@types/react-copy-to-clipboard": "^5.0.4",
"@types/react-dom": "^17.0.11",
"@types/react-syntax-highlighter": "*",
"@types/react-syntax-highlighter": "^15.5.6",
"autoprefixer": "^10.4.2",
"clsx": "^1.2.1",
"cmdk-supabase": "^0.2.2",
+1
View File
@@ -1,2 +1,3 @@
export * from './useBreakpoint'
export * from './useParams'
export * from './useTelemetryProps'
@@ -0,0 +1,12 @@
import { useRouter } from 'next/router'
const isBrowser = typeof window !== 'undefined'
export function useTelemetryProps() {
const { locale } = useRouter()
return {
screenResolution: isBrowser ? `${window.innerWidth}x${window.innerHeight}` : undefined,
language: locale ?? 'en-US',
}
}
@@ -1,4 +1,3 @@
import React from 'react'
import type {
ChatCompletionResponseMessage,
CreateChatCompletionResponse,
@@ -16,14 +15,23 @@ import {
import { SSE } from 'sse.js'
import { Alert, Button, IconAlertTriangle, IconCornerDownLeft, IconUser, Input } from 'ui'
import {
Button,
IconAlertTriangle,
IconCornerDownLeft,
IconUser,
Input,
markdownComponents,
} from 'ui'
import { AiIcon, AiIconChat } from './Command.icons'
import { CommandGroup, CommandItem, useAutoInputFocus, useHistoryKeys } from './Command.utils'
import { useCommandMenu } from './CommandMenuProvider'
import { AiWarning } from './Command.alerts'
import { useCommandMenu } from './CommandMenuProvider'
import ReactMarkdown from 'react-markdown'
import { cn } from './../../utils/cn'
import remarkGfm from 'remark-gfm'
const questions = [
'How do I get started with Supabase?',
@@ -339,7 +347,7 @@ export function queryAi(messages: Message[], timeout = 0) {
}
const AiCommand = () => {
const { isLoading, setIsLoading, search, setSearch, MarkdownHandler } = useCommandMenu()
const { isLoading, setIsLoading, search, setSearch } = useCommandMenu()
const { submit, reset, messages, isResponding, hasError } = useAiChat({
setIsLoading,
@@ -404,7 +412,9 @@ const AiCommand = () => {
{message.status === MessageStatus.Pending ? (
<div className="bg-scale-700 h-[21px] w-[13px] mt-1 animate-pulse animate-bounce"></div>
) : (
<MarkdownHandler
<ReactMarkdown
remarkPlugins={[remarkGfm]}
components={markdownComponents}
linkTarget="_blank"
className="prose dark:prose-dark"
transformLinkUri={(href) => {
@@ -419,7 +429,7 @@ const AiCommand = () => {
}}
>
{message.content}
</MarkdownHandler>
</ReactMarkdown>
)}
</>
</div>
@@ -1,7 +1,6 @@
import { useTheme, UseThemeProps } from 'common'
import { createContext, PropsWithChildren, useContext, useEffect, useState } from 'react'
import CommandMenu from './CommandMenu'
import { ReactMarkdownOptions } from 'react-markdown/lib/react-markdown'
export interface CommandMenuContextValue {
isOpen: boolean
@@ -29,10 +28,6 @@ export interface CommandMenuContextValue {
*/
isOptedInToAI: boolean
// to do: remove this prop
// this is a temporary hack as ReactMarkdown fails our jest tests if we import the package within this UI package
MarkdownHandler: (props: ReactMarkdownOptions) => JSX.Element
// Optional callback to save a generated SQL output
saveGeneratedSQL?: (answer: string, title: string) => Promise<void>
}
@@ -66,11 +61,6 @@ export interface CommandMenuProviderProps {
* Any additional metadata that CMDK component can use in its AI prompts
*/
metadata?: { definitions?: string; flags?: { [key: string]: string } }
/**
* TODO: remove this prop, temporary hack as ReactMarkdown fails our jest tests
* if we import the package directly within this UI package
*/
MarkdownHandler: (props: ReactMarkdownOptions) => JSX.Element
/**
* Call back when save SQL snippet button is selected
*/
@@ -84,7 +74,6 @@ const CommandMenuProvider = ({
apiKeys,
metadata,
isOptedInToAI = false,
MarkdownHandler,
saveGeneratedSQL,
}: PropsWithChildren<CommandMenuProviderProps>) => {
const [isOpen, setIsOpen] = useState(false)
@@ -116,7 +105,6 @@ const CommandMenuProvider = ({
project,
metadata,
isOptedInToAI,
MarkdownHandler,
saveGeneratedSQL,
}}
>
+8
View File
@@ -560,6 +560,14 @@
"product": "auth",
"type": "function"
},
{
"id": "exchange-code-for-session",
"title": "Exchange an auth code for a session",
"slug": "auth-exchangecodeforsession",
"product": "auth",
"type": "function"
},
{
"id": "mfa-enroll",
"title": "Enroll a factor",
+20 -3
View File
@@ -400,6 +400,9 @@ functions:
- id: get-session
title: 'getSession()'
$ref: '@supabase/gotrue-js.GoTrueClient.getSession'
notes: |
- This method retrieves the current local session (i.e local storage).
- If the session has an expired access token, this method will use the refresh token to get a new session.
examples:
- id: get-the-session-data
name: Get the session data
@@ -412,8 +415,8 @@ functions:
title: 'getUser()'
$ref: '@supabase/gotrue-js.GoTrueClient.getUser'
notes: |
- This method gets the user object from the current session.
- Fetches the user object from the database instead of local session.
- This method fetches the user object from the database instead of local session.
- This method is useful for checking if the user is authorized because it validates the user's access token JWT on the server.
- Should be used only when you require the most current user data. For faster results, `getSession().session.user` is recommended.
examples:
- id: get-the-logged-in-user-with-the-current-existing-session
@@ -486,7 +489,7 @@ functions:
title: 'refreshSession()'
$ref: '@supabase/gotrue-js.GoTrueClient.refreshSession'
notes: |
- This method will refresh the session whether the current one is expired or not.
- This method will refresh and return a new session whether the current one is expired or not.
examples:
- id: refresh-session-using-the-current-session
name: Refresh session using the current session
@@ -567,6 +570,20 @@ functions:
if (event == 'USER_UPDATED') console.log('USER_UPDATED', session)
})
```
- id: exchange-code-for-session
title: 'exchangeCodeForSession()'
$ref: '@supabase/gotrue-js.GoTrueClient.exchangeCodeForSession'
notes: |
- Used when `flowType` is set to `pkce` in client options.
examples:
- id: exchange-auth-code
name: Exchange Auth Code
isSpotlight: true
code: |
```js
supabase.auth.exchangeCodeForSession('34e770dd-9ff9-416c-87fa-43b31d7ef225')
```
- id: mfa-enroll
title: 'mfa.enroll()'
$ref: '@supabase/gotrue-js.GoTrueMFAApi.enroll'
+9
View File
@@ -0,0 +1,9 @@
/**
* Opting for option 1 as described here:
* https://github.com/remarkjs/react-markdown/issues/635#issuecomment-956158474
*/
function ReactMarkdown({ children }) {
return <>{children}</>
}
export default ReactMarkdown
+1
View File
@@ -0,0 +1 @@
export default function remarkGfm() {}
@@ -98,9 +98,6 @@ const CommandMenuWrapper = observer(({ children }: PropsWithChildren<{}>) => {
site="studio"
projectRef={ref}
apiKeys={apiKeys}
MarkdownHandler={(props) => (
<ReactMarkdown remarkPlugins={[remarkGfm]} components={markdownComponents} {...props} />
)}
metadata={cmdkMetadata}
isOptedInToAI={allowCMDKDataOptIn && isOptedInToAI}
saveGeneratedSQL={onSaveGeneratedSQL}
@@ -8,6 +8,7 @@ export interface TaxId {
code: string
country: string
placeholder: string
vatPrefix?: string
}
export interface StripeTaxId {
@@ -30,6 +31,7 @@ export const TAX_IDS: TaxId[] = [
code: 'eu_vat',
country: 'Austria',
placeholder: 'ATU12345678',
vatPrefix: 'ATU',
},
{
name: 'AU ABN',
@@ -48,12 +50,14 @@ export const TAX_IDS: TaxId[] = [
code: 'eu_vat',
country: 'Belgium',
placeholder: 'BE0123456789',
vatPrefix: 'BE',
},
{
name: 'BG VAT',
code: 'eu_vat',
country: 'Bulgaria',
placeholder: 'BG0123456789',
vatPrefix: 'BG',
},
{
name: 'BR CNPJ',
@@ -108,6 +112,7 @@ export const TAX_IDS: TaxId[] = [
code: 'ch_vat',
country: 'Switzerland',
placeholder: 'CHE-123.456.789 MWST',
vatPrefix: 'CHE',
},
{
name: 'CL TIN',
@@ -120,30 +125,35 @@ export const TAX_IDS: TaxId[] = [
code: 'eu_vat',
country: 'Cyprus',
placeholder: 'CY12345678Z',
vatPrefix: 'CY',
},
{
name: 'CZ VAT',
code: 'eu_vat',
country: 'Czech Republic',
placeholder: 'CZ1234567890',
vatPrefix: 'CZ',
},
{
name: 'DE VAT',
code: 'eu_vat',
country: 'Germany',
placeholder: 'DE123456789',
vatPrefix: 'DE',
},
{
name: 'DK VAT',
code: 'eu_vat',
country: 'Denmark',
placeholder: 'DK12345678',
vatPrefix: 'DK',
},
{
name: 'EE VAT',
code: 'eu_vat',
country: 'Estonia',
placeholder: 'EE123456789',
vatPrefix: 'EE',
},
{
name: 'ES CIF',
@@ -155,25 +165,29 @@ export const TAX_IDS: TaxId[] = [
name: 'ES VAT',
code: 'eu_vat',
country: 'Spain',
placeholder: 'ESA1234567Z',
placeholder: 'ESX1234567Z',
vatPrefix: 'ES',
},
{
name: 'FI VAT',
code: 'eu_vat',
country: 'Finland',
placeholder: 'FI12345678',
vatPrefix: 'FI',
},
{
name: 'FR VAT',
code: 'eu_vat',
country: 'France',
placeholder: 'FRAB123456789',
vatPrefix: 'FR',
},
{
name: 'GB VAT',
code: 'eu_vat',
country: 'United Kingdom',
placeholder: 'GB123456789',
vatPrefix: 'GB',
},
{
name: 'GE VAT',
@@ -186,6 +200,7 @@ export const TAX_IDS: TaxId[] = [
code: 'eu_vat',
country: 'Greece',
placeholder: 'EL123456789',
vatPrefix: 'EL',
},
{
name: 'HK BR',
@@ -198,12 +213,14 @@ export const TAX_IDS: TaxId[] = [
code: 'eu_vat',
country: 'Croatia',
placeholder: 'HR12345678912',
vatPrefix: 'HR',
},
{
name: 'HU VAT',
code: 'eu_vat',
country: 'Hungary',
placeholder: 'HU12345678912',
vatPrefix: 'HU',
},
{
name: 'ID NPWP',
@@ -216,6 +233,7 @@ export const TAX_IDS: TaxId[] = [
code: 'eu_vat',
country: 'Ireland',
placeholder: 'IE1234567AB',
vatPrefix: 'IE',
},
{
name: 'IL VAT',
@@ -240,6 +258,7 @@ export const TAX_IDS: TaxId[] = [
code: 'eu_vat',
country: 'Italy',
placeholder: 'IT12345678912',
vatPrefix: 'IT',
},
{
name: 'JP CN',
@@ -270,24 +289,28 @@ export const TAX_IDS: TaxId[] = [
code: 'eu_vat',
country: 'Lithuania',
placeholder: 'LT123456789123',
vatPrefix: 'LT',
},
{
name: 'LU VAT',
code: 'eu_vat',
country: 'Luxembourg',
placeholder: 'LU12345678',
vatPrefix: 'LU',
},
{
name: 'LV VAT',
code: 'eu_vat',
country: 'Latvia',
placeholder: 'LV12345678912',
vatPrefix: 'LV',
},
{
name: 'MT VAT',
code: 'eu_vat',
country: 'Malta',
placeholder: 'MT12345678',
vatPrefix: 'MT',
},
{
name: 'MX RFC',
@@ -318,6 +341,7 @@ export const TAX_IDS: TaxId[] = [
code: 'eu_vat',
country: 'Netherlands',
placeholder: 'NL123456789B12',
vatPrefix: 'NL',
},
{
name: 'NO VAT',
@@ -336,18 +360,21 @@ export const TAX_IDS: TaxId[] = [
code: 'eu_vat',
country: 'Poland',
placeholder: 'PL1234567890',
vatPrefix: 'PL',
},
{
name: 'PT VAT',
code: 'eu_vat',
country: 'Portugal',
placeholder: 'PT123456789',
vatPrefix: 'PT',
},
{
name: 'RO VAT',
code: 'eu_vat',
country: 'Romania',
placeholder: 'RO1234567891',
vatPrefix: 'RO',
},
{
name: 'RU INN',
@@ -372,6 +399,7 @@ export const TAX_IDS: TaxId[] = [
code: 'eu_vat',
country: 'Sweden',
placeholder: 'SE123456789123',
vatPrefix: 'SE',
},
{
name: 'SG GST',
@@ -390,12 +418,14 @@ export const TAX_IDS: TaxId[] = [
code: 'eu_vat',
country: 'Slovenia',
placeholder: 'SI12345678',
vatPrefix: 'SI',
},
{
name: 'SK VAT',
code: 'eu_vat',
country: 'Slovakia',
placeholder: 'SK1234567891',
vatPrefix: 'SK',
},
{
name: 'TH VAT',
@@ -426,6 +456,7 @@ export const TAX_IDS: TaxId[] = [
code: 'eu_vat',
country: 'United Kingdom (Northern Ireland)',
placeholder: 'XI123456789',
vatPrefix: 'XI',
},
{
name: 'ZA VAT',
@@ -11,12 +11,11 @@ export const sanitizeTaxID = (taxId: StripeTaxId) => {
const selectedTaxId = TAX_IDS.find((option) => option.name === taxId.name)
// The prefix of the country name (ex: "BE" for Belgium, "ESA" for Spain)")
const countryPrefix = selectedTaxId?.placeholder.match(/[a-z]+|[^a-z]+/gi)?.[0]
const vatIdPrefix = selectedTaxId?.vatPrefix
// if the value doesn't start with the country code, prepend them
if (taxId.type === 'eu_vat' && countryPrefix && !taxId.value.startsWith(countryPrefix)) {
newID.value = `${countryPrefix}${taxId.value}`
// if the value doesn't start with the prefix, prepend them
if (vatIdPrefix && !taxId.value.startsWith(vatIdPrefix)) {
newID.value = `${vatIdPrefix}${taxId.value}`
}
return newID
@@ -8,7 +8,7 @@ import { useProfileQuery } from 'data/profile/profile-query'
import { SQL_TEMPLATES } from 'components/interfaces/SQLEditor/SQLEditor.constants'
import SQLCard from './SQLCard'
import { createSqlSnippetSkeleton } from '../SQLEditor.utils'
import { useParams } from 'common'
import { useTelemetryProps, useParams } from 'common'
import { useRouter } from 'next/router'
import { SqlSnippet } from 'data/content/sql-snippets-query'
import { useSqlEditorStateSnapshot } from 'state/sql-editor'
@@ -21,6 +21,7 @@ const SQLTemplates = observer(() => {
const { data: profile } = useProfileQuery()
const [sql, quickStart] = partition(SQL_TEMPLATES, { type: 'template' })
const telemetryProps = useTelemetryProps()
const snap = useSqlEditorStateSnapshot()
const canCreateSQLSnippet = checkPermissions(PermissionAction.CREATE, 'user_content', {
resource: { type: 'sql', owner_id: profile?.id },
@@ -77,7 +78,8 @@ const SQLTemplates = observer(() => {
action: 'script_clicked',
label: x.title,
},
ui.googleAnalyticsProps
telemetryProps,
router
)
}}
/>
@@ -111,7 +113,8 @@ const SQLTemplates = observer(() => {
action: 'quickstart_clicked',
label: x.title,
},
ui.googleAnalyticsProps
telemetryProps,
router
)
}}
/>
@@ -9,6 +9,8 @@ import { CSVLink } from 'react-csv'
import { useSqlEditorStateSnapshot } from 'state/sql-editor'
// @ts-ignore
import MarkdownTable from 'markdown-table'
import { useTelemetryProps } from 'common'
import { useRouter } from 'next/router'
export type ResultsDropdownProps = {
id: string
@@ -17,9 +19,11 @@ export type ResultsDropdownProps = {
const ResultsDropdown = ({ id }: ResultsDropdownProps) => {
const { project } = useProjectContext()
const snap = useSqlEditorStateSnapshot()
const telemetryProps = useTelemetryProps()
const result = snap.results?.[id]?.[0] ?? undefined
const { ui } = useStore()
const csvRef = useRef<CSVLink & HTMLAnchorElement & { link: HTMLAnchorElement }>(null)
const router = useRouter()
const csvData = useMemo(
() => (result?.rows ? compact(Array.from(result.rows || [])) : ''),
@@ -30,7 +34,8 @@ const ResultsDropdown = ({ id }: ResultsDropdownProps) => {
csvRef.current?.link.click()
Telemetry.sendEvent(
{ category: 'sql_editor', action: 'sql_download_csv', label: '' },
ui.googleAnalyticsProps
telemetryProps,
router
)
}
@@ -53,7 +58,8 @@ const ResultsDropdown = ({ id }: ResultsDropdownProps) => {
ui.setNotification({ category: 'success', message: 'Copied results to clipboard' })
Telemetry.sendEvent(
{ category: 'sql_editor', action: 'sql_copy_as_markdown', label: '' },
ui.googleAnalyticsProps
telemetryProps,
router
)
})
}
@@ -21,6 +21,7 @@ import {
} from 'components/to-be-cleaned/Storage/StorageSettings/StorageSettings.utils'
import { useStore } from 'hooks'
import { useParams } from 'common'
import { IS_PLATFORM } from 'lib/constants'
import { useProjectStorageConfigQuery } from 'data/config/project-storage-config-query'
import { useRouter } from 'next/router'
@@ -36,7 +37,7 @@ const CreateBucketModal = ({ visible, onClose }: CreateBucketModalProps) => {
const storageExplorerStore = useStorageStore()
const { createBucket } = storageExplorerStore
const { data } = useProjectStorageConfigQuery({ projectRef: ref })
const { data } = useProjectStorageConfigQuery({ projectRef: ref }, { enabled: IS_PLATFORM })
const { value, unit } = convertFromBytes(data?.fileSizeLimit ?? 0)
const formattedGlobalUploadLimit = `${value} ${unit}`
@@ -203,17 +204,19 @@ const CreateBucketModal = ({ visible, onClose }: CreateBucketModalProps) => {
))}
</Listbox>
</div>
<div className="col-span-12">
<p className="text-scale-1000 text-sm">
Note: The{' '}
<Link href={`/project/${ref}/settings/storage`}>
<a className="text-brand-900 opacity-80 hover:opacity-100 transition">
global upload limit
</a>
</Link>{' '}
takes precedence over this value ({formattedGlobalUploadLimit})
</p>
</div>
{IS_PLATFORM && (
<div className="col-span-12">
<p className="text-scale-1000 text-sm">
Note: The{' '}
<Link href={`/project/${ref}/settings/storage`}>
<a className="text-brand-900 opacity-80 hover:opacity-100 transition">
global upload limit
</a>
</Link>{' '}
takes precedence over this value ({formattedGlobalUploadLimit})
</p>
</div>
)}
</div>
)}
</div>
@@ -22,6 +22,7 @@ import {
import { useStore } from 'hooks'
import { useParams } from 'common'
import { useProjectStorageConfigQuery } from 'data/config/project-storage-config-query'
import { IS_PLATFORM } from 'lib/constants'
export interface EditBucketModalProps {
visible: boolean
@@ -35,7 +36,7 @@ const EditBucketModal = ({ visible, bucket, onClose }: EditBucketModalProps) =>
const storageExplorerStore = useStorageStore()
const { editBucket } = storageExplorerStore
const { data } = useProjectStorageConfigQuery({ projectRef: ref })
const { data } = useProjectStorageConfigQuery({ projectRef: ref }, { enabled: IS_PLATFORM })
const { value, unit } = convertFromBytes(data?.fileSizeLimit ?? 0)
const formattedGlobalUploadLimit = `${value} ${unit}`
@@ -226,17 +227,19 @@ const EditBucketModal = ({ visible, bucket, onClose }: EditBucketModalProps) =>
))}
</Listbox>
</div>
<div className="col-span-12">
<p className="text-scale-1000 text-sm">
Note: The{' '}
<Link href={`/project/${ref}/settings/storage`}>
<a className="text-brand-900 opacity-80 hover:opacity-100 transition">
global upload limit
</a>
</Link>{' '}
takes precedence over this value ({formattedGlobalUploadLimit})
</p>
</div>
{IS_PLATFORM && (
<div className="col-span-12">
<p className="text-scale-1000 text-sm">
Note: The{' '}
<Link href={`/project/${ref}/settings/storage`}>
<a className="text-brand-900 opacity-80 hover:opacity-100 transition">
global upload limit
</a>
</Link>{' '}
takes precedence over this value ({formattedGlobalUploadLimit})
</p>
</div>
)}
</div>
)}
</div>
@@ -0,0 +1,48 @@
import Head from 'next/head'
import Image from 'next/image'
import { useTheme } from 'common'
import { PropsWithChildren } from 'react'
import { BASE_PATH } from 'lib/constants'
import Divider from 'components/ui/Divider'
export interface APIAuthorizationLayoutProps {}
const APIAuthorizationLayout = ({ children }: PropsWithChildren<APIAuthorizationLayoutProps>) => {
const { isDarkMode } = useTheme()
return (
<>
<Head>
<title>Authorize API access | Supabase</title>
</Head>
<main className="flex-grow flex flex-col w-full h-full overflow-y-auto">
<div>
<div className="mx-auto px-4 sm:px-6">
<div className="max-w-xl flex justify-between items-center mx-auto py-4">
<div className="flex justify-start lg:w-0 lg:flex-1">
<div>
<span className="sr-only">Supabase</span>
<Image
src={
isDarkMode
? `${BASE_PATH}/img/supabase-dark.svg`
: `${BASE_PATH}/img/supabase-light.svg`
}
alt="Supabase Logo"
height={20}
width={105}
/>
</div>
</div>
</div>
</div>
</div>
<Divider light />
<div className="flex flex-col justify-center flex-grow mx-auto max-w-[90vw] md:max-w-xl h-full space-y-4">
{children}
</div>
</main>
</>
)
}
export default APIAuthorizationLayout
@@ -1,8 +1,8 @@
import { useState } from 'react'
import * as Tooltip from '@radix-ui/react-tooltip'
import { Button, Form, IconClock, Input, Listbox } from 'ui'
import { checkPermissions, useStore } from 'hooks'
import { IS_PLATFORM } from 'lib/constants'
import { useProjectStorageConfigQuery } from 'data/config/project-storage-config-query'
import { useProjectStorageConfigUpdateUpdateMutation } from 'data/config/project-storage-config-update-mutation'
import UpgradeToPro from 'components/ui/UpgradeToPro'
@@ -15,7 +15,7 @@ export type StorageSettingsProps = {
}
const StorageSettings = ({ projectRef }: StorageSettingsProps) => {
const { data, error } = useProjectStorageConfigQuery({ projectRef })
const { data, error } = useProjectStorageConfigQuery({ projectRef }, { enabled: IS_PLATFORM })
if (error || data?.error) {
return (
+4 -4
View File
@@ -1,4 +1,4 @@
import { useStore } from 'hooks'
import { useTelemetryProps } from 'common'
import { post } from 'lib/common/fetch'
import { API_URL, IS_PLATFORM } from 'lib/constants'
import { observer } from 'mobx-react-lite'
@@ -25,7 +25,7 @@ function sanitizePageViewRoute(_route?: string) {
const PageTelemetry: FC = ({ children }) => {
const router = useRouter()
const { ui } = useStore()
const telemetryProps = useTelemetryProps()
useEffect(() => {
function handleRouteChange(url: string) {
@@ -71,8 +71,8 @@ const PageTelemetry: FC = ({ children }) => {
title: document.title,
route,
ga: {
screen_resolution: ui.googleAnalyticsProps?.screenResolution,
language: ui.googleAnalyticsProps?.language,
screen_resolution: telemetryProps?.screenResolution,
language: telemetryProps?.language,
},
})
}
@@ -0,0 +1,42 @@
import { useMutation, UseMutationOptions } from '@tanstack/react-query'
import { post } from 'lib/common/fetch'
import { API_ADMIN_URL } from 'lib/constants'
export type ApiAuthorizationApproveVariables = {
id: string
organization_id: string
}
export type ApiAuthorizationApproveResponse = {
url: string
}
export async function approveApiAuthorization({
id,
organization_id,
}: ApiAuthorizationApproveVariables) {
if (!id) throw new Error('Authorization ID is required')
if (!organization_id) throw new Error('Organization slug is required')
const response = await post(
`${API_ADMIN_URL}/oauth/authorizations/${id}?skip_browser_redirect=true`,
{ organization_id }
)
if (response.error) throw response.error
return response as ApiAuthorizationApproveResponse
}
type ApiAuthorizationApproveData = Awaited<ReturnType<typeof approveApiAuthorization>>
export const useApiAuthorizationApproveMutation = ({
onSuccess,
...options
}: Omit<
UseMutationOptions<ApiAuthorizationApproveData, unknown, ApiAuthorizationApproveVariables>,
'mutationFn'
> = {}) => {
return useMutation<ApiAuthorizationApproveData, unknown, ApiAuthorizationApproveVariables>(
(vars) => approveApiAuthorization(vars),
options
)
}
@@ -0,0 +1,34 @@
import { useMutation, UseMutationOptions } from '@tanstack/react-query'
import { delete_ } from 'lib/common/fetch'
import { API_ADMIN_URL } from 'lib/constants'
export type ApiAuthorizationDeclineVariables = {
id: string
}
export type ApiAuthorizationDeclineResponse = {
id: string
}
export async function declineApiAuthorization({ id }: ApiAuthorizationDeclineVariables) {
if (!id) throw new Error('Authorization ID is required')
const response = await delete_(`${API_ADMIN_URL}/oauth/authorizations/${id}`, {})
if (response.error) throw response.error
return response as ApiAuthorizationDeclineResponse
}
type ApiAuthorizationDeclineData = Awaited<ReturnType<typeof declineApiAuthorization>>
export const useApiAuthorizationDeclineMutation = ({
onSuccess,
...options
}: Omit<
UseMutationOptions<ApiAuthorizationDeclineData, unknown, ApiAuthorizationDeclineVariables>,
'mutationFn'
> = {}) => {
return useMutation<ApiAuthorizationDeclineData, unknown, ApiAuthorizationDeclineVariables>(
(vars) => declineApiAuthorization(vars),
options
)
}
@@ -0,0 +1,57 @@
import { useQuery, useQueryClient, UseQueryOptions } from '@tanstack/react-query'
import { get } from 'lib/common/fetch'
import { API_ADMIN_URL } from 'lib/constants'
import { useCallback } from 'react'
import { resourceKeys } from './keys'
export type ApiAuthorizationVariables = {
id?: string
}
export type ApiAuthorizationResponse = {
name: string
website: string
domain: string
expires_at: string
approved_at: string | null
approved_organization_slug?: string
}
export async function getApiAuthorizationDetails(
{ id }: ApiAuthorizationVariables,
signal?: AbortSignal
) {
if (!id) throw new Error('Authorization ID is required')
const response = await get(`${API_ADMIN_URL}/oauth/authorizations/${id}`, { signal })
if (response.error) throw response.error
return response as ApiAuthorizationResponse
}
export type ResourceData = Awaited<ReturnType<typeof getApiAuthorizationDetails>>
export type ResourceError = { errorEventId: string; message: string }
export const useApiAuthorizationQuery = <TData = ResourceData>(
{ id }: ApiAuthorizationVariables,
{ enabled = true, ...options }: UseQueryOptions<ResourceData, ResourceError, TData> = {}
) =>
useQuery<ResourceData, ResourceError, TData>(
resourceKeys.resource(id),
({ signal }) => getApiAuthorizationDetails({ id }, signal),
{
enabled: enabled && typeof id !== 'undefined',
...options,
}
)
export const useApiAuthorizationPrefetch = ({ id }: ApiAuthorizationVariables) => {
const client = useQueryClient()
return useCallback(() => {
if (id) {
client.prefetchQuery(resourceKeys.resource(id), ({ signal }) =>
getApiAuthorizationDetails({ id }, signal)
)
}
}, [id])
}
+3
View File
@@ -0,0 +1,3 @@
export const resourceKeys = {
resource: (id: string | undefined) => ['api-authorization', id] as const,
}
+1
View File
@@ -1,4 +1,5 @@
export const organizationKeys = {
list: () => ['organizations'] as const,
detail: (slug: string | undefined) => ['organizations', slug, 'detail'] as const,
roles: (slug: string | undefined) => ['organizations', slug, 'roles'] as const,
freeProjectLimitCheck: (slug: string | undefined) =>
@@ -0,0 +1,37 @@
import { useQuery, useQueryClient, UseQueryOptions } from '@tanstack/react-query'
import { get } from 'lib/common/fetch'
import { API_URL } from 'lib/constants'
import { useCallback } from 'react'
import { Organization } from 'types'
import { organizationKeys } from './keys'
export type OrganizationsResponse = Organization[]
export async function getOrganizations(signal?: AbortSignal) {
const data = await get(`${API_URL}/organizations`, { signal })
if (data.error) throw data.error
return data as OrganizationsResponse
}
export type OrganizationsData = Awaited<ReturnType<typeof getOrganizations>>
export type OrganizationsError = unknown
export const useOrganizationsQuery = <TData = OrganizationsData>({
enabled = true,
...options
}: UseQueryOptions<OrganizationsData, OrganizationsError, TData> = {}) =>
useQuery<OrganizationsData, OrganizationsError, TData>(
organizationKeys.list(),
({ signal }) => getOrganizations(signal),
{ enabled: enabled, ...options }
)
export const useOrganizationsPrefetch = () => {
const client = useQueryClient()
return useCallback(
() => client.prefetchQuery(organizationKeys.list(), ({ signal }) => getOrganizations(signal)),
[]
)
}
+4 -2
View File
@@ -1,18 +1,20 @@
import {
AuthContext as AuthContextInternal,
AuthProvider as AuthProviderInternal,
AuthProviderProps,
gotrueClient,
useTelemetryProps,
} from 'common'
import { useProfileQuery } from 'data/profile/profile-query'
import { useStore } from 'hooks'
import { PropsWithChildren, useEffect } from 'react'
import { GOTRUE_ERRORS, IS_PLATFORM } from './constants'
import Telemetry from 'lib/telemetry'
export const AuthContext = AuthContextInternal
export const AuthProvider = ({ children }: PropsWithChildren<{}>) => {
const { ui, app } = useStore()
const telemetryProps = useTelemetryProps()
// Check for unverified GitHub users after a GitHub sign in
useEffect(() => {
@@ -34,7 +36,7 @@ export const AuthProvider = ({ children }: PropsWithChildren<{}>) => {
// Track telemetry for the current user
useProfileQuery({
onSuccess(profile) {
ui.setProfile(profile)
Telemetry.sendIdentify(profile, telemetryProps)
// [Joshen] Temp fix: For new users, the GET profile call also creates a default org
// But because the dashboard's logged in state is using gotrue as the source of truth
+8 -3
View File
@@ -1,8 +1,9 @@
import { post } from 'lib/common/fetch'
import { API_URL, IS_PLATFORM } from 'lib/constants'
import { User } from 'types'
import { NextRouter } from 'next/router'
export interface GoogleAnalyticsProps {
export interface TelemetryProps {
screenResolution?: string
language: string
}
@@ -14,7 +15,8 @@ const sendEvent = (
label: string
value?: string
},
gaProps?: GoogleAnalyticsProps
gaProps: TelemetryProps,
router: NextRouter
) => {
if (!IS_PLATFORM) return
@@ -25,6 +27,9 @@ const sendEvent = (
category: category,
label: label,
value: value,
page_referrer: document?.referrer,
page_title: document?.title,
page_location: router.asPath,
ga: {
screen_resolution: gaProps?.screenResolution,
language: gaProps?.language,
@@ -36,7 +41,7 @@ const sendEvent = (
* TODO: GA4 doesn't have identify method.
* We may or may not need gaClientId here. Confirm later
*/
const sendIdentify = (user: User, gaProps?: GoogleAnalyticsProps) => {
const sendIdentify = (user: User, gaProps?: TelemetryProps) => {
if (!IS_PLATFORM) return
return post(`${API_URL}/telemetry/identify`, {
+1 -1
View File
@@ -33,7 +33,7 @@
"@supabase/auth-helpers-react": "^0.3.1",
"@supabase/react-data-grid": "^7.1.0-beta.7",
"@supabase/shared-types": "^0.1.40",
"@supabase/supabase-js": "^2.13.0",
"@supabase/supabase-js": "^2.21.0",
"@tanstack/react-query": "^4.22.0",
"@tanstack/react-query-devtools": "^4.22.0",
"@zip.js/zip.js": "^2.6.61",
@@ -22,10 +22,16 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
const handlePatch = async (req: NextApiRequest, res: NextApiResponse) => {
const { id } = req.query
const { public: isPublicBucket } = req.body
const {
public: isPublicBucket,
allowed_mime_types: allowedMimeTypes,
file_size_limit: fileSizeLimit,
} = req.body
const { data, error } = await supabase.storage.updateBucket(id as string, {
public: isPublicBucket,
allowedMimeTypes,
fileSizeLimit,
})
if (error) {
return res.status(400).json({ error: { message: error.message } })
@@ -27,5 +27,10 @@ const handlePost = async (req: NextApiRequest, res: NextApiResponse) => {
return res.status(400).json({ error: { message: error.message } })
}
// change the domain name to the SUPABASE_PUBLIC_URL since SUPABASE_URL is not accessible from the client
const signedUrl = new URL(data.signedUrl)
signedUrl.hostname = new URL(process.env.SUPABASE_PUBLIC_URL!).hostname
data.signedUrl = signedUrl.href
return res.status(200).json(data)
}
+232
View File
@@ -0,0 +1,232 @@
import { useParams } from 'common'
import APIAuthorizationLayout from 'components/layouts/APIAuthorizationLayout'
import { FormPanel } from 'components/ui/Forms'
import ShimmeringLoader from 'components/ui/ShimmeringLoader'
import { useApiAuthorizationApproveMutation } from 'data/api-authorization/api-authorization-approve-mutation'
import { useApiAuthorizationDeclineMutation } from 'data/api-authorization/api-authorization-decline-mutation'
import { useApiAuthorizationQuery } from 'data/api-authorization/api-authorization-query'
import { useOrganizationsQuery } from 'data/organizations/organizations-query'
import dayjs from 'dayjs'
import { useStore, withAuth } from 'hooks'
import { useRouter } from 'next/router'
import { useEffect, useState } from 'react'
import { NextPageWithLayout } from 'types'
import { Alert, Button, Listbox } from 'ui'
// Need to handle if no organizations in account
// Need to handle if not logged in yet state
const APIAuthorizationPage: NextPageWithLayout = () => {
const { ui } = useStore()
const router = useRouter()
const { auth_id } = useParams()
const [isSubmitting, setIsSubmitting] = useState(false)
const [selectedOrg, setSelectedOrg] = useState<string>()
const { data: organizations, isLoading: isLoadingOrganizations } = useOrganizationsQuery()
const { data: requester, isLoading, isError, error } = useApiAuthorizationQuery({ id: auth_id })
const isApproved = requester?.approved_at !== null
const isExpired = dayjs().isAfter(dayjs(requester?.expires_at))
const { mutateAsync: approveRequest } = useApiAuthorizationApproveMutation()
const { mutateAsync: declineRequest } = useApiAuthorizationDeclineMutation()
useEffect(() => {
if (!isLoadingOrganizations) {
setSelectedOrg(organizations?.[0].slug ?? undefined)
}
}, [isLoadingOrganizations])
const onApproveRequest = async () => {
if (!auth_id) {
return ui.setNotification({
category: 'error',
message: 'Unable to approve request: auth_id is missing ',
})
}
if (!selectedOrg) {
return ui.setNotification({
category: 'error',
message: 'Unable to approve request: No organization selected',
})
}
try {
setIsSubmitting(true)
const res = await approveRequest({ id: auth_id, organization_id: selectedOrg })
router.push(res.url)
} catch (error: any) {
ui.setNotification({
category: 'error',
message: `Failed to approve request: ${error.message}`,
})
setIsSubmitting(false)
}
}
const onDeclineRequest = async () => {
if (!auth_id)
return ui.setNotification({
category: 'error',
message: 'Unable to decline request: auth_id is missing ',
})
try {
setIsSubmitting(true)
await declineRequest({ id: auth_id })
ui.setNotification({ category: 'success', message: 'Declined API authorization request' })
router.push('/projects')
} catch (error: any) {
ui.setNotification({
category: 'error',
message: `Failed to decline request: ${error.message}`,
})
setIsSubmitting(false)
}
}
if (isLoading) {
return (
<FormPanel header={<p>Authorize API access</p>}>
<div className="w-[500px] px-8 py-6 space-y-2">
<ShimmeringLoader />
<ShimmeringLoader className="w-3/4" />
<ShimmeringLoader className="w-1/2" />
</div>
</FormPanel>
)
}
if (auth_id === undefined) {
return (
<FormPanel header={<p>Authorization for API access</p>}>
<div className="w-[500px] px-8 py-6">
<Alert withIcon variant="warning" title="Missing authorization ID">
Please provide a valid authorization ID in the URL
</Alert>
</div>
</FormPanel>
)
}
if (isError) {
return (
<FormPanel header={<p>Authorize API access</p>}>
<div className="w-[500px] px-8 py-6">
<Alert
withIcon
variant="warning"
title="Failed to fetch details for API authorization request"
>
<p>Please retry your authorization request from the requesting app</p>
{error !== undefined && <p className="mt-2">Error: {error?.message}</p>}
</Alert>
</div>
</FormPanel>
)
}
if (isApproved) {
const approvedOrganization = organizations?.find(
(org) => org.slug === requester.approved_organization_slug
)
return (
<FormPanel header={<p>Authorize API access for {requester?.name}</p>}>
<div className="w-full md:w-[500px] px-8 py-6 space-y-8">
<Alert withIcon variant="success" title="This authorization request has been approved">
<p>
{requester.name} has read and write access to the organization "
{approvedOrganization?.name ?? 'Unknown'}" and all of its projects
</p>
<p className="mt-2">
Approved on: {dayjs(requester.approved_at).format('DD MMM YYYY HH:mm:ss (ZZ)')}
</p>
</Alert>
</div>
</FormPanel>
)
}
return (
<FormPanel
header={<p>Authorize API access for {requester?.name}</p>}
footer={
<div className="flex items-center justify-end py-4 px-8">
<div className="flex items-center space-x-2">
<Button type="default" disabled={isSubmitting || isExpired} onClick={onDeclineRequest}>
Decline
</Button>
<Button
loading={isSubmitting}
disabled={isSubmitting || isExpired}
onClick={onApproveRequest}
>
Authorize {requester?.name}
</Button>
</div>
</div>
}
>
<div className="w-full md:w-[500px] px-8 py-6 space-y-8">
{/* API Authorization requester details */}
<div className="flex space-x-4">
<div>
<div className="rounded-md border border-scale-600 p-2.5 flex items-center">
<div
className="w-8 h-8 md:w-10 md:h-10 bg-center bg-no-repeat bg-cover flex items-center justify-center"
// [Joshen] For when we support icons
// style={{ backgroundImage: `url('${requester?.icon}')` }}
>
<p className="text-scale-1000 text-lg">{requester?.name[0]}</p>
</div>
</div>
</div>
<p className="text-sm text-scale-1100">
{requester?.name} ({requester?.domain}) is requesting API access to an organization. The
application will be able to{' '}
<span className="text-amber-1200">
read and write the organization's settings and all of its projects.
</span>
</p>
</div>
{/* Expiry warning */}
{isExpired && (
<Alert withIcon variant="warning" title="This authorization request is expired">
Please retry your authorization request from the requesting app
</Alert>
)}
{/* Organization selection */}
{isLoadingOrganizations ? (
<div className="py-4 space-y-2">
<ShimmeringLoader />
<ShimmeringLoader className="w-3/4" />
</div>
) : (
<Listbox
label="Select an organization to grant API access to"
value={selectedOrg}
disabled={isExpired}
onChange={setSelectedOrg}
>
{(organizations ?? []).map((organization) => (
<Listbox.Option
key={organization.id}
label={organization.name}
value={organization.slug}
>
{organization.name}
</Listbox.Option>
))}
</Listbox>
)}
</div>
</FormPanel>
)
}
APIAuthorizationPage.getLayout = (page) => <APIAuthorizationLayout>{page}</APIAuthorizationLayout>
export default withAuth(APIAuthorizationPage)
+1 -16
View File
@@ -1,8 +1,7 @@
import { uuidv4 } from 'lib/helpers'
import { action, makeAutoObservable } from 'mobx'
import { Project, Notification, User, Organization, ProjectBase, Permission } from 'types'
import { Project, Notification, Organization, ProjectBase, Permission } from 'types'
import { IRootStore } from './RootStore'
import Telemetry, { GoogleAnalyticsProps } from 'lib/telemetry'
export interface IUiStore {
language: 'en-US'
@@ -12,12 +11,10 @@ export interface IUiStore {
selectedOrganization?: Organization
notification?: Notification
permissions?: Permission[]
googleAnalyticsProps?: GoogleAnalyticsProps
load: () => void
setProjectRef: (ref?: string) => void
setOrganizationSlug: (slug?: string) => void
setNotification: (notification: Notification) => string
setProfile: (value: User) => void
setPermissions: (permissions?: Permission[]) => void
}
export default class UiStore implements IUiStore {
@@ -83,14 +80,6 @@ export default class UiStore implements IUiStore {
return undefined
}
get googleAnalyticsProps() {
return {
screenResolution:
typeof window !== 'undefined' ? `${window.innerWidth}x${window.innerHeight}` : undefined,
language: this.language,
}
}
load() {
if (typeof window === 'undefined') return
}
@@ -109,10 +98,6 @@ export default class UiStore implements IUiStore {
return id
}
setProfile(value: User) {
Telemetry.sendIdentify(value, this.googleAnalyticsProps)
}
setPermissions(permissions?: any) {
this.permissions = permissions
}