Merge remote-tracking branch 'supabase/master'
# Conflicts: # apps/docs/components/Navigation/NavigationMenu/HomeMenu.tsx
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)) {
|
||||
|
||||
@@ -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
|
||||
@@ -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" }
|
||||
]
|
||||
@@ -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>
|
||||
|
||||
@@ -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-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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,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,7 +1,5 @@
|
||||
interface DataProps {
|
||||
referrer?: string
|
||||
title: string
|
||||
route?: string
|
||||
[prop: string]: any
|
||||
}
|
||||
|
||||
export const post = (url: string, data: DataProps, options = {}) => {
|
||||
|
||||
@@ -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
|
||||
|
||||

|
||||
|
||||
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
|
||||
|
||||

|
||||
|
||||
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️
|
||||
|
||||
[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
|
||||
|
||||

|
||||
|
||||
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
|
||||
|
||||

|
||||
|
||||
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
|
||||
|
||||

|
||||
|
||||
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
|
||||
|
||||

|
||||
|
||||
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
|
||||
|
||||

|
||||
|
||||
[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
|
||||
|
||||

|
||||
|
||||
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.
|
||||
|
||||

|
||||
|
||||
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
|
||||
|
||||

|
||||
|
||||
- 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.
|
||||
|
||||

|
||||
@@ -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>
|
||||
@@ -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>
|
||||
)
|
||||
})
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
]
|
||||
@@ -82,7 +82,7 @@
|
||||
},
|
||||
{
|
||||
"text": "Open Source",
|
||||
"url": "/oss"
|
||||
"url": "/open-source"
|
||||
},
|
||||
{
|
||||
"text": "SupaSquad",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,7 +1,5 @@
|
||||
interface DataProps {
|
||||
referrer?: string
|
||||
title: string
|
||||
route?: string
|
||||
[prop: string]: any
|
||||
}
|
||||
|
||||
export const post = (url: string, data: DataProps, options = {}) => {
|
||||
|
||||
@@ -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
|
||||
@@ -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',
|
||||
},
|
||||
]
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
After Width: | Height: | Size: 628 KiB |
|
After Width: | Height: | Size: 311 KiB |
|
After Width: | Height: | Size: 67 KiB |
|
After Width: | Height: | Size: 316 KiB |
|
After Width: | Height: | Size: 281 KiB |
|
After Width: | Height: | Size: 250 KiB |
|
After Width: | Height: | Size: 322 KiB |
|
After Width: | Height: | Size: 524 KiB |
|
After Width: | Height: | Size: 346 KiB |
|
After Width: | Height: | Size: 355 KiB |
|
After Width: | Height: | Size: 240 KiB |
@@ -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,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,
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
@@ -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 (
|
||||
|
||||
@@ -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])
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export const resourceKeys = {
|
||||
resource: (id: string | undefined) => ['api-authorization', id] as const,
|
||||
}
|
||||
@@ -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)),
|
||||
[]
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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`, {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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,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
|
||||
}
|
||||
|
||||