Merge branch 'master' into docs/examples_reference

This commit is contained in:
Isaiah Hamilton authored and GitHub committed 2022-06-07 00:25:24 -04:00
commit 244486dc24
86 files changed
+19665 -17750

No files matched your search

+1 -1
View File
@@ -1,6 +1,6 @@
import React from 'react'
import Main from '.'
import '../styles/main.css'
import '../styles/tailwind.css'
const Index = () => {
return <Main />
@@ -1,6 +1,6 @@
import React from 'react'
import Main from '.'
import '../styles/main.css'
import '../styles/tailwind.css'
const Index = () => {
return <Main />
+1 -1
View File
@@ -54,7 +54,7 @@ or you can just copy/paste the SQL from below and run it yourself.
<TabsPanel id="UI" label="UI">
```sh
1. Go to the "SQL" section.
1. Go to the "SQL Editor" section.
2. Click "User Management Starter".
3. Click "Run".
```
+1 -1
View File
@@ -47,7 +47,7 @@ defaultActiveId="UI"
<TabsPanel id="UI" label="UI">
```sh
1. Go to the "SQL" section.
1. Go to the "SQL Editor" section.
2. Click "User Management Starter".
3. Click "Run".
```
+1 -1
View File
@@ -65,7 +65,7 @@ or you can just copy/paste the SQL from below and run it yourself.
<TabsPanel id="UI" label="Dashboard UI">
```sh
1. Go to the "SQL" section.
1. Go to the "SQL Editor" section.
2. Click "User Management Starter".
3. Click "Run".
```
+1 -1
View File
@@ -62,7 +62,7 @@ or you can just copy/paste the SQL from below and run it yourself.
<TabsPanel id="UI" label="Dashboard UI">
```sh
1. Go to the "SQL" section.
1. Go to the "SQL Editor" section.
2. Click "User Management Starter".
3. Click "Run".
```
@@ -114,7 +114,7 @@ defaultActiveId="UI"
<TabsPanel id="UI" label="UI">
```sh
1. Go to the "SQL" section.
1. Go to the "SQL Editor" section.
2. Click "User Management Starter".
4. Click "Run".
```
+1 -1
View File
@@ -50,7 +50,7 @@ defaultActiveId="UI"
<TabsPanel id="UI" label="UI">
```sh
1. Go to the "SQL" section.
1. Go to the "SQL Editor" section.
2. Click "User Management Starter".
3. Click "Run".
```
+1 -1
View File
@@ -47,7 +47,7 @@ defaultActiveId="UI"
<TabsPanel id="UI" label="UI">
```sh
1. Go to the "SQL" section.
1. Go to the "SQL Editor" section.
2. Click "User Management Starter".
3. Click "Run".
```
@@ -194,7 +194,7 @@ const { user, session, error } = await supabase.auth.signIn({
### Sign in with redirect.
Sometimes you want to control where the user is redirected to after they are logged in. Supabase supports this for
any URL path on your website (the base domain must be the same as the domain in your Auth settings).
any URL path on your website (the URL must either be on the same domain as your Site URL [see Auth>Settings in dashboard], or must match one of the Additional Redirect URLs [also in Auth>Settings]).
<Tabs
defaultActiveId="js"
@@ -194,7 +194,7 @@ const { user, session, error } = await supabase.auth.signIn({
### Sign in with redirect.
Sometimes you want to control where the user is redirected to after they are logged in. Supabase supports this for
any URL path on your website (the base domain must be the same as the domain in your Auth settings).
any URL path on your website (the URL must either be on the same domain as your Site URL [see Auth>Settings in dashboard], or must match one of the Additional Redirect URLs [also in Auth>Settings]).
<Tabs
defaultActiveId="js"
+10 -1
View File
@@ -1,6 +1,6 @@
type Investor = {
name: string
round: 'SEED' | 'A'
round: 'SEED' | 'A' | 'B'
title: string
img: string
lead: boolean
@@ -36,6 +36,15 @@ const data: Investor[] = [
twitter: null,
linkedIn: null,
},
{
name: 'Felicis',
round: 'B',
title: 'Felicis',
img: '/images/logos/felicis.png',
lead: true,
twitter: null,
linkedIn: null,
},
{
name: 'Tom Preston-Werner',
round: 'A',
+6
View File
@@ -6,6 +6,12 @@ type PressItem = {
}
const data: PressItem[] = [
{
type: 'article',
href: 'https://techcrunch.com/2022/05/10/supabase-raises-80m-series-b-for-its-open-source-firebase-alternative/',
title: 'Supabase raises $80M Series B for its open source Firebase alternative',
publisher: 'TechCrunch',
},
{
type: 'article',
href: 'https://techcrunch.com/2021/09/09/supabase-raises-30m-for-its-open-source-insta-backend',
+3 -3
View File
@@ -247,7 +247,7 @@ const Investors = () => {
<>
<p>
<p className="text-lg">
We've raised over $36 million in funding, backed by some of the world's leading
We've raised over $116 million in funding, backed by some of the world's leading
investors.
</p>
</p>
@@ -256,7 +256,7 @@ const Investors = () => {
/>
</div>
<div className="mx-auto mt-5 mb-16 grid max-w-lg gap-0.5 lg:max-w-none lg:grid-cols-3">
<div className="mx-auto mt-5 mb-16 grid max-w-lg gap-0.5 lg:max-w-none lg:grid-cols-4">
{InvestorData.filter((x) => x.lead === true).map((x) => (
<div key={x.name}>
<div
@@ -311,7 +311,7 @@ const Press = () => {
<div>
<SectionHeader title={'Press'} />
</div>
<div className="mx-auto mt-5 grid gap-5 lg:max-w-none lg:grid-cols-2">
<div className="mx-auto mt-5 grid gap-5 lg:max-w-none lg:grid-cols-3">
{PressData.filter((x) => x.type == 'article').map((x) => (
<a href={x.href} key={x.href} target="_blank">
<Card key={`press_${x.href}`} hoverable className="dark:bg-scale-400 bg-white">
@@ -1,12 +1,9 @@
import { serve } from 'https://deno.land/std@0.131.0/http/server.ts'
const FUNCTION_SECRET = Deno.env.get('FUNCTION_SECRET')
const HUBSPOT_API_KEY = Deno.env.get('HUBSPOT_API_KEY')
const OWNER_ID = Deno.env.get('OWNER_ID')
const ENTERPRISE_PIPELINE = Deno.env.get('ENTERPRISE_PIPELINE')
const PARTNERS_PIPELINE = Deno.env.get('PARTNERS_PIPELINE')
const ENTERPRISE_DEAL_STAGE = Deno.env.get('ENTERPRISE_DEAL_STAGE')
const PARTNERS_DEAL_STAGE = Deno.env.get('PARTNERS_DEAL_STAGE')
const HUBSPOT_PORTAL = Deno.env.get('HUBSPOT_PORTAL')
const PARTNER_FORM_ID = Deno.env.get('PARTNER_FORM_ID')
const ENTERPRISE_FORM_ID = Deno.env.get('ENTERPRISE_FORM_ID')
serve(async (req) => {
const requestSecret = req.headers.get('x-function-secret')
@@ -17,14 +14,51 @@ serve(async (req) => {
const input = await req.json()
const firstName = input.record.contact_first_name || input.record.first
const lastName = input.record.contact_last_name || input.record.last
const supabase_table_id = input.record.id
const country = input.record.country
const form_note = input.record.details
const company = input.record.company_name || input.record.company
const company_size = input.record.company_size || input.record.size
const email = input.record.contact_email || input.record.email
const phone = input.record.contact_phone || input.record.phone
const lastName = input.record.contact_last_name || input.record.last
const firstName = input.record.contact_first_name || input.record.first
const jobtitle = input.record.title
const website = input.record.website
const partner_gallery_type = input.record.type
const contactResponse = await fetch(
`https://api.hubapi.com/crm/v3/objects/contacts?hapikey=${HUBSPOT_API_KEY}`,
let formData: object = {
supabase_table_id,
country,
form_note,
company,
company_size,
email,
phone,
lastName,
firstName,
}
if (input.table === 'partner_contacts') {
formData = {
...formData,
jobtitle,
website,
partner_gallery_type,
}
}
const fields: { objectTypeId: '0-1'; name: string; value: string }[] = []
for (const [key, value] of Object.entries(formData)) {
fields.push({ objectTypeId: '0-1', name: key, value: value })
}
const formId = input.table === 'partner_contacts' ? PARTNER_FORM_ID : ENTERPRISE_FORM_ID
// Using HubSpot's submit data to a form API
// https://legacydocs.hubspot.com/docs/methods/forms/submit_form
const formResponse = await fetch(
`https://api.hsforms.com/submissions/v3/integration/submit/${HUBSPOT_PORTAL}/${formId}`,
{
method: 'POST',
headers: {
@@ -32,143 +66,18 @@ serve(async (req) => {
'Content-Type': 'application/json',
},
body: JSON.stringify({
properties: {
company,
email,
firstname: firstName,
lastname: lastName,
phone,
},
fields,
}),
}
)
const body = await contactResponse.json()
// Will create a deal using HubSpot's workflow tool instead of via API
let contactId: string | null = null
if (contactResponse.ok) {
contactId = body.id
} else {
if (body.status === 'error' && body.category === 'CONFLICT') {
// fetch existing contact
const contactAssociateResponse = await fetch(
`https://api.hubapi.com/crm/v3/objects/contacts/${email}?idProperty=email&hapikey=${HUBSPOT_API_KEY}`
)
const body = await contactAssociateResponse.json()
if (contactAssociateResponse.ok) {
contactId = body.id
} else {
console.log(`Failed to fetch contact`, body)
return new Response(`Failed to fetch contact`, { status: 500 })
}
} else {
console.log(`Failed to create contact`, body)
return new Response(`Failed to create contact`, { status: 500 })
}
}
const noteContent = Object.entries(input.record).reduce(
(acc, [key, value]) => acc + `${key}: ${value || 'NOT_PROVIDED'}<br />`,
''
)
// Add note
const noteResponse = await fetch(
`https://api.hubapi.com/crm/v3/objects/notes?hapikey=${HUBSPOT_API_KEY}`,
{
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
properties: {
hs_timestamp: Date.now(),
hs_note_body: noteContent,
hubspot_owner_id: OWNER_ID,
},
}),
}
)
const noteBody = await noteResponse.json()
if (!noteResponse.ok) {
console.log(`Failed to add note`, noteBody)
return new Response(`Failed to add note`, { status: 500 })
}
const noteId = noteBody.id
// Associate note with contact
const noteAssociateResponse = await fetch(
`https://api.hubapi.com/crm/v3/objects/notes/${noteId}/associations/contacts/${contactId}/note_to_contact?hapikey=${HUBSPOT_API_KEY}`,
{
method: 'PUT',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
}
)
if (!noteAssociateResponse.ok) {
console.log(`Failed to associate note`, await noteAssociateResponse.json())
return new Response(`Failed to associate note`, { status: 500 })
}
// Create deal
const dealCloseDate = new Date()
dealCloseDate.setDate(dealCloseDate.getDate() + 28) // 28 days
const dealResponse = await fetch(
`https://api.hubapi.com/crm/v3/objects/deals?hapikey=${HUBSPOT_API_KEY}`,
{
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
properties: {
closedate: dealCloseDate.toISOString(),
dealname: company || `${firstName} ${lastName}`,
dealstage:
input.table === 'enterprise_contacts' ? ENTERPRISE_DEAL_STAGE : PARTNERS_DEAL_STAGE,
hubspot_owner_id: OWNER_ID,
pipeline: input.table === 'enterprise_contacts' ? ENTERPRISE_PIPELINE : PARTNERS_PIPELINE,
},
}),
}
)
const dealBody = await dealResponse.json()
if (!dealResponse.ok) {
console.log(`Failed to create deal`, dealBody)
return new Response(`Failed to create deal: ` + dealBody.message, { status: 500 })
}
const dealId = dealBody.id
// Associate contact with a deal
const dealAssociateResponse = await fetch(
`https://api.hubapi.com/crm/v3/objects/contacts/${contactId}/associations/deals/${dealId}/contact_to_deal?hapikey=${HUBSPOT_API_KEY}`,
{
method: 'PUT',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
}
)
if (!dealAssociateResponse.ok) {
console.log(`Failed to associate contact with deal`, await dealAssociateResponse.json())
return new Response(`Failed to associate contact with deal`, { status: 500 })
if (!formResponse.ok) {
console.log(`Failed to submit form data to HubSpot`, await formResponse.json())
return new Response(`Failed to submit form data to HubSpot`, {
status: 500,
})
}
return new Response(JSON.stringify({ done: true }), {
+6 -6
View File
@@ -4214,9 +4214,9 @@
}
},
"node_modules/async": {
"version": "2.6.3",
"resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz",
"integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==",
"version": "2.6.4",
"resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz",
"integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==",
"dependencies": {
"lodash": "^4.17.14"
}
@@ -17284,9 +17284,9 @@
"integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg=="
},
"async": {
"version": "2.6.3",
"resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz",
"integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==",
"version": "2.6.4",
"resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz",
"integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==",
"requires": {
"lodash": "^4.17.14"
}
@@ -4495,9 +4495,9 @@
}
},
"node_modules/async": {
"version": "2.6.3",
"resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz",
"integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==",
"version": "2.6.4",
"resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz",
"integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==",
"dependencies": {
"lodash": "^4.17.14"
}
@@ -17949,9 +17949,9 @@
"integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg=="
},
"async": {
"version": "2.6.3",
"resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz",
"integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==",
"version": "2.6.4",
"resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz",
"integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==",
"requires": {
"lodash": "^4.17.14"
}
@@ -0,0 +1,29 @@
import { FC } from 'react'
import Link from 'next/link'
import { useFlag } from 'hooks'
import { IconExternalLink } from '@supabase/ui'
const AppBannerWrapper: FC = ({ children }) => {
const ongoingIncident = useFlag('ongoingIncident')
return (
<div className="flex h-full flex-col">
{ongoingIncident && (
<Link href="https://status.supabase.com">
<a target="_blank">
<div className="hover:bg-green-1000 flex cursor-pointer items-center justify-center space-x-2 bg-green-900 py-3 transition text-scale-400 dark:text-scale-1200">
<p className="text-sm font-medium">
We are currently investigating a technical issue, follow status.supabase.com for
updates
</p>
<IconExternalLink size={16} strokeWidth={2} />
</div>
</a>
</Link>
)}
{children}
</div>
)
}
export default AppBannerWrapper
+2 -1
View File
@@ -1,5 +1,6 @@
import PortalToast from './PortalToast'
import GoTrueWrapper from './GoTrueWrapper/GoTrueWrapper'
import RouteValidationWrapper from './RouteValidationWrapper'
import AppBannerWrapper from './AppBannerWrapper'
export { PortalToast, GoTrueWrapper, RouteValidationWrapper }
export { PortalToast, GoTrueWrapper, RouteValidationWrapper, AppBannerWrapper }
@@ -7,7 +7,6 @@ export const formatComputeSizes = (computeSizes: any[]) => {
const microOption = {
id: '',
name: 'Micro Add-on',
description: 'Basic set up to get started',
metadata: {
default_price_id: undefined,
supabase_prod_id: 'addon_instance_micro',
@@ -29,30 +28,23 @@ export const formatComputeSizes = (computeSizes: any[]) => {
],
}
const addonsOrder = [
'addon_instance_small',
'addon_instance_medium',
'addon_instance_large',
'addon_instance_xlarge',
'addon_instance_xxlarge',
'addon_instance_4xlarge',
'addon_instance_8xlarge',
'addon_instance_12xlarge',
'addon_instance_16xlarge',
]
return [microOption]
.concat(
computeSizes.filter(
(option: any) => option.metadata.supabase_prod_id === 'addon_instance_small'
)
)
.concat(
computeSizes.filter(
(option: any) => option.metadata.supabase_prod_id === 'addon_instance_medium'
)
)
.concat(
computeSizes.filter(
(option: any) => option.metadata.supabase_prod_id === 'addon_instance_large'
)
)
.concat(
computeSizes.filter(
(option: any) => option.metadata.supabase_prod_id === 'addon_instance_xlarge'
)
)
.concat(
computeSizes.filter(
(option: any) => option.metadata.supabase_prod_id === 'addon_instance_xxlarge'
)
addonsOrder.map((id: string) => {
return computeSizes.find((option: any) => option.metadata.supabase_prod_id === id)
})
)
.filter((option) => option !== undefined)
}
@@ -1,26 +1,21 @@
import { FC } from 'react'
import Link from 'next/link'
import { Badge, Button, Radio } from '@supabase/ui'
import { useStore } from 'hooks'
import { useStore, useFlag } from 'hooks'
import { getProductPrice } from '../Billing.utils'
import DisabledWarningDueToIncident from 'components/ui/DisabledWarningDueToIncident'
interface Props {
computeSizes: any[]
currentComputeSize: any
selectedComputeSize: any
onSelectOption: (option: any) => void
}
const ComputeSizeSelection: FC<Props> = ({
computeSizes,
currentComputeSize,
selectedComputeSize,
onSelectOption,
}) => {
// [Joshen] We should have some indication on what's the current compute
// size of the project based on currentComputeSize
const ComputeSizeSelection: FC<Props> = ({ computeSizes, selectedComputeSize, onSelectOption }) => {
const { ui } = useStore()
const projectRef = ui.selectedProjectRef
const addonUpdateDisabled = useFlag('disableProjectCreationAndUpdate')
return (
<div className="space-y-4">
@@ -29,49 +24,52 @@ const ComputeSizeSelection: FC<Props> = ({
<h4 className="text-lg">Database add-ons</h4>
<Badge color="green">Optional</Badge>
</div>
<p className="text-sm text-scale-1100">
<p className="text-scale-1100 text-sm">
Choose the database instance size that best fits your needs
</p>
</div>
<Radio.Group type="cards" className="billing-compute-radio">
{computeSizes.map((option: any) => {
const defaultPrice = getProductPrice(option)
return (
<Radio
hidden
key={option.id}
align="vertical"
label={option.name}
// @ts-ignore
description={
<div>
<p>{option.description}</p>
<p>{option.metadata.features}</p>
</div>
}
value={option.id}
optionalLabel={<div>${defaultPrice.unit_amount / 100} / month</div>}
checked={selectedComputeSize?.id === option.id}
onChange={() => onSelectOption(option)}
/>
)
})}
<Radio
hidden
disabled
key="compute-size-cta"
align="vertical"
label="Need a larger add on?"
description="Reach out to us - we've got you covered!"
optionalLabel={
<Link href={`/support/new?ref=${projectRef}&category=sales`}>
<a>
<Button>Contact us</Button>
</a>
</Link>
}
/>
</Radio.Group>
{addonUpdateDisabled ? (
<DisabledWarningDueToIncident title="Updating database add-ons is currently disabled" />
) : (
<Radio.Group type="cards" className="billing-compute-radio">
{computeSizes.map((option: any) => {
const defaultPrice = getProductPrice(option)
return (
<Radio
hidden
key={option.id}
align="vertical"
label={option.name}
// @ts-ignore
description={
<div>
<p>{option.metadata.features}</p>
</div>
}
value={option.id}
optionalLabel={<div>${defaultPrice.unit_amount / 100} / month</div>}
checked={selectedComputeSize?.id === option.id}
onChange={() => onSelectOption(option)}
/>
)
})}
<Radio
hidden
disabled
key="compute-size-cta"
align="vertical"
label="Need a larger add on?"
description="Reach out to us - we've got you covered!"
optionalLabel={
<Link href={`/support/new?ref=${projectRef}&category=sales`}>
<a>
<Button>Contact us</Button>
</a>
</Link>
}
/>
</Radio.Group>
)}
</div>
)
}
@@ -10,7 +10,7 @@ import {
Alert,
} from '@supabase/ui'
import { useFlag, useStore } from 'hooks'
import { useStore } from 'hooks'
import { STRIPE_PRODUCT_IDS } from 'lib/constants'
import { SubscriptionPreview } from '../Billing.types'
import { getProductPrice } from '../Billing.utils'
@@ -57,14 +57,9 @@ const PaymentSummaryPanel: FC<Props> = ({
onConfirmPayment,
isSubmitting,
}) => {
/**
* Feature flags
*/
const nativeBilling = useFlag('nativeBilling')
const [showConfirmModal, setShowConfirmModal] = useState(false)
const { ui } = useStore()
const projectRegion = ui.selectedProject?.region
const [showConfirmModal, setShowConfirmModal] = useState(false)
const isChangingPlan =
(currentPlan.prod_id !== STRIPE_PRODUCT_IDS.PAYG && currentPlan.prod_id !== selectedPlan?.id) ||
@@ -82,14 +77,14 @@ const PaymentSummaryPanel: FC<Props> = ({
return (
<>
<div
className="bg-panel-body-light dark:bg-panel-body-dark w-full px-6 lg:px-12 py-10 space-y-8 border-l"
className="bg-panel-body-light dark:bg-panel-body-dark w-full space-y-8 border-l px-6 py-10 lg:px-12"
style={{ height: 'calc(100vh - 57px)' }}
>
<p>Payment Summary</p>
{/* Subscription details */}
<div className="space-y-1">
<p className="text-sm text-scale-1100">Selected subscription</p>
<p className="text-scale-1100 text-sm">Selected subscription</p>
<div className="flex items-center justify-between">
<p className={`${isChangingPlan ? 'text-scale-1100 line-through' : ''} text-sm`}>
{getPlanName(currentPlan)}
@@ -109,57 +104,53 @@ const PaymentSummaryPanel: FC<Props> = ({
</div>
{/* Add on details */}
{nativeBilling && (
<>
{projectRegion !== 'af-south-1' && (
<div className="space-y-1">
<p className="text-sm">Selected add-ons</p>
{currentComputeSize === undefined && selectedComputeSize === undefined && (
<p className="text-scale-1100 text-sm">No add-ons selected</p>
)}
{currentComputeSize !== undefined && (
<div className="flex items-center justify-between">
<p
className={`${
isChangingComputeSize ? 'text-scale-1100 line-through' : ''
} text-sm`}
>
{currentComputeSize.name}
</p>
<p
className={`${
isChangingComputeSize ? 'text-scale-1100 line-through' : ''
} text-sm`}
>
${(getProductPrice(currentComputeSize).unit_amount / 100).toFixed(2)}
</p>
</div>
)}
{isChangingComputeSize && (
<div className="flex items-center justify-between">
<p className="text-sm">{selectedComputeSize.name}</p>
<p className="text-sm">
${(getProductPrice(selectedComputeSize).unit_amount / 100).toFixed(2)}
</p>
</div>
)}
{isChangingComputeSize && (
<div className="!mt-4">
<InformationBox
hideCollapse
defaultVisibility
icon={<IconAlertCircle strokeWidth={2} />}
title="Changing your compute size"
description="It will take up to 2 minutes for changes to take place, and your project will be unavailable during that time"
/>
</div>
)}
{projectRegion !== 'af-south-1' && (
<div className="space-y-1">
<p className="text-sm">Selected add-ons</p>
{currentComputeSize === undefined && selectedComputeSize === undefined && (
<p className="text-scale-1100 text-sm">No add-ons selected</p>
)}
{currentComputeSize !== undefined && (
<div className="flex items-center justify-between">
<p
className={`${
isChangingComputeSize ? 'text-scale-1100 line-through' : ''
} text-sm`}
>
{currentComputeSize.name}
</p>
<p
className={`${
isChangingComputeSize ? 'text-scale-1100 line-through' : ''
} text-sm`}
>
${(getProductPrice(currentComputeSize).unit_amount / 100).toFixed(2)}
</p>
</div>
)}
</>
{isChangingComputeSize && (
<div className="flex items-center justify-between">
<p className="text-sm">{selectedComputeSize.name}</p>
<p className="text-sm">
${(getProductPrice(selectedComputeSize).unit_amount / 100).toFixed(2)}
</p>
</div>
)}
{isChangingComputeSize && (
<div className="!mt-4">
<InformationBox
hideCollapse
defaultVisibility
icon={<IconAlertCircle strokeWidth={2} />}
title="Changing your compute size"
description="It will take up to 2 minutes for changes to take place, and your project will be unavailable during that time"
/>
</div>
)}
</div>
)}
<div className="h-px w-full bg-scale-600" />
<div className="bg-scale-600 h-px w-full" />
<PaymentTotal
subscriptionPreview={subscriptionPreview}
@@ -171,13 +162,13 @@ const PaymentSummaryPanel: FC<Props> = ({
<div className="space-y-2">
<p className="text-sm">Select payment method</p>
{isLoadingPaymentMethods ? (
<div className="flex items-center rounded-md space-x-4 px-4 py-2 bg-scale-400 border border-scale-700">
<div className="bg-scale-400 border-scale-700 flex items-center space-x-4 rounded-md border px-4 py-2">
<IconLoader className="animate-spin" size={14} />
<p className="text-sm text-scale-1100">Retrieving payment methods</p>
<p className="text-scale-1100 text-sm">Retrieving payment methods</p>
</div>
) : paymentMethods.length === 0 ? (
<div className="flex items-center justify-between rounded-md px-4 py-2 bg-scale-100 border border-dashed">
<div className="flex items-center space-x-4 text-scale-1100">
<div className="bg-scale-100 flex items-center justify-between rounded-md border border-dashed px-4 py-2">
<div className="text-scale-1100 flex items-center space-x-4">
<IconAlertCircle size={16} strokeWidth={1.5} />
<p className="text-sm">No saved payment methods</p>
</div>
@@ -214,11 +205,11 @@ const PaymentSummaryPanel: FC<Props> = ({
)
})}
<div
className="py-2 px-3 flex items-center space-x-2 cursor-pointer transition group hover:bg-scale-500"
className="hover:bg-scale-500 group flex cursor-pointer items-center space-x-2 py-2 px-3 transition"
onClick={onSelectAddNewPaymentMethod}
>
<IconPlus size={16} />
<p className="text-scale-1000 transition group-hover:text-scale-1200">
<p className="text-scale-1000 group-hover:text-scale-1200 transition">
Add new payment method
</p>
</div>
@@ -247,7 +238,7 @@ const PaymentSummaryPanel: FC<Props> = ({
header="Updating project database instance size"
onCancel={() => setShowConfirmModal(false)}
>
<div className="py-4 space-y-4">
<div className="space-y-4 py-4">
<Modal.Content>
<Alert
withIcon
@@ -259,7 +250,7 @@ const PaymentSummaryPanel: FC<Props> = ({
</Alert>
</Modal.Content>
<Modal.Content>
<p className="text-sm text-scale-1200">Would you like to update your project now?</p>
<p className="text-scale-1200 text-sm">Would you like to update your project now?</p>
</Modal.Content>
<Modal.Seperator />
<Modal.Content>
@@ -1,8 +1,10 @@
import { FC } from 'react'
import { Button, IconLoader } from '@supabase/ui'
import * as Tooltip from '@radix-ui/react-tooltip'
import { useFlag } from 'hooks'
import { STRIPE_PRODUCT_IDS } from 'lib/constants'
import { StripeProduct } from '../..'
import { StripeProduct } from 'components/interfaces/Billing'
import Link from 'next/link'
interface Props {
@@ -12,6 +14,8 @@ interface Props {
}
const PlanCTAButton: FC<Props> = ({ plan, currentPlan, onSelectPlan }) => {
const projectUpdateDisabled = useFlag('disableProjectCreationAndUpdate')
const getButtonType = (plan: any) => {
if (plan.name === 'Enterprise' || plan.id === STRIPE_PRODUCT_IDS.FREE) {
return 'default'
@@ -71,9 +75,9 @@ const PlanCTAButton: FC<Props> = ({ plan, currentPlan, onSelectPlan }) => {
}
return (
<div className="flex items-center justify-center">
<div className="flex flex-col items-center justify-center space-y-2">
<Button
disabled={disabled}
disabled={disabled || projectUpdateDisabled}
type={type}
onClick={() => onSelectPlan(plan)}
block
@@ -81,6 +85,11 @@ const PlanCTAButton: FC<Props> = ({ plan, currentPlan, onSelectPlan }) => {
>
{ctaText}
</Button>
{!disabled && projectUpdateDisabled && (
<p className="text-scale-1100 text-sm">
Subscription changes are currently disabled, our engineers are working on a fix
</p>
)}
</div>
)
}
@@ -1,7 +1,8 @@
import { IconCheck } from '@supabase/ui'
import { STRIPE_PRODUCT_IDS } from 'lib/constants'
import { FC } from 'react'
import { StripeProduct } from '../..'
import { IconCheck } from '@supabase/ui'
import { STRIPE_PRODUCT_IDS } from 'lib/constants'
import { StripeProduct } from 'components/interfaces/Billing'
import PlanCTAButton from './PlanCTAButton'
import { PRICING_META } from './Plans.Constants'
@@ -14,8 +15,8 @@ interface Props {
const AnimatedGradientBackground = () => (
<div
className={[
'absolute top-0 left-0 w-full h-full animate-sway',
'bg-gradient-to-b from-transparent to-green-800 via-transparent',
'animate-sway absolute top-0 left-0 h-full w-full',
'bg-gradient-to-b from-transparent via-transparent to-green-800',
].join(' ')}
/>
)
@@ -23,10 +24,10 @@ const AnimatedGradientBackground = () => (
const CurrentSubscriptionBanner = () => (
<div
className={[
'absolute h-8 -top-7 left-0 w-full',
'absolute -top-7 left-0 h-8 w-full',
'flex items-center justify-center',
'bg-brand-400 text-brand-900',
'rounded-t-md border-t border-l border-r border border-b-0',
'rounded-t-md border border-t border-l border-r border-b-0',
].join(' ')}
>
<p className="text-xs">Current plan</p>
@@ -52,30 +53,30 @@ const Plans: FC<Props> = ({ plans, currentPlan, onSelectPlan }) => {
return (
<div key={PRICING_META[plan.id].name} className="relative h-full">
{plan.id === STRIPE_PRODUCT_IDS.PRO && (
<div className="absolute overflow-hidden w-full h-full left-0 bottom-0 opacity-50">
<div className="absolute left-0 bottom-0 h-full w-full overflow-hidden opacity-50">
<AnimatedGradientBackground />
</div>
)}
<div className="flex flex-col rounded border overflow-hidden h-full">
<div className="flex h-full flex-col overflow-hidden rounded border">
{isCurrentPlan && <CurrentSubscriptionBanner />}
<div className="px-8 py-6 bg-white dark:bg-scale-300">
<div className="dark:bg-scale-300 bg-white px-8 py-6">
<span
className="inline-flex text-cd font-normal tracking-wide rounded-full text-base text-scale-1200"
className="text-cd text-scale-1200 inline-flex rounded-full text-base font-normal tracking-wide"
id="tier-standard"
>
{PRICING_META[plan.id].name}
</span>
<div className="flex items-baseline mt-2">
<div className="mt-2 flex items-baseline">
<div className="flex space-x-2">
{PRICING_META[plan.id].priceMonthly !== undefined ? (
<>
<div className="flex items-end gap-1">
{PRICING_META[plan.id].from && (
<span className="text-base font-medium text-scale-1200">From</span>
<span className="text-scale-1200 text-base font-medium">From</span>
)}
<div>
<span className="text-2xl">${PRICING_META[plan.id].priceMonthly}</span>
<span className="ml-1 text-xl font-medium text-scale-900">/mo</span>
<span className="text-scale-900 ml-1 text-xl font-medium">/mo</span>
</div>
</div>
</>
@@ -84,7 +85,7 @@ const Plans: FC<Props> = ({ plans, currentPlan, onSelectPlan }) => {
)}
<div className="flex h-8">
{PRICING_META[plan.id].warning && (
<div className="px-2 py-1 mt-2 text-xs rounded-md bg-brand-300 bg-opacity-30 text-brand-1000">
<div className="bg-brand-300 text-brand-1000 mt-2 rounded-md bg-opacity-30 px-2 py-1 text-xs">
{PRICING_META[plan.id].warning}
</div>
)}
@@ -98,28 +99,28 @@ const Plans: FC<Props> = ({ plans, currentPlan, onSelectPlan }) => {
onSelectPlan={onSelectPlan}
/>
</div>
<p className="text-sm text-scale-1100">{PRICING_META[plan.id].description}</p>
<p className="text-scale-1100 text-sm">{PRICING_META[plan.id].description}</p>
</div>
<div
className={[
'flex-col justify-between flex-1',
'space-y-6 border-t dark:border-scale-400',
'flex-1 flex-col justify-between',
'dark:border-scale-400 space-y-6 border-t',
'bg-scale-100 dark:bg-scale-300',
'px-8 py-6 h-full hidden lg:flex',
'hidden h-full px-8 py-6 lg:flex',
].join(' ')}
>
{PRICING_META[plan.id].preface && (
<p className="text-sm text-scale-1200">{PRICING_META[plan.id].preface}</p>
<p className="text-scale-1200 text-sm">{PRICING_META[plan.id].preface}</p>
)}
<ul role="list" className="divide-y dark:divide-scale-400">
<ul role="list" className="dark:divide-scale-400 divide-y">
{PRICING_META[plan.id].features.map((feature) => (
<li key={feature} className="flex items-center py-2">
<IconCheck
className="w-3 h-3 text-brand-900 "
className="text-brand-900 h-3 w-3 "
aria-hidden="true"
strokeWidth={3}
/>
<p className="mb-0 ml-3 text-xs text-scale-1100 dark:text-scale-1200">
<p className="text-scale-1100 dark:text-scale-1200 mb-0 ml-3 text-xs">
{feature}
</p>
</li>
@@ -128,13 +129,13 @@ const Plans: FC<Props> = ({ plans, currentPlan, onSelectPlan }) => {
<div className="flex flex-col gap-6">
<div className="space-y-2">
{PRICING_META[plan.id].additional && (
<p className="text-sm text-scale-1200">{PRICING_META[plan.id].additional}</p>
<p className="text-scale-1200 text-sm">{PRICING_META[plan.id].additional}</p>
)}
{PRICING_META[plan.id].scale && (
<p className="text-xs text-scale-900">{PRICING_META[plan.id].scale}</p>
<p className="text-scale-900 text-xs">{PRICING_META[plan.id].scale}</p>
)}
{PRICING_META[plan.id].shutdown && (
<p className="text-xs text-scale-900">{PRICING_META[plan.id].shutdown}</p>
<p className="text-scale-900 text-xs">{PRICING_META[plan.id].shutdown}</p>
)}
</div>
</div>
@@ -3,7 +3,7 @@ import { Transition } from '@headlessui/react'
import { useRouter } from 'next/router'
import { Button, IconHelpCircle, Toggle, Modal } from '@supabase/ui'
import { useFlag, useStore } from 'hooks'
import { useStore } from 'hooks'
import { post, patch } from 'lib/common/fetch'
import { API_URL, PROJECT_STATUS } from 'lib/constants'
import { getURL } from 'lib/helpers'
@@ -38,11 +38,6 @@ const ProUpgrade: FC<Props> = ({
isLoadingPaymentMethods,
onSelectBack,
}) => {
/**
* Feature flags
*/
const nativeBilling = useFlag('nativeBilling')
const { app, ui } = useStore()
const router = useRouter()
@@ -169,14 +164,14 @@ const ProUpgrade: FC<Props> = ({
enter="transition ease-out duration-300"
enterFrom="transform opacity-0 translate-x-10"
enterTo="transform opacity-100 translate-x-0"
className="w-full flex items-start justify-between"
className="flex w-full items-start justify-between"
>
<>
<div className="2xl:min-w-5xl mx-auto mt-10">
<div className="relative px-5 space-y-4">
<div className="relative space-y-4 px-5">
<BackButton onClick={() => onSelectBack()} />
<div className="space-y-8">
<h4 className="text-lg text-scale-900">Change your project's subscription</h4>
<h4 className="text-scale-900 text-lg">Change your project's subscription</h4>
<div
className="space-y-8 overflow-scroll pb-8"
style={{ height: 'calc(100vh - 9rem - 57px)' }}
@@ -187,7 +182,7 @@ const ProUpgrade: FC<Props> = ({
<h3 className="text-xl">
Welcome to <span className="text-brand-900">Pro</span>
</h3>
<p className="text-base text-scale-1100">
<p className="text-scale-1100 text-base">
Your new subscription will begin immediately after payment
</p>
</>
@@ -196,24 +191,24 @@ const ProUpgrade: FC<Props> = ({
<h3 className="text-3xl">
Managing your <span className="text-brand-900">Pro</span> plan
</h3>
<p className="text-base text-scale-1100">
<p className="text-scale-1100 text-base">
Your billing cycle will reset after payment
</p>
</>
)}
</div>
<div className="flex items-center justify-between gap-16 border rounded px-6 py-4 bg-panel-body-light dark:bg-panel-body-dark drop-shadow-sm border-panel-border-light border-panel-border-dark">
<div className="bg-panel-body-light dark:bg-panel-body-dark border-panel-border-light border-panel-border-dark flex items-center justify-between gap-16 rounded border px-6 py-4 drop-shadow-sm">
<div>
<div className="flex items-center space-x-2">
<p>Enable spend cap</p>
<IconHelpCircle
size={16}
strokeWidth={1.5}
className="cursor-pointer opacity-50 hover:opacity-100 transition"
className="cursor-pointer opacity-50 transition hover:opacity-100"
onClick={() => setShowSpendCapHelperModal(true)}
/>
</div>
<p className="text-sm text-scale-1100">
<p className="text-scale-1100 text-sm">
If enabled, additional resources will not be charged on a per-usage basis
</p>
</div>
@@ -222,12 +217,11 @@ const ProUpgrade: FC<Props> = ({
onChange={() => setIsSpendCapEnabled(!isSpendCapEnabled)}
/>
</div>
{nativeBilling && projectRegion !== 'af-south-1' && (
{projectRegion !== 'af-south-1' && (
<>
<Divider light />
<ComputeSizeSelection
computeSizes={computeSizes || []}
currentComputeSize={currentComputeSize}
selectedComputeSize={selectedComputeSize}
onSelectOption={onSelectComputeSizeOption}
/>
@@ -274,7 +268,7 @@ const ProUpgrade: FC<Props> = ({
header="Enabling spend cap"
onCancel={() => setShowSpendCapHelperModal(false)}
>
<div className="py-4 space-y-4">
<div className="space-y-4 py-4">
<Modal.Content>
<div className="space-y-4">
<p className="text-sm">
@@ -289,11 +283,11 @@ const ProUpgrade: FC<Props> = ({
</p>
{/* Maybe instead of a table, show something more interactive like a spend cap playground */}
{/* Maybe ideate this in Figma first but this is good enough for now */}
<div className="border border-scale-600 bg-scale-500 rounded">
<div className="border-scale-600 bg-scale-500 rounded border">
<div className="flex items-center px-4 pt-2 pb-1">
<p className="w-[50%] text-sm text-scale-1100">Item</p>
<p className="w-[25%] text-sm text-scale-1100">Limit</p>
<p className="w-[25%] text-sm text-scale-1100">Rate</p>
<p className="text-scale-1100 w-[50%] text-sm">Item</p>
<p className="text-scale-1100 w-[25%] text-sm">Limit</p>
<p className="text-scale-1100 w-[25%] text-sm">Rate</p>
</div>
<div className="py-1">
<div className="flex items-center px-4 py-1">
@@ -3,9 +3,10 @@ import dayjs from 'dayjs'
import { sum } from 'lodash'
import { useRouter } from 'next/router'
import { Loading, Button } from '@supabase/ui'
import { formatBytes } from 'lib/helpers'
import { STRIPE_PRODUCT_IDS } from 'lib/constants'
import { useStore } from 'hooks'
import { useStore, useFlag } from 'hooks'
import CostBreakdownRow from './CostBreakdownRow'
import { StripeSubscription } from './Subscription.types'
import { deriveFeatureCost, deriveProductCost } from '../PAYGUsage/PAYGUsage.utils'
@@ -34,6 +35,7 @@ const Subscription: FC<Props> = ({
const router = useRouter()
const { ui } = useStore()
const isOrgOwner = ui.selectedOrganization?.is_owner
const projectUpdateDisabled = useFlag('disableProjectCreationAndUpdate')
const isPayg = subscription?.tier.prod_id === STRIPE_PRODUCT_IDS.PAYG
const addOns = subscription?.addons ?? []
@@ -62,16 +64,24 @@ const Subscription: FC<Props> = ({
</div>
<div className="flex flex-col items-end space-y-2">
<Button
disabled={!isOrgOwner}
disabled={!isOrgOwner || projectUpdateDisabled}
onClick={() => router.push(`/project/${project.ref}/settings/billing/update`)}
type="primary"
>
Change subscription
</Button>
{!isOrgOwner && (
<p className="text-scale-1100 text-sm">
{!isOrgOwner ? (
<p className="text-scale-1100 text-xs">
Only the organization owner can amend subscriptions
</p>
) : projectUpdateDisabled ? (
<p className="text-scale-1100 text-right text-xs">
Subscription changes are currently disabled
<br />
Our engineers are working on a fix
</p>
) : (
<div />
)}
</div>
</div>
@@ -0,0 +1,46 @@
import { useState } from 'react'
import { observer } from 'mobx-react-lite'
import { Button, IconAlertCircle } from '@supabase/ui'
import { useStore } from 'hooks'
import { getURL } from 'lib/helpers'
import InformationBox from 'components/ui/InformationBox'
import { AddNewPaymentMethodModal } from 'components/interfaces/Billing'
const EmptyPaymentMethodWarning = observer(
({ stripeCustomerId }: { stripeCustomerId: string | undefined }) => {
const { ui } = useStore()
const slug = ui.selectedOrganization?.slug
const [showAddPaymentMethodModal, setShowAddPaymentMethodModal] = useState<boolean>(false)
return (
<div className="mt-4">
<InformationBox
icon={<IconAlertCircle className="text-white" size="large" strokeWidth={1.5} />}
defaultVisibility={true}
hideCollapse
title="Your organization has no payment methods"
description={
<div className="space-y-3">
<p className="text-sm leading-normal">
You need to add a payment method for your organization before creating a paid
project.
</p>
<Button type="secondary" onClick={() => setShowAddPaymentMethodModal(true)}>
Add a payment method
</Button>
</div>
}
/>
<AddNewPaymentMethodModal
visible={showAddPaymentMethodModal}
returnUrl={`${getURL()}/new/${slug}`}
onCancel={() => setShowAddPaymentMethodModal(false)}
/>
</div>
)
}
)
export default EmptyPaymentMethodWarning
@@ -0,0 +1,29 @@
import { FC } from 'react'
import { IconAlertCircle } from '@supabase/ui'
import InformationBox from 'components/ui/InformationBox'
interface Props {
limit: number
}
const FreeProjectLimitWarning: FC<Props> = ({ limit }) => {
return (
<div className="mt-4">
<InformationBox
icon={<IconAlertCircle className="text-white" size="large" strokeWidth={1.5} />}
defaultVisibility={true}
hideCollapse
title="Your account has reached its free project limit"
description={
<div className="space-y-3">
<p className="text-sm leading-normal">
{`Your account can only have up to ${limit} free projects - to create another free project, you'll need to delete an existing free project first. Otherwise, you may create a project on the Pro tier instead.`}
</p>
</div>
}
/>
</div>
)
}
export default FreeProjectLimitWarning
@@ -0,0 +1,25 @@
import { IconAlertCircle } from '@supabase/ui'
import InformationBox from 'components/ui/InformationBox'
const NotOrganizationOwnerWarning = () => {
return (
<div className="mt-4">
<InformationBox
icon={<IconAlertCircle className="text-white" size="large" strokeWidth={1.5} />}
defaultVisibility={true}
hideCollapse
title="You do not have permission to create a project"
description={
<div className="space-y-3">
<p className="text-sm leading-normal">
Only the organization owner can create new projects. Contact your organization owner
to create a new project for this organization.
</p>
</div>
}
/>
</div>
)
}
export default NotOrganizationOwnerWarning
@@ -0,0 +1,5 @@
import NotOrganizationOwnerWarning from './NotOrganizationOwnerWarning'
import FreeProjectLimitWarning from './FreeProjectLimitWarning'
import EmptyPaymentMethodWarning from './EmptyPaymentMethodWarning'
export { NotOrganizationOwnerWarning, FreeProjectLimitWarning, EmptyPaymentMethodWarning }
@@ -21,6 +21,8 @@ const LogEventChart: React.FC<Props> = ({ data, onBarClick }) => {
attribute="count"
label="Events"
onBarClick={(v: any) => {
if (!v) return
const timestamp = v.activePayload[0].payload.timestamp
// 60s before
onBarClick((Number(timestamp) + 60) * 1000 * 1000)
@@ -3,7 +3,7 @@ import { useRouter } from 'next/router'
import { observer } from 'mobx-react-lite'
import { API_URL, IS_PLATFORM } from 'lib/constants'
import { useStore, withAuth } from 'hooks'
import { useStore, withAuth, useFlag } from 'hooks'
import WithSidebar from './WithSidebar'
import { auth } from 'lib/gotrue'
@@ -17,9 +17,11 @@ import { auth } from 'lib/gotrue'
const AccountLayout = ({ children, title, breadcrumbs }: any) => {
const router = useRouter()
const { app, ui } = useStore()
const ongoingIncident = useFlag('ongoingIncident')
const maxHeight = ongoingIncident ? 'calc(100vh - 44px)' : '100vh'
const onClickLogout = async () => {
await auth.signOut()
router.reload()
@@ -117,7 +119,7 @@ const AccountLayout = ({ children, title, breadcrumbs }: any) => {
</Head>
<div className="flex h-full">
<main
style={{ maxHeight: '100vh' }}
style={{ height: maxHeight, maxHeight }}
className="flex w-full flex-1 flex-col overflow-y-auto"
>
<WithSidebar title={title} breadcrumbs={breadcrumbs} links={linksWithHeaders}>
@@ -1,7 +1,8 @@
import Link from 'next/link'
import { CSSProperties, FC, ReactNode } from 'react'
import { Menu, Typography, IconArrowUpRight, Badge } from '@supabase/ui'
import { FC, ReactNode } from 'react'
import { isUndefined } from 'lodash'
import { Menu, Typography, IconArrowUpRight, Badge } from '@supabase/ui'
import { useFlag } from 'hooks'
import LayoutHeader from '../ProjectLayout/LayoutHeader'
interface Props {
@@ -11,7 +12,6 @@ interface Props {
header?: ReactNode
subitems?: any[]
subitemsParentKey?: number
sidebarStyle?: CSSProperties
hideSidebar?: boolean
customSidebarContent?: ReactNode
children: ReactNode
@@ -22,7 +22,6 @@ const WithSidebar: FC<Props> = ({
header,
breadcrumbs = [],
children,
sidebarStyle,
links,
subitems,
subitemsParentKey,
@@ -32,20 +31,23 @@ const WithSidebar: FC<Props> = ({
const noContent = !links && !customSidebarContent
const linksHaveHeaders = links && links[0].heading
const ongoingIncident = useFlag('ongoingIncident')
const maxHeight = ongoingIncident ? 'calc(100vh - 44px)' : '100vh'
return (
<div className={`flex `}>
<div className="flex max-h-full">
{!hideSidebar && !noContent && (
<div
id="with-sidebar"
className="
w-64 h-screen overflow-auto bg-sidebar-linkbar-light dark:bg-sidebar-linkbar-dark hide-scrollbar
border-r dark:border-dark
"
style={sidebarStyle}
style={{ height: maxHeight, maxHeight }}
className={[
'bg-sidebar-linkbar-light dark:bg-sidebar-linkbar-dark h-full',
'hide-scrollbar dark:border-dark w-64 overflow-auto border-r',
].join(' ')}
>
{title && (
<div className="mb-2">
<div className="max-h-12 h-12 flex items-center border-b dark:border-dark px-6">
<div className="dark:border-dark flex h-12 max-h-12 items-center border-b px-6">
<Typography.Title level={4} className="mb-0">
{title}
</Typography.Title>
@@ -74,9 +76,9 @@ const WithSidebar: FC<Props> = ({
</div>
</div>
)}
<div className="flex-1 flex flex-col h-screen">
<div className="flex flex-1 flex-col">
<LayoutHeader breadcrumbs={breadcrumbs} />
<div className="flex-grow flex-1 overflow-auto">{children}</div>
<div className="flex-1 flex-grow overflow-auto">{children}</div>
</div>
</div>
)
@@ -85,10 +87,10 @@ export default WithSidebar
const LinksWithHeaders: FC<any> = ({ links, subitems, subitemsParentKey }) => {
return links.map((x: any) => (
<div key={x.heading} className="py-5 border-b dark:border-dark px-6">
<div key={x.heading} className="dark:border-dark border-b py-5 px-6">
<Menu.Group title={x.heading} />
{x.versionLabel && (
<div className="px-3 mb-1">
<div className="mb-1 px-3">
<Badge color="yellow">{x.versionLabel}</Badge>
</div>
)}
@@ -156,7 +158,7 @@ const SidebarItem: FC<any> = ({ id, label, href, isActive, isSubitem, onClick, e
onClick={onClick || (() => {})}
icon={external && <IconArrowUpRight size={'tiny'} />}
>
{isSubitem ? <Typography.Text small>{label}</Typography.Text> : label}
{isSubitem ? <p className="text-sm">{label}</p> : label}
</Menu.Item>
)
}
@@ -165,15 +167,18 @@ const SidebarItem: FC<any> = ({ id, label, href, isActive, isSubitem, onClick, e
<Link href={href || ''}>
<a className="block" target={external ? '_blank' : '_self'}>
<button
className="cursor-pointer flex space-x-2 items-center outline-none focus-visible:ring-1 ring-scale-1200 focus-visible:z-10 group py-1 font-normal border-scale-500 group-hover:border-scale-900"
className="ring-scale-1200 border-scale-500 group-hover:border-scale-900 group flex max-w-full cursor-pointer items-center space-x-2 py-1 font-normal outline-none focus-visible:z-10 focus-visible:ring-1"
onClick={onClick || (() => {})}
>
{external && (
<span className="transition truncate text-sm text-scale-900 group-hover:text-scale-1100">
<span className="text-scale-900 group-hover:text-scale-1100 truncate text-sm transition">
<IconArrowUpRight size={'tiny'} />
</span>
)}
<span className="transition truncate text-sm w-full text-scale-1100 group-hover:text-scale-1200">
<span
title={label}
className="text-scale-1100 group-hover:text-scale-1200 w-full truncate text-sm transition"
>
{isSubitem ? <p>{label}</p> : label}
</span>
</button>
+3 -3
View File
@@ -1,9 +1,9 @@
import Link from 'next/link'
import { FC, ReactNode } from 'react'
import { useStore, withAuth } from 'hooks'
import { Button, IconX } from '@supabase/ui'
import { observer } from 'mobx-react-lite'
import { IconX } from '@supabase/ui'
import { headWithTimeout } from 'lib/common/fetch'
import BaseLayout from 'components/layouts'
interface Props {
@@ -55,4 +55,4 @@ const BillingLayout: FC<Props> = ({ children }) => {
)
}
export default withAuth(BillingLayout)
export default withAuth(observer(BillingLayout))
@@ -1,6 +1,5 @@
import { FC, Fragment } from 'react'
import { observer } from 'mobx-react-lite'
import { Typography, IconChevronRight } from '@supabase/ui'
interface Props {
defaultValue: any
@@ -27,16 +26,15 @@ const BreadcrumbsView: FC<Props> = ({ defaultValue: breadcrumbs }) => {
<path d="M16 3.549L7.12 20.600"></path>
</svg>
</span>
<Typography.Text>
<a
onClick={breadcrumb.onClick || (() => {})}
className={`block px-2 py-1 text-xs leading-5 text-gray-1100 focus:outline-none focus:bg-gray-100 focus:text-gray-900 ${
breadcrumb.onClick ? 'cursor-pointer hover:text-white' : ''
}`}
>
{breadcrumb.label}
</a>
</Typography.Text>
<a
onClick={breadcrumb.onClick || (() => {})}
className={`text-gray-1100 block px-2 py-1 text-xs leading-5 focus:bg-gray-100 focus:text-gray-900 focus:outline-none ${
breadcrumb.onClick ? 'cursor-pointer hover:text-white' : ''
}`}
>
{breadcrumb.label}
</a>
</Fragment>
))
: null}
@@ -8,6 +8,7 @@ import OrgDropdown from './OrgDropdown'
import ProjectDropdown from './ProjectDropdown'
import FeedbackDropdown from './FeedbackDropdown'
import HelpPopover from './HelpPopover'
import NotificationsPopover from './NotificationsPopover'
const LayoutHeader = ({ customHeaderComponents, breadcrumbs = [], headerBorder = true }: any) => {
const { ui } = useStore()
@@ -15,13 +16,11 @@ const LayoutHeader = ({ customHeaderComponents, breadcrumbs = [], headerBorder =
return (
<div
className={`py-2 px-5 max-h-12 h-12 flex justify-between items-center ${
headerBorder ? 'border-b dark:border-dark' : ''
className={`flex h-12 max-h-12 items-center justify-between py-2 px-5 ${
headerBorder ? 'dark:border-dark border-b' : ''
}`}
>
{/* <div className="PageHeader"> */}
{/* <div className="flex justify-between"> */}
<div className="text-sm flex items-center -ml-2">
<div className="-ml-2 flex items-center text-sm">
{/* Organization is selected */}
{selectedOrganization ? (
<>
@@ -54,7 +53,7 @@ const LayoutHeader = ({ customHeaderComponents, breadcrumbs = [], headerBorder =
) : (
<Link href="/">
<a
className={`text-xs text-scale-1200 px-2 py-1 focus:outline-none focus:bg-transparent cursor-pointer`}
className={`text-scale-1200 cursor-pointer px-2 py-1 text-xs focus:bg-transparent focus:outline-none`}
>
Supabase
</a>
@@ -63,10 +62,13 @@ const LayoutHeader = ({ customHeaderComponents, breadcrumbs = [], headerBorder =
{/* Additional breadcrumbs are supplied */}
<BreadcrumbsView defaultValue={breadcrumbs} />
</div>
<div className="flex space-x-2">
<div className="flex items-center space-x-6">
{customHeaderComponents && customHeaderComponents}
{IS_PLATFORM && <HelpPopover />}
{IS_PLATFORM && <FeedbackDropdown />}
<div className="flex items-center space-x-2">
{IS_PLATFORM && <HelpPopover />}
{IS_PLATFORM && <FeedbackDropdown />}
</div>
{IS_PLATFORM && <NotificationsPopover />}
</div>
</div>
// </div>
@@ -0,0 +1,41 @@
import { FC } from 'react'
import { useRouter } from 'next/router'
import { Button } from '@supabase/ui'
import { Project } from 'types'
interface Props {
project: Project
availableActions: any[]
onSelectRestartProject: () => void
}
const NotificationActions: FC<Props> = ({ project, availableActions, onSelectRestartProject }) => {
const router = useRouter()
const onSelectUpgradeProject = () => {
return router.push(`/project/${project.ref}/settings/billing/update/pro`)
}
return (
<div className="space-y-2">
{availableActions.map((action: any) => {
if (action.action_type === 'project.upgrade') {
return (
<Button key={action.action_type} type="default" onClick={onSelectUpgradeProject}>
Upgrade project
</Button>
)
} else if (action.action_type === 'postgresql.restart') {
return (
<Button key={action.action_type} type="default" onClick={onSelectRestartProject}>
Restart project
</Button>
)
}
})}
</div>
)
}
export default NotificationActions
@@ -0,0 +1,49 @@
import dayjs from 'dayjs'
import { FC } from 'react'
import { Notification, NotificationStatus } from '@supabase/shared-types/out/notifications'
import { useStore } from 'hooks'
import { Project } from 'types'
import { formatNotificationCTAText, formatNotificationText } from './NotificationRows.utils'
import NotificationActions from './NotificationActions'
interface Props {
notification: Notification
onSelectRestartProject: (project: Project, notification: Notification) => void
}
const NotificationRow: FC<Props> = ({ notification, onSelectRestartProject }) => {
const { app } = useStore()
const [project] = app.projects.list((project: Project) => project.id === notification.project_id)
const insertedAt = dayjs(notification.inserted_at).format('DD MMM YYYY, HH:mma')
const availableActions = notification.meta?.actions_available ?? []
return (
<div className="flex py-2">
<div className="flex w-[50px] justify-center">
{notification.notification_status !== NotificationStatus.Seen && (
<div className="mt-1.5 h-2 w-2 rounded-full bg-green-900" />
)}
</div>
<div className="mr-8 flex items-center space-x-4">
<div className="space-y-1">
<p className="text-sm">{formatNotificationText(project, notification)}</p>
<p className="text-sm">{formatNotificationCTAText(availableActions)}</p>
<p className="text-scale-1100 text-sm">{insertedAt}</p>
</div>
<div className="col-span-3 flex items-center justify-end">
{availableActions.length > 0 && (
<NotificationActions
project={project}
availableActions={availableActions}
onSelectRestartProject={() => onSelectRestartProject(project, notification)}
/>
)}
</div>
</div>
</div>
)
}
export default NotificationRow
@@ -0,0 +1,71 @@
import { Project } from 'types'
import {
Action,
ActionType,
ExtensionsUpgrade,
Notification,
NotificationName,
PostgresqlUpgradeData,
ProjectExceedingTierLimitData,
ProjectUpdateData,
ServerUpgrade,
ServiceUpgrade,
ViolatedLimit,
} from '@supabase/shared-types/out/notifications'
export const formatNotificationText = (project: Project, notification: Notification) => {
const projectName = project.name
const notificationName = notification.notification_name
if (notificationName === NotificationName.ProjectExceedingTierLimit) {
const { violations } = notification.data as ProjectExceedingTierLimitData
const violationsText = violations
.map((violation: ViolatedLimit) => violation.dimension as string)
.reduce((a: string, b: string) => `${a}, ${b}`)
return `Your project "${projectName}" has exceeded its limits in the following areas: ${violationsText}.`
} else if (notificationName === NotificationName.PostgresqlUpgradeAvailable) {
const { upgrade_type, additional } = notification.data as PostgresqlUpgradeData
if (upgrade_type === 'postgresql-server') {
const { version_to } = additional as ServerUpgrade
return `New version of Postgres (${version_to}) is now available for project "${projectName}".`
} else if (upgrade_type === 'extensions') {
const { name, version_to } = additional as ExtensionsUpgrade
return `New version of "${name}" (${version_to}) is now available for project "${projectName}".`
}
return ''
} else if (notificationName === NotificationName.PostgresqlUpgradeCompleted) {
const { upgrade_type, additional } = notification.data as PostgresqlUpgradeData
if (upgrade_type === 'postgresql-server') {
const { version_to } = additional as ServerUpgrade
return `Postgres (${version_to}) has been successfully updated to ${version_to} for project "${projectName}".`
} else if (upgrade_type === 'extensions') {
const { name, version_to } = additional as ExtensionsUpgrade
return `The extension "${name}" has been successfully updated to ${version_to} for project "${projectName}`
}
} else if (notificationName === 'project.update-completed') {
const { upgrades } = notification.data as ProjectUpdateData
const upgradesText = upgrades
.map((upgrade: ServiceUpgrade) => upgrade.name)
.reduce((a: string, b: string) => `${a}, ${b}`)
return `The following services have been successfully updated for project "${projectName}": ${upgradesText}`
} else {
return `Unknown notification type: ${notification.notification_name} - Please reach out to support for more information`
}
}
export const formatNotificationCTAText = (availableActions: Action[]) => {
const [action] = availableActions
if (!action) return ''
switch (action.action_type) {
case ActionType.SchedulePostgresRestart:
return 'Restart your project to get the latest updates.'
case ActionType.UpgradeProjectToPro:
return 'Upgrade your project to ensure continued availability.'
default:
return ''
}
}
@@ -0,0 +1,145 @@
import { FC, Fragment, useState } from 'react'
import { useRouter } from 'next/router'
import { Button, IconBell, Popover } from '@supabase/ui'
import { Notification, NotificationStatus } from '@supabase/shared-types/out/notifications'
import { Project } from 'types'
import { useNotifications, useStore } from 'hooks'
import { patch, post } from 'lib/common/fetch'
import { API_URL } from 'lib/constants'
import NotificationRow from './NotificationRow'
import ConfirmModal from 'components/ui/Dialogs/ConfirmDialog'
interface Props {}
const NotificationsPopover: FC<Props> = () => {
const router = useRouter()
const { app, ui } = useStore()
const { notifications, refresh } = useNotifications()
const [projectToRestart, setProjectToRestart] = useState<Project>()
const [targetNotification, setTargetNotification] = useState<Notification>()
if (!notifications) return <div />
const hasNewNotifications = notifications?.some(
(notification) => notification.notification_status === NotificationStatus.New
)
const onOpenChange = async (open: boolean) => {
if (!open) {
// Mark notifications as seen
const notificationIds = notifications
.filter((notification) => notification.notification_status === NotificationStatus.New)
.map((notification) => notification.id)
if (notificationIds.length > 0) {
const { error } = await patch(`${API_URL}/notifications`, { ids: notificationIds })
if (error) console.error('Failed to update notifications', error)
refresh()
}
}
}
const onConfirmProjectRestart = async () => {
if (!projectToRestart || !targetNotification) return
const { id } = targetNotification
const { id: projectId, ref, region } = projectToRestart
const { error } = await post(`${API_URL}/projects/${ref}/restart-services`, {
restartRequest: {
region,
source_notification_id: id,
services: ['postgresql'],
},
})
if (error) {
ui.setNotification({
category: 'error',
message: `Failed to restart project: ${error.message}`,
error,
})
} else {
app.onProjectPostgrestStatusUpdated(projectId, 'OFFLINE')
ui.setNotification({ category: 'success', message: `Restarting project` })
router.push(`/project/${ref}`)
}
setProjectToRestart(undefined)
setTargetNotification(undefined)
}
return (
<>
<Popover
size="content"
align="end"
side="bottom"
sideOffset={8}
onOpenChange={onOpenChange}
overlay={
<div className="w-[540px]">
<div className="flex items-center justify-between border-b border-gray-500 bg-gray-400 px-4 py-2">
<p className="text-sm">Notifications</p>
{/* Area for improvement: Paginate notifications and show in a side panel */}
{/* <p className="text-scale-1000 hover:text-scale-1200 cursor-pointer text-sm transition">
See all{' '}
{notifications.length > MAX_NOTIFICATIONS_TO_SHOW && `(${notifications.length})`}
</p> */}
</div>
<div className="max-h-[380px] overflow-y-auto py-2">
{notifications.length === 0 ? (
<div className="py-2 px-4">
<p className="text-scale-1000 text-sm">No notifications available</p>
</div>
) : (
<>
{notifications.map((notification, i: number) => (
<Fragment key={notification.id}>
<NotificationRow
notification={notification}
onSelectRestartProject={(project, notification) => {
setProjectToRestart(project)
setTargetNotification(notification)
}}
/>
{i !== notifications.length - 1 && <Popover.Seperator />}
</Fragment>
))}
</>
)}
</div>
</div>
}
>
<div className="relative flex">
<Button
as="span"
id="notification-button"
type="default"
icon={<IconBell size={16} strokeWidth={1.5} className="text-scale-1200" />}
/>
{hasNewNotifications && (
<div className="absolute -top-1 -right-1 z-50 flex h-3 w-3 items-center justify-center">
<div className="h-full w-full animate-ping rounded-full bg-green-800 opacity-60"></div>
<div className="z-60 absolute top-0 right-0 h-full w-full rounded-full bg-green-900 opacity-80"></div>
</div>
)}
</div>
</Popover>
<ConfirmModal
danger
visible={projectToRestart !== undefined}
title={`Restart project "${projectToRestart?.name}"`}
description={`Are you sure you want to restart the project? There will be a few minutes of downtime.`}
buttonLabel="Restart"
buttonLoadingLabel="Restarting"
onSelectCancel={() => setProjectToRestart(undefined)}
onSelectConfirm={onConfirmProjectRestart}
/>
</>
)
}
export default NotificationsPopover
@@ -40,7 +40,7 @@ const ProjectDropdown = () => {
</>
}
>
<Button as="span" type="text" size="tiny">
<Button as="span" type="text" size="tiny" className="my-1">
{selectedProject.name}
</Button>
</Dropdown>
@@ -6,7 +6,7 @@ import { isUndefined } from 'lodash'
import { Button, IconHome, Dropdown, IconUser, IconSettings } from '@supabase/ui'
import { IS_PLATFORM } from 'lib/constants'
import { useStore } from 'hooks'
import { useStore, useFlag } from 'hooks'
import { generateProductRoutes, generateOtherRoutes } from './NavigationBar.utils'
import NavigationIconButton from './NavigationIconButton'
@@ -18,22 +18,19 @@ const NavigationBar: FC<Props> = ({}) => {
const projectRef = ui.selectedProjectRef as string
const projectBaseInfo = ui.selectedProjectBaseInfo
const ongoingIncident = useFlag('ongoingIncident')
const activeRoute = router.pathname.split('/')[3]
const productRoutes = generateProductRoutes(projectRef, projectBaseInfo)
const otherRoutes = generateOtherRoutes(projectRef, projectBaseInfo)
return (
<div
className="
h-screen w-14
flex flex-col
justify-between
p-2
overflow-y-hidden
bg-sidebar-light
dark:bg-sidebar-dark
border-r
dark:border-dark"
style={{ height: ongoingIncident ? 'calc(100vh - 44px)' : '100vh' }}
className={[
'flex w-14 flex-col justify-between overflow-y-hidden p-2',
'bg-sidebar-light dark:bg-sidebar-dark dark:border-dark border-r',
].join(' ')}
>
<ul className="flex flex-col space-y-2">
<Link href={'/'}>
@@ -41,7 +38,7 @@ const NavigationBar: FC<Props> = ({}) => {
<img
src="/img/supabase-logo.svg"
alt="Supabase"
className="rounded h-[40px] w-6 mx-auto cursor-pointer"
className="mx-auto h-[40px] w-6 cursor-pointer rounded"
/>
</a>
</Link>
@@ -54,7 +51,7 @@ const NavigationBar: FC<Props> = ({}) => {
link: `/project/${projectRef}`,
}}
/>
<div className="h-px w-full bg-scale-500"></div>
<div className="bg-scale-500 h-px w-full"></div>
{productRoutes.map((route) => (
<NavigationIconButton
key={route.key}
@@ -62,7 +59,7 @@ const NavigationBar: FC<Props> = ({}) => {
isActive={activeRoute === route.key}
/>
))}
<div className="h-px w-full bg-scale-500"></div>
<div className="bg-scale-500 h-px w-full"></div>
{otherRoutes.map((route) => (
<NavigationIconButton
key={route.key}
@@ -1,5 +1,6 @@
import { FC, ReactNode } from 'react'
import { Typography } from '@supabase/ui'
import { useFlag } from 'hooks'
interface Props {
title: string
@@ -7,21 +8,25 @@ interface Props {
}
const ProductMenuBar: FC<Props> = ({ title, children }) => {
const ongoingIncident = useFlag('ongoingIncident')
const maxHeight = ongoingIncident ? 'calc(100vh - 44px)' : '100vh'
return (
<div
style={{ height: maxHeight, maxHeight }}
className={[
'w-64 h-screen flex flex-col hide-scrollbar border-r', // Layout
'hide-scrollbar flex w-64 flex-col border-r', // Layout
'bg-sidebar-linkbar-light', // Light mode
'dark:bg-sidebar-linkbar-dark dark:border-dark ', // Dark mode
].join(' ')}
>
<div
className="px-6 max-h-12 border-b dark:border-dark flex items-center"
className="dark:border-dark flex max-h-12 items-center border-b px-6"
style={{ minHeight: '3rem' }}
>
<Typography.Title level={4}>{title}</Typography.Title>
</div>
<div className="overflow-y-auto flex-grow">{children}</div>
<div className="flex-grow overflow-y-auto">{children}</div>
</div>
)
}
@@ -2,7 +2,7 @@ import Head from 'next/head'
import { FC, ReactNode, PropsWithChildren } from 'react'
import { observer } from 'mobx-react-lite'
import { useRouter } from 'next/router'
import { useStore, withAuth } from 'hooks'
import { useStore, withAuth, useFlag } from 'hooks'
import { PROJECT_STATUS } from 'lib/constants'
import Connecting from 'components/ui/Loading'
@@ -30,6 +30,8 @@ const ProjectLayout = ({
hideHeader = false,
hideIconBar = false,
}: PropsWithChildren<Props>) => {
const ongoingIncident = useFlag('ongoingIncident')
return (
<>
<Head>
@@ -47,8 +49,8 @@ const ProjectLayout = ({
</MenuBarWrapper>
<main
style={{ maxHeight: '100vh' }}
className="flex w-full flex-1 flex-col overflow-x-hidden"
style={{ height: ongoingIncident ? 'calc(100vh - 44px)' : '100vh' }}
>
{!hideHeader && <LayoutHeader />}
<ContentWrapper isLoading={isLoading}>{children}</ContentWrapper>
+5 -2
View File
@@ -1,12 +1,15 @@
import { FC } from 'react'
import Link from 'next/link'
import { IconChevronRight } from '@supabase/ui'
import { withAuth } from 'hooks'
import { withAuth, useFlag } from 'hooks'
import { observer } from 'mobx-react-lite'
const WizardLayout: FC<any> = ({ organization, project, children }) => {
const ongoingIncident = useFlag('ongoingIncident')
const maxHeight = ongoingIncident ? 'calc(100vh - 44px)' : '100vh'
return (
<div className="flex h-full w-full flex-col">
<div className="flex w-full flex-col" style={{ height: maxHeight, maxHeight }}>
<Header organization={organization} project={project} />
<div className="overflow-auto">
<section className="has-slide-in slide-in relative mx-auto my-10 max-w-2xl">
@@ -273,8 +273,6 @@ export function BarChart({
/>
))}
</Bar>
<ReferenceLine y={8000000000} label="8GB Included" stroke="red" strokeDasharray="3 3" />
</RechartBarChart>
</ResponsiveContainer>
{data && (
@@ -0,0 +1,30 @@
import { FC } from 'react'
import { IconAlertCircle } from '@supabase/ui'
import InformationBox from 'components/ui/InformationBox'
interface Props {
title: string
}
const DisabledWarningDueToIncident: FC<Props> = ({ title }) => {
return (
<InformationBox
icon={<IconAlertCircle size={20} strokeWidth={1.5} />}
defaultVisibility={true}
hideCollapse
title={title}
description={
<div className="space-y-3">
<p className="text-sm leading-normal">
Our engineers are currently working on a fix. You can follow updates on{' '}
<a className="text-brand-900" href="https://status.supabase.com/">
https://status.supabase.com/
</a>
</p>
</div>
}
/>
)
}
export default DisabledWarningDueToIncident
+1 -1
View File
@@ -60,7 +60,7 @@ const InformationBox: FC<Props> = ({
}`}
style={{ maxHeight: isExpanded ? 500 : 0 }}
>
<p className="text-scale-1100 text-sm">{description}</p>
<div className="text-scale-1100 text-sm">{description}</div>
{url && (
<a
+1
View File
@@ -1,5 +1,6 @@
export * from './misc/useStore'
export * from './misc/withAuth'
export * from './misc/useNotifications'
export * from './deprecated'
export * from './queries'
export * from './ui'
+18
View File
@@ -0,0 +1,18 @@
import useSWR from 'swr'
import { Notification } from '@supabase/shared-types/out/notifications'
import { get } from 'lib/common/fetch'
import { API_URL } from 'lib/constants'
export const useNotifications = () => {
const { data, error, mutate } = useSWR<Notification[]>(`${API_URL}/notifications`, get)
const anyError = error !== undefined
const refresh = () => mutate()
return {
notifications: anyError ? undefined : data,
isLoading: !anyError && !data,
isError: !!anyError,
refresh,
}
}
+7 -2
View File
@@ -26,7 +26,12 @@ import { StoreProvider } from 'hooks'
import { getParameterByName } from 'lib/common/fetch'
import { GOTRUE_ERRORS } from 'lib/constants'
import { PortalToast, GoTrueWrapper, RouteValidationWrapper } from 'components/interfaces/App'
import {
PortalToast,
GoTrueWrapper,
RouteValidationWrapper,
AppBannerWrapper,
} from 'components/interfaces/App'
import PageTelemetry from 'components/ui/PageTelemetry'
import FlagProvider from 'components/ui/Flag/FlagProvider'
@@ -62,7 +67,7 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) {
<GoTrueWrapper>
<PageTelemetry>
<RouteValidationWrapper>
{getLayout(<Component {...pageProps} />)}
<AppBannerWrapper>{getLayout(<Component {...pageProps} />)}</AppBannerWrapper>
</RouteValidationWrapper>
</PageTelemetry>
</GoTrueWrapper>
+15 -113
View File
@@ -7,9 +7,10 @@ import { useRef, useState, useEffect } from 'react'
import { debounce, isUndefined, values } from 'lodash'
import { toJS } from 'mobx'
import { observer } from 'mobx-react-lite'
import { Button, Listbox, IconUsers, IconAlertCircle, Input, IconLoader, Alert } from '@supabase/ui'
import { Button, Listbox, IconUsers, Input, IconLoader, Alert } from '@supabase/ui'
import { getURL, passwordStrength } from 'lib/helpers'
import { NextPageWithLayout } from 'types'
import { passwordStrength } from 'lib/helpers'
import { post } from 'lib/common/fetch'
import {
API_URL,
@@ -23,14 +24,17 @@ import {
DEFAULT_FREE_PROJECTS_LIMIT,
PRICING_TIER_PRODUCT_IDS,
} from 'lib/constants'
import { useStore, withAuth, useSubscriptionStats } from 'hooks'
import { useStore, useFlag, withAuth, useSubscriptionStats } from 'hooks'
import { WizardLayoutWithoutAuth } from 'components/layouts'
import Panel from 'components/to-be-cleaned/Panel'
import InformationBox from 'components/ui/InformationBox'
import PasswordStrengthBar from 'components/ui/PasswordStrengthBar'
import { AddNewPaymentMethodModal } from 'components/interfaces/Billing'
import { NextPageWithLayout } from 'types'
import DisabledWarningDueToIncident from 'components/ui/DisabledWarningDueToIncident'
import {
FreeProjectLimitWarning,
NotOrganizationOwnerWarning,
EmptyPaymentMethodWarning,
} from 'components/interfaces/Organization/NewProject'
interface StripeCustomer {
paymentMethods: any
@@ -38,29 +42,6 @@ interface StripeCustomer {
error?: any
}
const DisabledBox = () => {
return (
<div className="mt-4">
<InformationBox
icon={<IconAlertCircle className="text-white" size="large" strokeWidth={1.5} />}
defaultVisibility={true}
hideCollapse
title="Project creation is currently disabled"
description={
<div className="space-y-3">
<p className="text-sm leading-normal">
Our engineers are currently working on a fix. You can follow updates on{' '}
<a className="text-brand-900" href="https://status.supabase.com/">
https://status.supabase.com/
</a>
</p>
</div>
}
/>
</div>
)
}
async function fetchStripeAccount(stripeCustomerId: string) {
try {
const customer = await post(`${API_URL}/stripe/customer`, {
@@ -79,6 +60,7 @@ const Wizard: NextPageWithLayout = () => {
const { app, ui } = useStore()
const subscriptionStats = useSubscriptionStats()
const projectCreationDisabled = useFlag('disableProjectCreationAndUpdate')
const [projectName, setProjectName] = useState('')
const [dbPass, setDbPass] = useState('')
@@ -211,11 +193,6 @@ const Wizard: NextPageWithLayout = () => {
}
}
/**
* Disable project creation override
*/
const projectCreationDisabled = false
return (
<Panel
hideHeaderStyling
@@ -231,7 +208,9 @@ const Wizard: NextPageWithLayout = () => {
Cancel
</Button>
<div className="items-center space-x-3">
<span className="text-scale-900 text-xs">You can rename your project later</span>
{!projectCreationDisabled && (
<span className="text-scale-900 text-xs">You can rename your project later</span>
)}
<Button
onClick={onClickNext}
loading={newProjectedLoading}
@@ -254,7 +233,7 @@ const Wizard: NextPageWithLayout = () => {
</Panel.Content>
{projectCreationDisabled ? (
<Panel.Content className="border-panel-border-interior-light dark:border-panel-border-interior-dark border-t pb-8">
<DisabledBox />
<DisabledWarningDueToIncident title="Project creation is currently disabled" />
</Panel.Content>
) : (
<>
@@ -432,80 +411,3 @@ const PageLayout = withAuth(
Wizard.getLayout = (page) => <PageLayout>{page}</PageLayout>
export default observer(Wizard)
const NotOrganizationOwnerWarning = () => {
return (
<div className="mt-4">
<InformationBox
icon={<IconAlertCircle className="text-white" size="large" strokeWidth={1.5} />}
defaultVisibility={true}
hideCollapse
title="You do not have permission to create a project"
description={
<div className="space-y-3">
<p className="text-sm leading-normal">
Only the organization owner can create new projects. Contact your organization owner
to create a new project for this organization.
</p>
</div>
}
/>
</div>
)
}
const FreeProjectLimitWarning = ({ limit }: { limit: number }) => {
return (
<div className="mt-4">
<InformationBox
icon={<IconAlertCircle className="text-white" size="large" strokeWidth={1.5} />}
defaultVisibility={true}
hideCollapse
title="Your account has reached its free project limit"
description={
<div className="space-y-3">
<p className="text-sm leading-normal">
{`Your account can only have up to ${limit} free projects - to create another free project, you'll need to delete an existing free project first. Otherwise, you may create a project on the Pro tier instead.`}
</p>
</div>
}
/>
</div>
)
}
const EmptyPaymentMethodWarning = observer(
({ stripeCustomerId }: { stripeCustomerId: string | undefined }) => {
const { ui } = useStore()
const slug = ui.selectedOrganization?.slug
const [showAddPaymentMethodModal, setShowAddPaymentMethodModal] = useState<boolean>(false)
return (
<div className="mt-4">
<InformationBox
icon={<IconAlertCircle className="text-white" size="large" strokeWidth={1.5} />}
defaultVisibility={true}
hideCollapse
title="Your organization has no payment methods"
description={
<div className="space-y-3">
<p className="text-sm leading-normal">
You need to add a payment method for your organization before creating a paid
project.
</p>
<Button type="secondary" onClick={() => setShowAddPaymentMethodModal(true)}>
Add a payment method
</Button>
</div>
}
/>
<AddNewPaymentMethodModal
visible={showAddPaymentMethodModal}
returnUrl={`${getURL()}/new/${slug}`}
onCancel={() => setShowAddPaymentMethodModal(false)}
/>
</div>
)
}
)
+1 -1
View File
@@ -145,7 +145,7 @@ const Settings = () => {
<AutoField
name="URI_ALLOW_LIST"
showInlineError
errorMessage="Must be a comma separated list of exact URIs. No spaces."
errorMessage="Must be a comma separated list of URIs. No spaces."
/>
<NumField name="JWT_EXP" step="1" />
<NumField name="PASSWORD_MIN_LENGTH" step="1" min={6} max={10} />
@@ -14,8 +14,7 @@ import DateRangePicker from 'components/to-be-cleaned/DateRangePicker'
import { PaygStats } from 'components/interfaces/Billing/PAYGUsage/PAYGUsage.types'
import { NextPageWithLayout } from 'types'
type ProjectBillingProps = {} & any
const ProjectBilling: NextPageWithLayout = ({ store }: ProjectBillingProps) => {
const ProjectBilling: NextPageWithLayout = () => {
const { ui } = useStore()
const project = ui.selectedProject
@@ -2,25 +2,29 @@ import { useState, useEffect } from 'react'
import { observer } from 'mobx-react-lite'
import { useRouter } from 'next/router'
import { useStore } from 'hooks'
import { NextPageWithLayout } from 'types'
import { useStore, useFlag } from 'hooks'
import { get } from 'lib/common/fetch'
import { API_URL, STRIPE_PRODUCT_IDS } from 'lib/constants'
import { BillingLayout } from 'components/layouts'
import { ExitSurvey, StripeSubscription } from 'components/interfaces/Billing'
import { NextPageWithLayout } from 'types'
const BillingUpdateFree: NextPageWithLayout = () => {
const { ui } = useStore()
const router = useRouter()
const projectRef = ui.selectedProject?.ref
const projectUpdateDisabled = useFlag('disableProjectCreationAndUpdate')
const [products, setProducts] = useState<{ tiers: any[]; addons: any[] }>()
const [subscription, setSubscription] = useState<StripeSubscription>()
const [isLoadingProducts, setIsLoadingProducts] = useState(false)
useEffect(() => {
if (projectRef) {
if (projectUpdateDisabled) {
router.push(`/project/${projectRef}/settings/billing/update`)
} else if (projectRef) {
getStripeProducts()
getSubscription()
}
@@ -2,7 +2,7 @@ import { useState, useEffect } from 'react'
import { observer } from 'mobx-react-lite'
import { useRouter } from 'next/router'
import { useStore } from 'hooks'
import { useStore, useFlag } from 'hooks'
import { get } from 'lib/common/fetch'
import { API_URL } from 'lib/constants'
@@ -19,6 +19,8 @@ const BillingUpdatePro: NextPageWithLayout = () => {
const projectRef = ui.selectedProject?.ref
const orgSlug = ui.selectedOrganization?.slug
const projectUpdateDisabled = useFlag('disableProjectCreationAndUpdate')
const [isLoadingProducts, setIsLoadingProducts] = useState(false)
const [isLoadingPaymentMethods, setIsLoadingPaymentMethods] = useState(false)
@@ -34,7 +36,9 @@ const BillingUpdatePro: NextPageWithLayout = () => {
}, [])
useEffect(() => {
if (projectRef) {
if (projectUpdateDisabled) {
router.push(`/project/${projectRef}/settings/billing/update`)
} else if (projectRef) {
getStripeProducts()
getSubscription()
}
+20 -12
View File
@@ -16,7 +16,7 @@ import Link from 'next/link'
import { useRouter } from 'next/router'
import { API_URL, PRICING_TIER_PRODUCT_IDS } from 'lib/constants'
import { useStore, withAuth } from 'hooks'
import { useStore, withAuth, useFlag } from 'hooks'
import { post, get } from 'lib/common/fetch'
import { Project } from 'types'
import { isUndefined } from 'lodash'
@@ -125,6 +125,9 @@ const SupportNew = () => {
const [loading, setLoading] = useState<boolean>(false)
const [sent, setSent] = useState<boolean>(false)
const ongoingIncident = useFlag('ongoingIncident')
const maxHeight = ongoingIncident ? 'calc(100vh - 44px)' : '100vh'
/*
* Get all orgs and projects from global store
*/
@@ -178,7 +181,8 @@ const SupportNew = () => {
if (x.name === 'project') {
const selectedProject = projects.find((project: any) => project.ref === x.value)
if (
(selectedProject?.subscription_tier ?? PRICING_TIER_PRODUCT_IDS.FREE) === PRICING_TIER_PRODUCT_IDS.FREE &&
(selectedProject?.subscription_tier ?? PRICING_TIER_PRODUCT_IDS.FREE) ===
PRICING_TIER_PRODUCT_IDS.FREE &&
formState.severity.value === 'Critical'
) {
formDispatch({
@@ -250,7 +254,7 @@ const SupportNew = () => {
const Success = () => {
return (
<div className="w-100 px-6 space-y-4">
<div className="w-100 space-y-4 px-6">
<div className="relative flex">
<IconCheck size={24} background={'brand'} />
</div>
@@ -272,8 +276,11 @@ const SupportNew = () => {
if (!isInitialized) return <Connecting />
return (
<div className="flex h-screen relative overflow-y-auto overflow-x-hidden">
<div className="max-w-2xl mx-auto my-8 px-4 lg:px-6">
<div
className="relative flex overflow-y-auto overflow-x-hidden"
style={{ height: maxHeight, maxHeight }}
>
<div className="mx-auto my-8 max-w-2xl px-4 lg:px-6">
<Button
type="text"
className="opacity-50 hover:opacity-100"
@@ -285,14 +292,14 @@ const SupportNew = () => {
>
Go back
</Button>
<div className="py-8 space-y-12">
<div className="space-y-12 py-8">
<div className="flex items-center space-x-3">
<SVG src={`/img/supabase-logo.svg`} className="w-4 h-4" />
<SVG src={`/img/supabase-logo.svg`} className="h-4 w-4" />
<Typography.Title level={4} className="m-0">
Supabase support
</Typography.Title>
</div>
<div className="bg-panel-body-light dark:bg-panel-body-dark py-8 rounded border dark:border-dark shadow-md space-y-12 min-w-full">
<div className="bg-panel-body-light dark:bg-panel-body-dark dark:border-dark min-w-full space-y-12 rounded border py-8 shadow-md">
{sent ? (
<Success />
) : (
@@ -319,7 +326,7 @@ const SupportNew = () => {
return (
<>
<span>{option.label}</span>
<span className="opacity-50 block text-xs">
<span className="block text-xs opacity-50">
{option.description}
</span>
</>
@@ -351,7 +358,7 @@ const SupportNew = () => {
return (
<div>
<span>{option.name}</span>
<span className="opacity-50 block text-xs">
<span className="block text-xs opacity-50">
{organization?.name}
</span>
</div>
@@ -375,7 +382,8 @@ const SupportNew = () => {
(project: any) => project.ref === formState.project.value
)
const isAllowedCritical =
(selectedProject?.subscription_tier ?? PRICING_TIER_PRODUCT_IDS.FREE) !== PRICING_TIER_PRODUCT_IDS.FREE
(selectedProject?.subscription_tier ?? PRICING_TIER_PRODUCT_IDS.FREE) !==
PRICING_TIER_PRODUCT_IDS.FREE
return (
<Listbox.Option
key={`option-${option.value}`}
@@ -386,7 +394,7 @@ const SupportNew = () => {
return (
<>
<span>{option.label}</span>
<span className="opacity-50 block text-xs">
<span className="block text-xs opacity-50">
{option.description}
</span>
</>
@@ -58,7 +58,7 @@
"URI_ALLOW_LIST": {
"title": "Additional redirect URLs",
"type": "string",
"help": "A comma separated list of *exact* URLs that auth providers are permitted to redirect to post authentication."
"help": "A comma separated list of URLs that auth providers are permitted to redirect to post authentication. Wildcards allowed (e.g. https://*.domain.com)"
},
"DISABLE_SIGNUP": {
"title": "Disable signup",
+8 -2
View File
@@ -11,9 +11,14 @@ To enable Keycloak Auth for your project, you need to set up an Keycloak OAuth a
## Overview
To get started with Keycloak, you can run it in a docker container with: `docker run -e KEYCLOAK_USER=admin -e KEYCLOAK_PASSWORD=admin -p 8080:8080 jboss/keycloak:latest`
This guide will be assuming that you are running keycloak in a docker container as described in the command above.
Keycloak OAuth consists of five broad steps:
- Create a new client in your specified keycloak realm.
- Obtain the `issuer` from the "OpenID Endpoint Configuration". This will be used as the `Keycloak URL`.
- Ensure that the new client has the "Client Protocol" set to "openid-connect" and the "Access Type" is set to "confidential".
- The `Client ID` of the client created will be used as the `client id`.
- Obtain the `Secret` from the credentials tab which will be used as the `client secret`.
@@ -23,12 +28,13 @@ Keycloak OAuth consists of five broad steps:
### Access your Keycloak Admin console
- To get started with Keycloak, you can run it in a docker container with: `docker run -e KEYCLOAK_USER=admin -e KEYCLOAK_PASSWORD=admin -p 8080:8080 jboss/keycloak:latest`
- Login by visiting `http://localhost:8080` and clicking on "Administration Console".
- Login by visiting [`http://localhost:8080`](http://localhost:8080) and clicking on "Administration Console".
### Create a Keycloak Realm
- Once you've logged in to the Keycloak console, you can add a realm from the side panel. The default realm should be named "Master".
- After you've added a new realm, you can retrieve the `issuer` from the "OpenID Endpoint Configuration" endpoint. The `issuer` will be used as the `Keycloak URL`.
- You can find this endpoint from the realm settings under the "General Tab" or visit [`http://localhost:8080/auth/realms/my_realm_name/.well-known/openid-configuration`](http://localhost:8080/auth/realms/my_realm_name/.well-known/openid-configuration)
![Add a Keycloak Realm.](/img/guides/auth-keycloak/keycloak-create-realm.png)
+20 -16
View File
@@ -14,51 +14,55 @@ To enable Notion Auth for your project, you need to set up a Notion Application
Setting up Notion logins for your application consists of 3 parts:
- Create and configure a Notion Application [Notion Developer Portal](https://www.notion.so/my-integrations)
- Add your Notion OAuth Consumer keys to your [Supabase Project](https://app.supabase.com)
- Retrieve your OAuth client ID and OAuth client secret and add them to your [Supabase Project](https://app.supabase.com)
- Add the login code to your [Supabase JS Client App](https://github.com/supabase/supabase-js)
## Steps
### Access your Notion account
### Create your notion integration
- Go to [notion.so](https://notion.so/).
- Click on `Login` at the top right to log in.
- Go to [developers.notion.com](https://developers.notion.com/).
- Click "View my integrations" and login.
![notion.so](/img/guides/auth-notion/notion.png)
- Once logged in, go to [notion.so/my-integrations](https://notion.so/my-integrations).
- Once logged in, go to [notion.so/my-integrations](https://notion.so/my-integrations) and create a new integration.
- When creating your integration, ensure that you select "Public integration" under "Integration type" and "Read user information including email addresses" under "Capabilities".
- You will need to add a redirect uri, see [Add the redirect uri](#add-the-redirect-uri)
- Once you've filled in the necessary fields, click "Submit" to finish creating the integration.
![notion.so](/img/guides/auth-notion/notion-developer.png)
### Find your callback URL
### Add the redirect uri
- After selecting "Public integration", you should see an option to add "Redirect URIs".
In the next step you require a callback URL, which looks like this:
![notion.so](/img/guides/auth-notion/notion-redirect-uri.png)
`https://<project-ref>.supabase.co/auth/v1/callback`
You can retrieve the redirect uri with the following steps:
- Go to your [Supabase Project Dashboard](https://app.supabase.com).
- Click on the `Settings` icon at the bottom of the left sidebar.
- Click on `API` in the list.
- Under Config / URL you'll find your API URL, you can click `Copy` to copy it to the clipboard.
- Now just add `/auth/v1/callback` to the end of that to get your full `OAuth Redirect URI`.
- Add `/auth/v1/callback` to the end of that to get your full `OAuth Redirect URI`.
Your redirect uri should look like the following: `https://<project-ref>.supabase.co/auth/v1/callback`
<video width="99%" muted playsInline controls="true">
<source src="/docs/videos/api/api-url-and-key.mp4" type="video/mp4" muted playsInline />
</video>
### Create a Notion Application
- Click on `New integration` in the center of the screen.
- Enter the name of your application and click `Submit`.
### Add your Notion credentials into your Supabase Project
- Once you've created your notion integration, you should be able to retrieve the "OAuth client ID" and "OAuth client secret" from the "OAuth Domain and URIs" tab.
![notion.so](/img/guides/auth-notion/notion-creds.png)
- Go to your [Supabase Project Dashboard](https://app.supabase.com)
- In the left sidebar, click the `Authentication` icon (near the top)
- Click `Settings` from the list to go to the `Authentication Settings` page
- Enter the final (hosted) URL of your app under `Site URL` (this is important)
- Under `External OAuth Providers` turn `Notion Enabled` to ON
- Enter your `Internal Integration Token` saved in the previous step
- Enter the "OAuth client ID" and "OAuth client secret" obtained in the `client id` and `client secret` fields.
- Click `Save`
### Add login code to your client app
+1 -1
View File
@@ -355,7 +355,7 @@ This limits the potential damage if you allow access to schemas which the user e
className="w-full video-with-border"
width="640"
height="385"
src="https://www.youtube-nocookie.com/embed/I6nnp9AINJk"
src="https://www.youtube-nocookie.com/embed/rARgrELRCwY"
frameBorder="1"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
+1 -1
View File
@@ -147,7 +147,7 @@ When running migrations you need to use the non pooled connection URL (like the
```env
# prisma/.env
postgres://postgres:[YOUR-PASSWORD]@db.vdbnhqozmlzdsaejdxwr.supabase.co:6543/postgres?pgbouncer=true&connection_limtit=1
postgres://postgres:[YOUR-PASSWORD]@db.vdbnhqozmlzdsaejdxwr.supabase.co:6543/postgres?pgbouncer=true&connection_limit=1
```
Prisma Migrate uses database transactions to check out the current state of the database and the migrations table. However, the Migration Engine is designed to use a single connection to the database, and does not support connection pooling with PgBouncer. If you attempt to run Prisma Migrate commands in any environment that uses PgBouncer for connection pooling, you might see the following error:
+14 -1
View File
@@ -236,8 +236,21 @@ supabase db push
### Deploying Edge Functions
Coming soon!
You can deploy any Edge Functions using [`functions deploy`](/docs/reference/cli/supabase-functions-deploy):
```sh
supabase functions deploy <function_name>
```
## Limitations
The local development environment is not as feature-complete as the Plaform. Here are some of the differences:
- The Storage interface is coming soon.
- The Functions interface is coming soon.
- Logs are not supported though the interface (however you can accesss them via the docker containers).
- You cannot update your project settings via the Dashboard - this can be done using the CLI instead.
## Next steps
- Got a question? [Ask in our Discussions](https://github.com/supabase/supabase/discussions).
+1 -1
View File
@@ -61,7 +61,7 @@ values={[
<TabItem value="UI">
```sh
1. Go to the "SQL" section.
1. Go to the "SQL Editor" section.
2. Click "User Management Starter".
3. Click "Run".
```
+1 -1
View File
@@ -45,7 +45,7 @@ values={[
<TabItem value="UI">
```sh
1. Go to the "SQL" section.
1. Go to the "SQL Editor" section.
2. Click "User Management Starter".
3. Click "Run".
```
+1 -1
View File
@@ -51,7 +51,7 @@ values={[
<TabItem value="UI">
```sh
1. Go to the "SQL" section.
1. Go to the "SQL Editor" section.
2. Click "User Management Starter".
3. Click "Run".
```
+1 -1
View File
@@ -61,7 +61,7 @@ values={[
<TabItem value="UI">
```sh
1. Go to the "SQL" section.
1. Go to the "SQL Editor" section.
2. Click "User Management Starter".
3. Click "Run".
```
+1 -1
View File
@@ -61,7 +61,7 @@ values={[
<TabItem value="UI">
```sh
1. Go to the "SQL" section.
1. Go to the "SQL Editor" section.
2. Click "User Management Starter".
3. Click "Run".
```
+1 -1
View File
@@ -61,7 +61,7 @@ values={[
<TabItem value="UI">
```sh
1. Go to the "SQL" section.
1. Go to the "SQL Editor" section.
2. Click "User Management Starter".
3. Click "Run".
```
+1 -1
View File
@@ -69,7 +69,7 @@ values={[
<TabItem value="UI">
```sh
1. Go to the "SQL" section.
1. Go to the "SQL Editor" section.
2. Click "User Management Starter".
3. Click "Run".
```
+1 -1
View File
@@ -68,7 +68,7 @@ values={[
<TabItem value="UI">
```sh
1. Go to the "SQL" section.
1. Go to the "SQL Editor" section.
2. Click "User Management Starter".
3. Click "Run".
```
+1 -1
View File
@@ -115,7 +115,7 @@ values={[
<TabItem value="UI">
```sh
1. Go to the "SQL" section.
1. Go to the "SQL Editor" section.
2. Click "User Management Starter".
4. Click "Run".
```
+5 -6
View File
@@ -68,7 +68,7 @@ values={[
<TabItem value="UI">
```sh
1. Go to the "SQL" section.
1. Go to the "SQL Editor" section.
2. Click "User Management Starter".
3. Click "Run".
```
@@ -426,14 +426,13 @@ Let's create an avatar for the user so that they can upload a profile photo. We
```jsx title="src/Avatar.jsx"
import { createEffect, createSignal } from 'solid-js'
import { supabase } from './supabaseClient'
// import VisuallyHidden from '@reach/visually-hidden'
export default (props) => {
const [avatarUrl, setAvatarUrl] = createSignal(null)
const [uploading, setUploading] = createSignal(false)
createEffect(() => {
if (props.url) downloadImage(props.url)
if (props.url()) downloadImage(props.url())
})
const downloadImage = async (path) => {
@@ -468,7 +467,7 @@ export default (props) => {
throw uploadError
}
props.onUpload(filePath)
props.onUpload(event, filePath)
} catch (error) {
alert(error.message)
} finally {
@@ -523,9 +522,9 @@ return (
<Avatar
url={avatar_url}
size={150}
onUpload={(url) => {
onUpload={(e, url) => {
setAvatarUrl(url)
updateProfile({ username, website, avatar_url: url })
updateProfile(e)
}}
/>
{/* ... */}
+1 -1
View File
@@ -51,7 +51,7 @@ values={[
<TabItem value="UI">
```sh
1. Go to the "SQL" section.
1. Go to the "SQL Editor" section.
2. Click "User Management Starter".
3. Click "Run".
```
+1 -1
View File
@@ -51,7 +51,7 @@ values={[
<TabItem value="UI">
```sh
1. Go to the "SQL" section.
1. Go to the "SQL Editor" section.
2. Click "User Management Starter".
3. Click "Run".
```
+272 -152
View File
File diff suppressed because it is too large. Load diff
+1 -1
View File
@@ -185,7 +185,7 @@ module.exports = {
type: 'category',
label: 'Platform',
collapsed: false,
items: ['guides/platform/logs', 'guides/platform/metrics', 'going-into-prod'],
items: ['guides/platform/logs', 'guides/platform/metrics', 'going-into-prod', 'guides/platform/performance'],
},
{
type: 'category',
+9206 -8495
View File
File diff suppressed because it is too large. Load diff
+2633 -2328
View File
File diff suppressed because it is too large. Load diff
+792 -826
View File
File diff suppressed because it is too large. Load diff
+5781 -5341
View File
File diff suppressed because it is too large. Load diff
+3 -1
View File
@@ -344,7 +344,7 @@ pages:
the Auth server. If you are using email/phone logins you should set up your own redirects (within the email/sms template).
Sometimes you want to control where the user is redirected to after they are logged in. Supabase supports this for
any URL path on your website (the base domain must be the same as the domain in your Auth settings).
any URL path on your website (the URL must either be on the same domain as your Site URL [see Auth>Settings in dashboard], or must match one of the Additional Redirect URLs [also in Auth>Settings]).
js: |
```js
const { user, session, error } = await supabase.auth.signIn({
@@ -1276,6 +1276,8 @@ pages:
```
- name: Listening to row level changes
description: You can listen to individual rows using the format `{table}:{col}=eq.{val}` - where `{col}` is the column name, and `{val}` is the value which you want to match.
notes: |
- ``eq`` filter works with all database types as under the hood, it's casting both the filter value and the database value to the correct type and then comparing them.
js: |
```js
const mySubscription = supabase
+4
View File
@@ -349,6 +349,10 @@ li + li {
font-size: 0.9rem;
}
.alert--warning .admonition-icon svg {
fill: var(--custom-color-orange-600);
}
.navbar__brand strong {
font-family: var(--ifm-heading-font-family);
font-size: 1.5rem;
Binary file not shown.

After

Width:  |  Height:  |  Size: 239 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 393 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 965 KiB

After

Width:  |  Height:  |  Size: 530 KiB