mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 17:35:10 +03:00
Merge branch 'master' into docs/examples_reference
This commit is contained in:
86 files changed
+19665
-17750
No files matched your search
@@ -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 />
|
||||
|
||||
@@ -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".
|
||||
```
|
||||
|
||||
@@ -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".
|
||||
```
|
||||
|
||||
@@ -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".
|
||||
```
|
||||
|
||||
@@ -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".
|
||||
```
|
||||
|
||||
@@ -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".
|
||||
```
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,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',
|
||||
|
||||
@@ -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
@@ -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
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
+41
@@ -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
|
||||
+49
@@ -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
|
||||
+71
@@ -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>
|
||||
|
||||
@@ -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
|
||||
@@ -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,5 +1,6 @@
|
||||
export * from './misc/useStore'
|
||||
export * from './misc/withAuth'
|
||||
export * from './misc/useNotifications'
|
||||
export * from './deprecated'
|
||||
export * from './queries'
|
||||
export * from './ui'
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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>
|
||||
)
|
||||
}
|
||||
)
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||

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

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

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

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

|
||||
|
||||
- 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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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".
|
||||
```
|
||||
|
||||
@@ -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".
|
||||
```
|
||||
|
||||
@@ -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".
|
||||
```
|
||||
|
||||
@@ -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".
|
||||
```
|
||||
|
||||
@@ -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".
|
||||
```
|
||||
|
||||
@@ -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".
|
||||
```
|
||||
|
||||
@@ -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".
|
||||
```
|
||||
|
||||
@@ -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".
|
||||
```
|
||||
|
||||
@@ -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".
|
||||
```
|
||||
|
||||
@@ -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)
|
||||
}}
|
||||
/>
|
||||
{/* ... */}
|
||||
|
||||
@@ -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".
|
||||
```
|
||||
|
||||
@@ -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".
|
||||
```
|
||||
|
||||
Generated
+272
-152
File diff suppressed because it is too large.
Load diff
+1
-1
@@ -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
File diff suppressed because it is too large.
Load diff
+2633
-2328
File diff suppressed because it is too large.
Load diff
+792
-826
File diff suppressed because it is too large.
Load diff
+5781
-5341
File diff suppressed because it is too large.
Load diff
@@ -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
|
||||
|
||||
@@ -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 |
Reference in new issue
Block a user