Merge branch 'master' into feat/github-integration

This commit is contained in:
Alaister Young committed 2023-07-31 16:55:50 +10:00
commit 9fbc18ca2f
112 files changed
+3348 -1573

No files matched your search

@@ -357,6 +357,11 @@ export const SocialLoginItems = [
icon: '/docs/img/icons/discord-icon',
url: '/guides/auth/social-login/auth-discord',
},
{
name: 'Figma',
icon: '/docs/img/icons/figma-icon',
url: '/guides/auth/social-login/auth-figma',
},
{
name: 'Kakao',
icon: '/docs/img/icons/kakao-icon',
+10
View File
@@ -49,6 +49,16 @@ const authProviders = [
selfHosted: true,
authType: 'social',
},
{
name: 'Figma',
logo: '/docs/img/icons/figma-icon',
href: '/guides/auth/social-login/auth-figma',
official: true,
supporter: 'Supabase',
platform: true,
selfHosted: true,
authType: 'social',
},
{
name: 'GitHub',
logo: '/docs/img/icons/github-icon',
@@ -4,29 +4,24 @@ export const TabPanel = Tabs.Panel
export const meta = {
id: 'ai-choosing-compute-addon',
title: 'Choosing Compute Add-on',
description: 'Choosing the right Compute Add-on for your workload.',
subtitle: 'Choosing the right Compute Add-on for your workload.',
title: 'Choosing your Compute Add-on',
description: 'Choosing the right Compute Add-on for your vector workload.',
subtitle: 'Choosing the right Compute Add-on for your vector workload.',
sidebar_label: 'Choosing Compute Add-on',
}
This guide will help you choose the right Compute Add-on for your workload. We'll provide general guidance, as it is impossible to provide specific instructions for every possible use case. The goal is to give you a starting point from which you can make your own benchmarks and optimizations.
You have two options for scaling your vector workload:
Note that it is only useful for index searches, not for sequential scans. Sequential scans will result to significantly higher latencies and lower throughput, but will guarantee 100% precision and will not be RAM bound. Therefore it is possible to use a smaller plan for sequential scans.
1. Increase the size of your database. This guide will help you choose the right size for your workload.
2. Spread your workload across multiple databases. You can find more details about this approach in [Engineering for Scale](engineering-for-scale).
For more information about engineering at scale, see our [Engineering for Scale](/docs/guides/ai/engineering-for-scale) guide.
## Dimensionality
## Simple workloads
The number of dimensions in your embeddings is the most important factor in choosing the right Compute Add-on. In general, the lower the dimensionality the better the performance. We've provided guidance for some of the more common embedding dimensions below. For each benchmark, we used [Vecs](https://github.com/supabase/vecs) to create a collection, upload the embeddings to a single table, and create an `inner-product` index for the embedding column. We then ran a series of queries to measure the performance of different compute add-ons:
We've run a set of benchmarks using
### 1536 Dimensions
- The [dbpedia-entities-openai-1M](https://huggingface.co/datasets/KShivendu/dbpedia-entities-openai-1M) dataset. This dataset contains 1,000,000 embeddings for text, with each embedding being 1536 dimensions made using OpenAI API.
- The [gist-960-angular](http://corpus-texmex.irisa.fr/) dataset. This dataset contains 1,000,000 embeddings for images, with each embedding being 960 dimensions.
- The [GloVe Reddit comments](https://nlp.stanford.edu/projects/glove/) dataset, which contains 1,623,397 embeddings for words, with each embedding being 512 dimensions.
We used [Vecs](https://github.com/supabase/vecs) to create a collection, upload the embeddings to a single table, and create an `inner-product` index for the embedding column. We then ran a series of queries to measure the performance of different compute add-ons:
### Results
This benchmark uses the [dbpedia-entities-openai-1M](https://huggingface.co/datasets/KShivendu/dbpedia-entities-openai-1M) dataset, which contains 1,000,000 embeddings of text. Each embedding is 1536 dimensions created with the [OpenAI Embeddings API](https://platform.openai.com/docs/guides/embeddings).
<Tabs
scrollable
@@ -36,8 +31,6 @@ We used [Vecs](https://github.com/supabase/vecs) to create a collection, upload
>
<TabPanel id="dbpedia1536" label="OpenAI-1536, probes = 10">
Emdeddings of 1536 dimensions by OpenAI for dbpedia dataset with 1,000,000 vectors.
| Plan | Vectors | Lists | RPS | Latency Mean | Latency p95 | RAM Usage | RAM |
| ------ | --------- | ----- | ---- | ------------ | ----------- | ------------------ | ------ |
| Free | 20,000 | 40 | 135 | 0.372 sec | 0.412 sec | 1 GB + 200 Mb Swap | 1 GB |
@@ -56,8 +49,6 @@ For 1,000,000 vectors 10 probes results to precision of 0.91. And for 500,000 ve
</TabPanel>
<TabPanel id="dbpedia1536_40" label="OpenAI-1536, probes = 40">
Emdeddings of 1536 dimensions by OpenAI for dbpedia dataset with 1,000,000 vectors.
| Plan | Vectors | Lists | RPS | Latency Mean | Latency p95 | RAM Usage | RAM |
| ------ | --------- | ----- | --- | ------------ | ----------- | --------- | ------ |
| Free | 20,000 | 40 | - | - | - | - | 1 GB |
@@ -74,9 +65,32 @@ Emdeddings of 1536 dimensions by OpenAI for dbpedia dataset with 1,000,000 vecto
For 1,000,000 vectors 40 probes results to precision of 0.98. Note that exact values may vary depending on the dataset and queries, we recommend to run benchmarks with your own data to get precise results. Use this table as a reference.
</TabPanel>
<TabPanel id="gist960" label="gist-960, probes = 10">
</Tabs>
Emdeddings of 960 dimensions from gist-960 dataset with 1,000,000 vectors.
<div>
<img
alt="multi database"
className="dark:hidden"
src="/docs/img/ai/going-prod/size-to-rps--light.png"
/>
<img
alt="multi database"
className="hidden dark:block"
src="/docs/img/ai/going-prod/size-to-rps--dark.png"
/>
</div>
### 960 Dimensions
This benchmark uses the [gist-960-angular](http://corpus-texmex.irisa.fr/) dataset, which contains 1,000,000 embeddings of images. Each embedding is 960 dimensions.
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="gist960"
>
<TabPanel id="gist960" label="gist-960, probes = 10">
| Plan | Vectors | Lists | RPS | Latency Mean | Latency p95 | RAM Usage | RAM |
| ------ | --------- | ----- | ---- | ------------ | ----------- | ------------------ | ------ |
@@ -92,9 +106,19 @@ Emdeddings of 960 dimensions from gist-960 dataset with 1,000,000 vectors.
| 16XL | 1,000,000 | 1000 | 1345 | 0.072 sec | 0.106 sec | 17.5 GB | 256 GB |
</TabPanel>
<TabPanel id="glove512" label="GloVe-512, probes = 10">
</Tabs>
Emdeddings of 512 dimensions from GloVe Reddit comments dataset with 1,623,397 vectors.
### 512 Dimensions
This benchmark uses the [GloVe Reddit comments](https://nlp.stanford.edu/projects/glove/) dataset, which contains 1,623,397 embeddings of text. Each embedding is 512 dimensions. Random vectors were generated for queries.
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="glove512"
>
<TabPanel id="glove512" label="GloVe-512, probes = 10">
| Plan | Vectors | Lists | RPS | Latency Mean | Latency p95 | RAM Usage | RAM |
| ------ | --------- | ----- | ---- | ------------ | ----------- | ------------------ | ------ |
@@ -109,13 +133,9 @@ Emdeddings of 512 dimensions from GloVe Reddit comments dataset with 1,623,397 v
| 12XL | 1,623,397 | 1275 | 3700 | 0.020 sec | 0.036 sec | 26 GB | 192 GB |
| 16XL | 1,623,397 | 1275 | 3700 | 0.025 sec | 0.042 sec | 29 GB | 256 GB |
Random vectors were generated for queries.
</TabPanel>
<TabPanel id="glove512_60" label="GloVe-512, probes = 60">
Emdeddings of 512 dimensions from GloVe Reddit comments dataset with 1,623,397 vectors.
| Plan | Vectors | Lists | RPS | Latency Mean | Latency p95 | RAM Usage | RAM |
| ------ | --------- | ----- | --- | ------------ | ----------- | --------- | ------ |
| Free | 100,000 | 100 | - | - | - | - | 1 GB |
@@ -130,8 +150,6 @@ Emdeddings of 512 dimensions from GloVe Reddit comments dataset with 1,623,397 v
| 12XL | 1,623,397 | 1275 | 840 | 0.093 sec | 0.124 sec | 26 GB | 192 GB |
| 16XL | 1,623,397 | 1275 | 940 | 0.084 sec | 0.108 sec | 29 GB | 256 GB |
Random vectors were generated for queries.
</TabPanel>
</Tabs>
@@ -141,7 +159,34 @@ It is possible to upload more vectors to a single table if Memory allows it (for
</Admonition>
## Methodology
## Performance tips
There are various ways to improve your pgvector performance. Here are some tips:
### Pre-warming your database
It's useful to execute a few thousand “warm-up” queries before going into production. This helps help with RAM utilization. This can also help to determine that you've selected the right instance size for your workload.
### Increase the number of lists
You can increase the Requests per Second by increasing the number of `lists`. This also has an important caveat: building the index takes longer with more lists.
<div>
<img
alt="multi database"
className="dark:hidden"
src="/docs/img/ai/instance-type/lists-for-1m--light.png"
/>
<img
alt="multi database"
className="hidden dark:block"
src="/docs/img/ai/instance-type/lists-for-1m--dark.png"
/>
</div>
Check out more tips and the complete step-by-step guide in [Going to Production for AI applications](going-to-prod).
## Benchmark Methodology
We follow techniques outlined in the [ANN Benchmarks](https://github.com/erikbern/ann-benchmarks) methodology. A Python test runner is responsible for uploading the data, creating the index, and running the queries. The pgvector engine is implemented using [vecs](https://github.com/supabase/vecs), a Python client for pgvector.
@@ -153,7 +153,7 @@ And after ChatGPT receives a response from the plugin it will answer your questi
## Resources
- ChatGPT Retrieval Plugin: [github.com/openai/chatgpt-retrieval-plugin](https://github.com/openai/chatgpt-retrieval-plugin)
- ChatGTP Plugins: [official documentation](https://platform.openai.com/docs/plugins/introduction)
- ChatGPT Plugins: [official documentation](https://platform.openai.com/docs/plugins/introduction)
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
@@ -848,6 +848,7 @@ See [refreshing session example](/docs/guides/auth/auth-helpers/nextjs#managing-
## More examples
- [Build a Twitter Clone with the Next.js App Router and Supabase - free egghead course](https://egghead.io/courses/build-a-twitter-clone-with-the-next-js-app-router-and-supabase-19bebadb)
- [Cookie-based Auth and the Next.js 13 App Router (free course)](https://youtube.com/playlist?list=PL5S4mPUpp4OtMhpnp93EFSo42iQ40XjbF)
- [Full App Router example](https://github.com/supabase/supabase/tree/master/examples/auth/nextjs)
- [Realtime Subscriptions](https://github.com/supabase/supabase/tree/master/examples/auth/nextjs/app/realtime-posts.tsx)
@@ -0,0 +1,77 @@
import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
id: 'auth-figma',
title: 'Login with Figma',
description: 'Add Figma OAuth to your Supabase project',
}
To enable Figma Auth for your project, you need to set up a Figma OAuth application and add the application credentials to your Supabase Dashboard.
## Overview
Setting up Figma logins for your application consists of 3 parts:
- Create and configure a Figma App on the [Figma Developers page](https://www.figma.com/developers).
- Add your Figma `client_id` and `client_secret` to your [Supabase Project](https://app.supabase.com).
- Add the login code to your [Supabase JS Client App](https://github.com/supabase/supabase-js).
## Access the Figma Developers page
- Go to the [Figma Developers page](https://www.figma.com/developers)
- Click on `My apps` at the top right
- Log in (if necessary)
![Figma Developers page](/docs/img/guides/auth-figma/figma_developers_page.png)
## Find your callback URL
<SocialProviderSetup provider="Figma" />
## Create a Figma OAuth app
- Enter your `App name`, `Website URL` and upload your app logo
- Click on `Add callback`
- Add your `Callback URL`
- Click on `Save`
![Create Figma app](/docs/img/guides/auth-figma/figma_create_app.png)
- Copy and save your newly-generated `Client ID`
- Copy and save your newly-generated `Client Secret`
![Get Figma app credentials](/docs/img/guides/auth-figma/figma_app_credentials.png)
## Enter your Figma credentials into your Supabase Project
<SocialProviderSettingsSupabase provider="Figma" />
## Add login code to your client app
When your user signs in, call [signInWithOAuth()](/docs/reference/javascript/auth-signinwithoauth) with `figma` as the `provider`:
```js
async function signInWithFigma() {
const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'figma',
})
}
```
When your user signs out, call [signOut()](/docs/reference/javascript/auth-signout) to remove them from the browser session and any objects from localStorage:
```js
async function signout() {
const { error } = await supabase.auth.signOut()
}
```
## Resources
- [Supabase Account - Free Plan OK](https://supabase.com)
- [Supabase JS Client](https://github.com/supabase/supabase-js)
- [Figma Developers page](https://www.figma.com/developers)
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
@@ -979,6 +979,7 @@ At this stage you have a fully functional application!
## See also
- See the complete [example on GitHub](https://github.com/supabase/supabase/tree/master/examples/user-management/nextjs-user-management) and deploy it to Vercel
- [Build a Twitter Clone with the Next.js App Router and Supabase - free egghead course](https://egghead.io/courses/build-a-twitter-clone-with-the-next-js-app-router-and-supabase-19bebadb)
- Explore the [pre-built Auth UI for React](/docs/guides/auth/auth-helpers/auth-ui)
- Explore the [Auth Helpers for Next.js](/docs/guides/auth/auth-helpers/nextjs)
- Explore the [Supabase Cache Helpers](https://github.com/psteinroe/supabase-cache-helpers)
@@ -13,6 +13,18 @@ We have a [set of examples](https://github.com/supabase/supabase/tree/master/exa
## Featured
## Build a Twitter Clone with the Next.js App Router and Supabase
By [Jon Meyers](https://twitter.com/jonmeyers_io)
<a href="https://egghead.io/courses/build-a-twitter-clone-with-the-next-js-app-router-and-supabase-19bebadb">
<img
src="https://pbs.twimg.com/media/F2DEsxTaYAA1Coe?format=jpg&name=medium"
alt="Build a Twitter Clone with the Next.js App Router and Supabase"
width="100%"
/>
</a>
### Supabase Crash Course
By [Traversy Media](https://www.youtube.com/watch?v=7uKQBl9uZ00).
@@ -80,6 +92,7 @@ Build a basic Todo List with Supabase and your favorite frontend framework:
### Courses
- [Build a Twitter Clone with the Next.js App Router and Supabase - free egghead course](https://egghead.io/courses/build-a-twitter-clone-with-the-next-js-app-router-and-supabase-19bebadb)
- Build a FullStack App with Next.js, Supabase & Prisma by [@gdangel0](https://twitter.com/gdangel0): [Free course](https://themodern.dev/courses/build-a-fullstack-app-with-nextjs-supabase-and-prisma-322389284337222224)
### Libraries
Binary file not shown.

After

Width:  |  Height:  |  Size: 256 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

@@ -0,0 +1 @@
<svg width="300" height="300" viewBox="-50 0 300 300" xmlns="http://www.w3.org/2000/svg"><style>.st0{fill:#0acf83}.st1{fill:#a259ff}.st2{fill:#f24e1e}.st3{fill:#ff7262}.st4{fill:#1abcfe}</style><path id="path0_fill" class="st0" d="M50 300c27.6 0 50-22.4 50-50v-50H50c-27.6 0-50 22.4-50 50s22.4 50 50 50z"/><path id="path1_fill" class="st1" d="M0 150c0-27.6 22.4-50 50-50h50v100H50c-27.6 0-50-22.4-50-50z"/><path id="path1_fill_1_" class="st2" d="M0 50C0 22.4 22.4 0 50 0h50v100H50C22.4 100 0 77.6 0 50z"/><path id="path2_fill" class="st3" d="M100 0h50c27.6 0 50 22.4 50 50s-22.4 50-50 50h-50V0z"/><path id="path3_fill" class="st4" d="M200 150c0 27.6-22.4 50-50 50s-50-22.4-50-50 22.4-50 50-50 50 22.4 50 50z"/></svg>

After

Width:  |  Height:  |  Size: 717 B

@@ -0,0 +1,80 @@
import { useEffect, useState } from 'react'
import _announcement from './data/Announcement.json'
import { IconX } from 'ui'
import { useRouter } from 'next/router'
import { PropsWithChildren } from 'react'
import Link from 'next/link'
export interface AnnouncementProps {
show: boolean
text: string
launchDate: string
link: string
badge?: string
}
const announcement = _announcement as AnnouncementProps
interface AnnouncementComponentProps {
show?: boolean
className?: string
link?: string
}
const Announcement = ({
show = true,
className,
children,
link,
}: PropsWithChildren<AnnouncementComponentProps>) => {
const [hidden, setHidden] = useState(true)
const router = useRouter()
const isLaunchWeekSection = router.pathname.includes('launch-week')
// override to hide announcement
if (!show || !announcement.show) return null
// construct the key for the announcement, based on the title text
const announcementKey = 'announcement_' + announcement.text.replace(/ /g, '')
// window.localStorage is kept inside useEffect
// to prevent error
useEffect(function () {
if (!window.localStorage.getItem(announcementKey)) {
return setHidden(false)
}
}, [])
function handleClose(event: any) {
event.stopPropagation()
window.localStorage.setItem(announcementKey, 'hidden')
return setHidden(true)
}
// Always show if on LW section
if (!isLaunchWeekSection && hidden) {
return null
} else {
return (
<div className={['relative z-40 w-full cursor-pointer', className].join(' ')}>
{!isLaunchWeekSection && (
<div
className="absolute z-50 right-4 flex h-full items-center opacity-100 text-white transition-opacity hover:opacity-90"
onClick={handleClose}
>
<IconX size={16} />
</div>
)}
{children}
<Link href={link ?? announcement.link}>
<a className="absolute inset-0 z-40" />
</Link>
</div>
)
}
}
export default Announcement
@@ -0,0 +1,7 @@
{
"show": true,
"text": "Supabase Launch Week 8",
"launchDate": "2023-08-07T09:00:00.000-07:00",
"link": "https://supabase.com/launch-week",
"badge": "Get your ticket"
}
+26 -19
View File
@@ -3,21 +3,17 @@ import { useRouter } from 'next/router'
import Telemetry, { TelemetryEvent } from '~/lib/telemetry'
import { useTelemetryProps } from 'common/hooks/useTelemetryProps'
import gaEvents from '~/lib/gaEvents'
import { Badge, Button, IconArrowRight, IconBookOpen } from 'ui'
import { Button, IconArrowRight, IconBookOpen } from 'ui'
import SectionContainer from '~/components/Layouts/SectionContainer'
import HeroFrameworks from './HeroFrameworks'
import styles from './hero.module.css'
import Image from 'next/image'
import { range } from 'lodash'
import ParticlesCanvas from '../LaunchWeek/8/AnimatedParticles/ParticlesCanvas'
import { useBreakpoint } from 'common'
import { useEffect, useState } from 'react'
const Hero = () => {
const router = useRouter()
const [showAnnouncement, setShowAnnouncement] = useState(false)
const telemetryProps = useTelemetryProps()
const isMobile = useBreakpoint()
const sendTelemetryEvent = async (event: TelemetryEvent) => {
await Telemetry.sendEvent(event, telemetryProps, router)
}
@@ -46,9 +42,9 @@ const Hero = () => {
<Link href="/launch-week">
<a className="group w-auto rounded-full p-1 mb-6 mt-8 md:mb-10 bg-gradient-to-b from-[#1b1f2124] to-[#02040550] hover:from-[#ffffff15] hover:to-[#ffffff05] transition-all backdrop-blur-lg border flex items-center justify-between gap-2 md:gap-4 text-scale-1100 hover:text-scale-1200 text-sm">
<div className="text-scale-200 rounded-full bg-brand-900 px-3 py-1 flex items-center justify-center">
Get you ticket
Get your ticket
</div>
<span>Launch Week 8 - August 7th</span>
<span>Launch Week 8 - August 7-11</span>
<IconArrowRight
size={16}
className="transform mr-2 transition-transform translate-x-0 sm:-translate-x-1 group-hover:translate-x-0"
@@ -97,23 +93,34 @@ const Hero = () => {
</div>
</div>
</div>
<div className="absolute -top-2 md:top-0 -left-10 -right-10 md:left-0 md:right-0 h-[500px] lg:h-[550px] z-0">
<ParticlesCanvas users={range(0, 60)} />
<Image
src="/images/launchweek/8/stars.svg"
alt="starts background"
layout="fill"
objectFit="cover"
className="opacity-70"
draggable={false}
/>
<div className="absolute -top-2 md:top-0 -left-10 -right-10 md:left-0 md:right-0 h-[500px] lg:h-[550px] z-0 flex items-center justify-center">
<div className="relative w-full aspect-[1.65/1] mb-14 max-w-sm md:max-w-md opacity-0 !animate-[fadeIn_0.5s_cubic-bezier(0.25,0.25,0,1)_0.5s_both]">
<Image
src="/images/launchweek/8/lw8-visual.png"
alt="launch week 8 shape"
layout="fill"
objectFit="contain"
quality={100}
draggable={false}
/>
</div>
<div className="absolute inset-0">
<Image
src="/images/launchweek/8/stars.svg"
alt="stars background"
layout="fill"
objectFit="cover"
className="opacity-70"
draggable={false}
/>
</div>
</div>
</SectionContainer>
<div className="absolute w-full aspect-[1/1] md:aspect-[1.5/1] lg:aspect-[2.5/1] inset-0 z-0">
<div className="absolute w-full max-w-[1600px] mx-auto h-[500px] lg:h-[750px] inset-0 z-0">
<Image
src="/images/launchweek/8/LW8-gradient.png"
layout="fill"
objectFit={isMobile ? 'cover' : 'contain'}
objectFit="cover"
objectPosition="top"
priority
draggable={false}
@@ -19,7 +19,7 @@ const ParticlesCanvas = ({ supabase, users }: { supabase?: SupabaseClient; users
> | null>(null)
const loadUsers = async () => {
return await supabase!.from('lw8_tickets_golden').select('id, golden', { count: 'exact' })
return await supabase!.from('lw8_tickets_golden').select('username, golden', { count: 'exact' })
}
// Update particles live when new tickets are generated
@@ -119,7 +119,7 @@ const ParticlesCanvas = ({ supabase, users }: { supabase?: SupabaseClient; users
config={config}
animate={animate}
>
{(!!user.id && user.golden) || (config.showGold && Math.random() <= 0.5) ? (
{config.showGold && Math.random() <= 0.5 ? (
<>
<GoldGeometry />
<GoldMaterial />
+3 -4
View File
@@ -2,7 +2,8 @@ import React, { useState } from 'react'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { Button, Badge, IconStar, IconChevronDown, Announcement, LW8CountdownBanner } from 'ui'
import Announcement from '~/components/Announcement/Announcement'
import { Button, Badge, IconStar, IconChevronDown, LW8CountdownBanner } from 'ui'
import FlyOut from '~/components/UI/FlyOut'
import Transition from 'lib/Transition'
@@ -19,8 +20,6 @@ import Image from 'next/image'
import * as supabaseLogoWordmarkDark from 'common/assets/images/supabase-logo-wordmark--dark.png'
import * as supabaseLogoWordmarkLight from 'common/assets/images/supabase-logo-wordmark--light.png'
import * as supabaseLogoWordmarkWhite from 'common/assets/images/supabase-logo-wordmark--white.png'
const Nav = () => {
const { isDarkMode } = useTheme()
const router = useRouter()
@@ -199,7 +198,7 @@ const Nav = () => {
return (
<>
<Announcement>
<Announcement link="/launch-week">
<LW8CountdownBanner />
</Announcement>
<div className="sticky top-0 z-40 transform" style={{ transform: 'translate3d(0,0,999px)' }}>
-1
View File
@@ -19,7 +19,6 @@
"@mdx-js/react": "^2.1.5",
"@next/bundle-analyzer": "^13.4.0",
"@next/mdx": "^12.3.2",
"@react-three/drei": "^8.20.2",
"@react-three/fiber": "^7.0.29",
"@supabase/auth-ui-react": "^0.1.6",
"@supabase/supabase-js": "^2.23.0",
+1 -1
View File
@@ -49,7 +49,7 @@ export async function getStaticProps() {
},
{
login: 'lyqht',
avatar_url: 'https://pbs.twimg.com/profile_images/1589662526941253632/s1cu3vuD_400x400.jpg',
avatar_url: 'https://pbs.twimg.com/profile_images/1665778877837504514/4SWgLpjA_400x400.png',
html_url: 'https://twitter.com/estee_tey',
},
{
+3 -2
View File
@@ -1,5 +1,5 @@
import dynamic from 'next/dynamic'
import { useEffect } from 'react'
import { useEffect, useState } from 'react'
import { getSortedPosts } from '~/lib/posts'
import PostTypes from '~/types/post'
import Layout from '~/components/Layouts/Default'
@@ -22,13 +22,14 @@ type Props = { customerStories: PostTypes[] }
const Index = ({ customerStories }: Props) => {
const { isDarkMode, toggleTheme } = useTheme()
const [initialDarkMode] = useState(isDarkMode)
useEffect(() => {
toggleTheme(true)
document.body.className = 'dark'
return () => {
document.body.className = ''
isDarkMode ? toggleTheme(true) : toggleTheme(false)
toggleTheme(initialDarkMode)
}
}, [])
+2 -1
View File
@@ -51,6 +51,7 @@ export default function TicketHome({ users }: Props) {
const [supabase, setSupabase] = useState<SupabaseClient | null>(null)
const [session, setSession] = useState<Session | null>(null)
const { isDarkMode, toggleTheme } = useTheme()
const [initialDarkMode] = useState(isDarkMode)
const defaultUserData = {
id: query.id?.toString(),
@@ -93,7 +94,7 @@ export default function TicketHome({ users }: Props) {
document.body.className = 'dark bg-[#020405]'
return () => {
document.body.className = ''
isDarkMode ? toggleTheme(true) : toggleTheme(false)
toggleTheme(initialDarkMode)
}
}, [])
@@ -44,6 +44,7 @@ export default function UsernamePage({ user, users, ogImageUrl }: Props) {
const [supabase, setSupabase] = useState<SupabaseClient | null>(null)
const [session, setSession] = useState<Session | null>(null)
const { isDarkMode, toggleTheme } = useTheme()
const [initialDarkMode] = useState(isDarkMode)
const [pageState, setPageState] = useState<PageState>('ticket')
@@ -67,7 +68,7 @@ export default function UsernamePage({ user, users, ogImageUrl }: Props) {
document.body.className = 'dark bg-[#020405]'
return () => {
document.body.className = ''
isDarkMode ? toggleTheme(true) : toggleTheme(false)
toggleTheme(initialDarkMode)
}
}, [])
+2 -1
View File
@@ -45,6 +45,7 @@ export default function TicketsPage({ users }: Props) {
const OG_IMAGE = `${SITE_ORIGIN}/images/launchweek/8/lw8-og.jpg`
const { isDarkMode, toggleTheme } = useTheme()
const [initialDarkMode] = useState(isDarkMode)
const [isLoading, setIsLoading] = useState(false)
const [offset, setOffset] = useState(1)
const [isLast, setIsLast] = useState(false)
@@ -97,7 +98,7 @@ export default function TicketsPage({ users }: Props) {
document.body.className = 'dark bg-[#020405]'
return () => {
document.body.className = ''
isDarkMode ? toggleTheme(true) : toggleTheme(false)
toggleTheme(initialDarkMode)
}
}, [])
Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

@@ -0,0 +1,19 @@
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>'Postgres | Supabase Blog</title>
<link>https://supabase.com/blog</link>
<description>Latest Postgres news from Egor Romanov at Supabase</description>
<language>en</language>
<lastBuildDate>Wed, 12 Jul 2023 22:00:00 GMT</lastBuildDate>
<atom:link href="https://supabase.com/planetpg-egor_romanov-rss.xml" rel="self" type="application/rss+xml"/>
<item>
<guid>https://supabase.com/blog/pgvector-performance</guid>
<title>pgvector 0.4.0 performance</title>
<link>https://supabase.com/blog/pgvector-performance</link>
<description>There&apos;s been lot of talk about pgvector performance lately, so we took some datasets and pushed pgvector to the limits to find out its strengths and limitations.</description>
<pubDate>Wed, 12 Jul 2023 22:00:00 GMT</pubDate>
</item>
</channel>
</rss>
+8 -1
View File
@@ -5,9 +5,16 @@
<link>https://supabase.com/blog</link>
<description>Latest Postgres news from Pavel Borisov at Supabase</description>
<language>en</language>
<lastBuildDate>Thu, 15 Dec 2022 23:00:00 GMT</lastBuildDate>
<lastBuildDate>Wed, 12 Jul 2023 22:00:00 GMT</lastBuildDate>
<atom:link href="https://supabase.com/planetpg-pavel-rss.xml" rel="self" type="application/rss+xml"/>
<item>
<guid>https://supabase.com/blog/pgvector-performance</guid>
<title>pgvector 0.4.0 performance</title>
<link>https://supabase.com/blog/pgvector-performance</link>
<description>There&apos;s been lot of talk about pgvector performance lately, so we took some datasets and pushed pgvector to the limits to find out its strengths and limitations.</description>
<pubDate>Wed, 12 Jul 2023 22:00:00 GMT</pubDate>
</item>
<item>
<guid>https://supabase.com/blog/new-in-postgres-15</guid>
<title>What&apos;s new in Postgres 15?</title>
<link>https://supabase.com/blog/new-in-postgres-15</link>
+18 -4
View File
@@ -5,9 +5,23 @@
<link>https://supabase.com</link>
<description>Latest news from Supabase</description>
<language>en</language>
<lastBuildDate>Tue, 11 Jul 2023 22:00:00 GMT</lastBuildDate>
<lastBuildDate>Mon, 17 Jul 2023 22:00:00 GMT</lastBuildDate>
<atom:link href="https://supabase.com/rss.xml" rel="self" type="application/rss+xml"/>
<item>
<guid>https://supabase.com/blog/flutter-authentication</guid>
<title>Getting started with Flutter authentication</title>
<link>https://supabase.com/blog/flutter-authentication</link>
<description>Learn how authentication on Flutter works through Google sign in with Supabase auth.</description>
<pubDate>Mon, 17 Jul 2023 22:00:00 GMT</pubDate>
</item>
<item>
<guid>https://supabase.com/blog/pgvector-performance</guid>
<title>pgvector 0.4.0 performance</title>
<link>https://supabase.com/blog/pgvector-performance</link>
<description>There&apos;s been lot of talk about pgvector performance lately, so we took some datasets and pushed pgvector to the limits to find out its strengths and limitations.</description>
<pubDate>Wed, 12 Jul 2023 22:00:00 GMT</pubDate>
</item>
<item>
<guid>https://supabase.com/blog/postgrest-11-1-release</guid>
<title>What is new in PostgREST v11.1?</title>
<link>https://supabase.com/blog/postgrest-11-1-release</link>
@@ -400,9 +414,9 @@
<pubDate>Wed, 23 Nov 2022 23:00:00 GMT</pubDate>
</item>
<item>
<guid>https://supabase.com/blog/flutter-authentication-and-authorization-with-rls</guid>
<title>Flutter Authentication and Authorization with RLS</title>
<link>https://supabase.com/blog/flutter-authentication-and-authorization-with-rls</link>
<guid>https://supabase.com/blog/flutter-authorization-with-rls</guid>
<title>Flutter Authorization with RLS</title>
<link>https://supabase.com/blog/flutter-authorization-with-rls</link>
<description>Learn how you can secure your Flutter app using Supabase Row Level Security.</description>
<pubDate>Mon, 21 Nov 2022 23:00:00 GMT</pubDate>
</item>
+153 -613
View File
File diff suppressed because it is too large. Load diff
+5 -3
View File
@@ -29,7 +29,7 @@ export const ThemeProvider = ({
detectSystemColorPreference = true,
children,
}: ThemeProviderProps) => {
const [isDarkMode, setIsDarkMode] = useState(false)
const [isDarkMode, setIsDarkMode] = useState(true)
useEffect(() => {
const key = localStorage.getItem('supabaseDarkMode')
@@ -37,11 +37,13 @@ export const ThemeProvider = ({
// and detectSystemColorPreference is false
// default to dark mode
const hasNoKey = key === null
const isDarkMode = hasNoKey || key === 'true'
const shouldBeDarkMode = hasNoKey || key === 'true'
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
hasNoKey && detectSystemColorPreference ? toggleTheme(prefersDark) : toggleTheme(isDarkMode)
hasNoKey && detectSystemColorPreference
? toggleTheme(prefersDark)
: toggleTheme(shouldBeDarkMode)
}, [])
const toggleTheme: UseThemeProps['toggleTheme'] = (darkMode) => {
+1 -1
View File
@@ -8,10 +8,10 @@
"@headlessui/react": "^1.7.14",
"@supabase/gotrue-js": "^2.46.1",
"@supabase/ui": "^0.37.0-alpha.50",
"@types/react": "^17.0.39",
"react-use": "^17.4.0"
},
"devDependencies": {
"@types/react": "17.0.39",
"@types/react-dom": "^17.0.11",
"config": "*",
"tsconfig": "*",
+7
View File
@@ -139,6 +139,13 @@ export {
PopoverContent as PopoverContent_Shadcn_,
} from './src/components/shadcn/ui/popover'
export {
Accordion as Accordion_Shadcn_,
AccordionItem as AccordionItem_Shadcn_,
AccordionTrigger as AccordionTrigger_Shadcn_,
AccordionContent as AccordionContent_Shadcn_,
} from './src/components/shadcn/ui/accordion'
// links
export * from './src/components/TextLink'
+1 -1
View File
@@ -54,7 +54,7 @@
"autoprefixer": "^10.4.14",
"class-variance-authority": "^0.6.0",
"clsx": "^1.2.1",
"cmdk-supabase": "^0.2.2",
"cmdk-supabase": "^1.0.7",
"color": "^4.2.3",
"colorjs.io": "^0.4.3",
"common-tags": "^1.8.2",
@@ -103,7 +103,7 @@ export function Item({ icon, className, ...props }: ItemProps) {
{...props}
>
{icon && icon}
<span>{props.children}</span>
<span className="w-full">{props.children}</span>
</RadixDropdown.Item>
)
}
@@ -16,8 +16,8 @@ AccordionItem.displayName = 'AccordionItem'
const AccordionTrigger = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger> & { hideIcon?: boolean }
>(({ className, children, hideIcon, ...props }, ref) => (
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger
ref={ref}
@@ -28,7 +28,7 @@ const AccordionTrigger = React.forwardRef<
{...props}
>
{children}
<ChevronDown className="h-4 w-4 transition-transform duration-200" />
{!hideIcon && <ChevronDown className="h-4 w-4 transition-transform duration-200" />}
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
))
@@ -70,8 +70,12 @@ CommandList.displayName = CommandPrimitive.List.displayName
const CommandEmpty = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Empty>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
>((props, ref) => (
<CommandPrimitive.Empty ref={ref} className="py-6 text-center text-sm" {...props} />
>(({ className, ...props }, ref) => (
<CommandPrimitive.Empty
ref={ref}
className={cn('py-6 text-center text-sm', className)}
{...props}
/>
))
CommandEmpty.displayName = CommandPrimitive.Empty.displayName
@@ -153,7 +153,7 @@ const DropdownMenuSeparator = React.forwardRef<
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator
ref={ref}
className={cn('-mx-1 my-1 h-px bg-muted', className)}
className={cn('-mx-1 my-1 h-px bg-border', className)}
{...props}
/>
))
@@ -1,31 +1,37 @@
'use client'
import * as React from 'react'
import * as PopoverPrimitive from '@radix-ui/react-popover'
import { cn } from '@ui/lib/utils'
const Popover = PopoverPrimitive.Root
const PopoverTrigger = PopoverPrimitive.Trigger
type PopoverContentProps = {
portal?: boolean
align?: 'center' | 'start' | 'end'
sideOffset?: number
} & React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
const PopoverContent = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = 'center', sideOffset = 4, ...props }, ref) => (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
'z-50 w-72 rounded-md border bg-overlay p-4 text-popover-foreground shadow-md outline-none animate-in data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
className
)}
{...props}
/>
</PopoverPrimitive.Portal>
))
PopoverContent.displayName = PopoverPrimitive.Content.displayName
PopoverContentProps
>(({ className, align = 'center', sideOffset = 4, portal = false, ...props }, ref) => {
const Portal = portal ? PopoverPrimitive.Portal : React.Fragment
return (
<Portal>
<PopoverPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
'z-50 w-72 rounded-md border bg-overlay p-4 text-popover-foreground shadow-md outline-none animate-in data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
className
)}
{...props}
/>
</Portal>
)
})
PopoverContent.displayName = 'PopoverContent'
export { Popover, PopoverTrigger, PopoverContent }
@@ -6,20 +6,13 @@ import _announcement from '../data/Announcement.json'
import CountdownWidget from '../../../components/CountdownWidget/CountdownWidget'
import Lw8BannerBg from './Lw8BannerBg'
interface CountdownButtonProps {
dest: string
}
const announcement = _announcement as AnnouncementProps
function CountdownButton({ children, dest }: PropsWithChildren<CountdownButtonProps>) {
function CountdownButton({ children }: PropsWithChildren<{}>) {
return (
<a
href={dest}
className="bg-white text-xs px-1.5 md:px-2.5 py-1 rounded-full text-black shadow-none hover:shadow-mg cursor-pointer"
>
<div className="bg-white text-xs px-1.5 md:px-2.5 py-1 rounded-full text-black shadow-none hover:shadow-mg cursor-pointer">
{children}
</a>
</div>
)
}
@@ -37,7 +30,7 @@ export function LW8CountdownBanner() {
<div className="w-full flex gap-3 md:gap-6 items-center justify-center">
<p>Supabase Launch Week 8</p>
<div>
<CountdownButton dest="https://supabase.com/launch-week">Live now</CountdownButton>
<CountdownButton>Live now</CountdownButton>
</div>
</div>
)
@@ -58,9 +51,7 @@ export function LW8CountdownBanner() {
</div>
{!isLaunchWeekPage && (
<div className="hidden md:block">
<CountdownButton dest="https://supabase.com/launch-week">
Get your ticket
</CountdownButton>
<CountdownButton>Get your ticket</CountdownButton>
</div>
)}
</div>
+463 -17
View File
@@ -1,7 +1,7 @@
clispec: '001'
info:
id: cli
version: 1.77.9
version: 1.82.2
title: Supabase CLI
language: sh
source: https://github.com/supabase/cli
@@ -1388,45 +1388,268 @@ commands:
default_value: ''
- id: supabase-inspect
title: supabase inspect
summary: Tools to inspect your Supabase Database
summary: Tools to inspect your Supabase project
tags:
- local-dev
links: []
subcommands:
- supabase-inspect-db:cache-hit
- supabase-inspect-db:index-usage
- supabase-inspect-db:replication-slots
- supabase-inspect-db
flags: []
- id: supabase-inspect-db:replication-slots
title: supabase inspect db:replication-slots
summary: Shows information about replication slots on the database
- id: supabase-inspect-db
title: supabase inspect db
summary: Tools to inspect your Supabase database
tags: []
links: []
usage: supabase inspect db:replication-slots
subcommands:
- supabase-inspect-db-bloat
- supabase-inspect-db-blocking
- supabase-inspect-db-cache-hit
- supabase-inspect-db-calls
- supabase-inspect-db-index-sizes
- supabase-inspect-db-index-usage
- supabase-inspect-db-locks
- supabase-inspect-db-long-running-queries
- supabase-inspect-db-outliers
- supabase-inspect-db-replication-slots
- supabase-inspect-db-role-connections
- supabase-inspect-db-seq-scans
- supabase-inspect-db-table-index-sizes
- supabase-inspect-db-table-record-counts
- supabase-inspect-db-table-sizes
- supabase-inspect-db-total-index-size
- supabase-inspect-db-total-table-sizes
- supabase-inspect-db-unused-indexes
- supabase-inspect-db-vacuum-stats
flags: []
- id: supabase-inspect-db-vacuum-stats
title: supabase inspect db vacuum-stats
summary: Show statistics related to vacuum operations per table
tags: []
links: []
usage: supabase inspect db vacuum-stats
subcommands: []
flags:
- id: db-url
name: --db-url <string>
description: connect using the specified database url
default_value: ''
- id: supabase-inspect-db:index-usage
title: supabase inspect db:index-usage
summary: Shows information about the efficiency of indexes
- id: supabase-inspect-db-unused-indexes
title: supabase inspect db unused-indexes
summary: Show indexes with low usage
tags: []
links: []
usage: supabase inspect db:index-usage
usage: supabase inspect db unused-indexes
subcommands: []
flags:
- id: db-url
name: --db-url <string>
description: connect using the specified database url
default_value: ''
- id: supabase-inspect-db:cache-hit
title: supabase inspect db:cache-hit
summary: Shows cache hit rates for tables and indices
- id: supabase-inspect-db-total-table-sizes
title: supabase inspect db total-table-sizes
summary: Show total table sizes, including table index sizes
tags: []
links: []
usage: supabase inspect db:cache-hit
usage: supabase inspect db total-table-sizes
subcommands: []
flags:
- id: db-url
name: --db-url <string>
description: connect using the specified database url
default_value: ''
- id: supabase-inspect-db-total-index-size
title: supabase inspect db total-index-size
summary: Show total size of all indexes
tags: []
links: []
usage: supabase inspect db total-index-size
subcommands: []
flags:
- id: db-url
name: --db-url <string>
description: connect using the specified database url
default_value: ''
- id: supabase-inspect-db-table-sizes
title: supabase inspect db table-sizes
summary: |
Show table sizes of individual tables without their index sizes
tags: []
links: []
usage: supabase inspect db table-sizes
subcommands: []
flags:
- id: db-url
name: --db-url <string>
description: connect using the specified database url
default_value: ''
- id: supabase-inspect-db-table-record-counts
title: supabase inspect db table-record-counts
summary: Show estimated number of rows per table
tags: []
links: []
usage: supabase inspect db table-record-counts
subcommands: []
flags:
- id: db-url
name: --db-url <string>
description: connect using the specified database url
default_value: ''
- id: supabase-inspect-db-table-index-sizes
title: supabase inspect db table-index-sizes
summary: Show index sizes of individual tables
tags: []
links: []
usage: supabase inspect db table-index-sizes
subcommands: []
flags:
- id: db-url
name: --db-url <string>
description: connect using the specified database url
default_value: ''
- id: supabase-inspect-db-seq-scans
title: supabase inspect db seq-scans
summary: Show number of sequential scans recorded against all tables
tags: []
links: []
usage: supabase inspect db seq-scans
subcommands: []
flags:
- id: db-url
name: --db-url <string>
description: connect using the specified database url
default_value: ''
- id: supabase-inspect-db-role-connections
title: supabase inspect db role-connections
summary: Show number of active connections for all database roles
tags: []
links: []
usage: supabase inspect db role-connections
subcommands: []
flags:
- id: db-url
name: --db-url <string>
description: connect using the specified database url
default_value: ''
- id: supabase-inspect-db-replication-slots
title: supabase inspect db replication-slots
summary: Show information about replication slots on the database
tags: []
links: []
usage: supabase inspect db replication-slots
subcommands: []
flags:
- id: db-url
name: --db-url <string>
description: connect using the specified database url
default_value: ''
- id: supabase-inspect-db-outliers
title: supabase inspect db outliers
summary: |
Show queries from pg_stat_statements ordered by total execution time
tags: []
links: []
usage: supabase inspect db outliers
subcommands: []
flags:
- id: db-url
name: --db-url <string>
description: connect using the specified database url
default_value: ''
- id: supabase-inspect-db-long-running-queries
title: supabase inspect db long-running-queries
summary: |
Show currently running queries running for longer than 5 minutes
tags: []
links: []
usage: supabase inspect db long-running-queries
subcommands: []
flags:
- id: db-url
name: --db-url <string>
description: connect using the specified database url
default_value: ''
- id: supabase-inspect-db-locks
title: supabase inspect db locks
summary: |
Show queries which have taken out an exclusive lock on a relation
tags: []
links: []
usage: supabase inspect db locks
subcommands: []
flags:
- id: db-url
name: --db-url <string>
description: connect using the specified database url
default_value: ''
- id: supabase-inspect-db-index-usage
title: supabase inspect db index-usage
summary: Show information about the efficiency of indexes
tags: []
links: []
usage: supabase inspect db index-usage
subcommands: []
flags:
- id: db-url
name: --db-url <string>
description: connect using the specified database url
default_value: ''
- id: supabase-inspect-db-index-sizes
title: supabase inspect db index-sizes
summary: Show index sizes of individual indexes
tags: []
links: []
usage: supabase inspect db index-sizes
subcommands: []
flags:
- id: db-url
name: --db-url <string>
description: connect using the specified database url
default_value: ''
- id: supabase-inspect-db-calls
title: supabase inspect db calls
summary: |
Show queries from pg_stat_statements ordered by total times called
tags: []
links: []
usage: supabase inspect db calls
subcommands: []
flags:
- id: db-url
name: --db-url <string>
description: connect using the specified database url
default_value: ''
- id: supabase-inspect-db-cache-hit
title: supabase inspect db cache-hit
summary: Show cache hit rates for tables and indices
tags: []
links: []
usage: supabase inspect db cache-hit
subcommands: []
flags:
- id: db-url
name: --db-url <string>
description: connect using the specified database url
default_value: ''
- id: supabase-inspect-db-blocking
title: supabase inspect db blocking
summary: |
Show queries that are holding locks and the queries that are waiting for them to be released
tags: []
links: []
usage: supabase inspect db blocking
subcommands: []
flags:
- id: db-url
name: --db-url <string>
description: connect using the specified database url
default_value: ''
- id: supabase-inspect-db-bloat
title: supabase inspect db bloat
summary: |
Estimates space allocated to a relation that is full of dead tuples
tags: []
links: []
usage: supabase inspect db bloat
subcommands: []
flags:
- id: db-url
@@ -1908,6 +2131,14 @@ commands:
description: |
Print the migrations that would be applied, but don't actually apply them.
default_value: 'false'
- id: include-roles
name: --include-roles
description: Include custom roles from supabase/roles.sql.
default_value: 'false'
- id: include-seed
name: --include-seed
description: Include seed data from supabase/seed.sql.
default_value: 'false'
- id: password
name: -p, --password <string>
description: Password to your remote Postgres database.
@@ -2021,6 +2252,10 @@ commands:
name: --data-only
description: Dumps only data records.
default_value: 'false'
- id: dry-run
name: --dry-run
description: Print the pg_dump script that would be executed.
default_value: 'false'
- id: file
name: -f, --file <string>
description: File path to save the dumped contents.
@@ -2037,6 +2272,10 @@ commands:
name: --role-only
description: Dumps only cluster roles.
default_value: 'false'
- id: schema
name: -s, --schema <strings>
description: List of schema to include.
default_value: all
- id: use-copy
name: --use-copy
description: Uses copy statements in place of inserts.
@@ -2258,3 +2497,210 @@ commands:
name: --no-descriptions
description: disable completion descriptions
default_value: 'false'
- id: supabase-branches
title: supabase branches
summary: Manage Supabase preview branches
tags:
- management-api
links: []
subcommands:
- supabase-branches-create
- supabase-branches-delete
- supabase-branches-disable
- supabase-branches-get
- supabase-branches-list
- supabase-branches-update
flags: []
- id: supabase-branches-update
title: supabase branches update
summary: Update a preview branch
description: Update a preview branch by its ID.
tags: []
links: []
usage: supabase branches update <branch-id> [flags]
subcommands: []
flags:
- id: git-branch
name: --git-branch <string>
description: Change the associated git branch.
default_value: ''
- id: name
name: --name <string>
description: Rename the preview branch.
default_value: ''
- id: experimental
name: --experimental
description: enable experimental features
required: true
default_value: 'false'
- id: supabase-branches-list
title: supabase branches list
summary: List all preview branches
description: List all preview branches of the linked project.
tags: []
links: []
usage: supabase branches list
subcommands: []
flags:
- id: experimental
name: --experimental
description: enable experimental features
required: true
default_value: 'false'
- id: supabase-branches-get
title: supabase branches get
summary: Retrieve details of a preview branch
description: Retrieve details of the specified preview branch.
tags: []
links: []
usage: supabase branches get <branch-id>
subcommands: []
flags:
- id: experimental
name: --experimental
description: enable experimental features
required: true
default_value: 'false'
- id: supabase-branches-disable
title: supabase branches disable
summary: Disable preview branching
description: Disable preview branching for the linked project.
tags: []
links: []
usage: supabase branches disable
subcommands: []
flags:
- id: experimental
name: --experimental
description: enable experimental features
required: true
default_value: 'false'
- id: supabase-branches-delete
title: supabase branches delete
summary: Delete a preview branch
description: Delete a preview branch by its ID.
tags: []
links: []
usage: supabase branches delete <branch-id>
subcommands: []
flags:
- id: experimental
name: --experimental
description: enable experimental features
required: true
default_value: 'false'
- id: supabase-branches-create
title: supabase branches create
summary: Create a preview branch
description: Create a preview branch for the linked project.
tags: []
links: []
usage: supabase branches create <name> [flags]
subcommands: []
flags:
- id: region
name: --region <string>
description: Select a region to deploy the branch database.
default_value: ''
accepted_values:
- id: ams
name: ams
type: string
- id: arn
name: arn
type: string
- id: bog
name: bog
type: string
- id: bos
name: bos
type: string
- id: cdg
name: cdg
type: string
- id: den
name: den
type: string
- id: dfw
name: dfw
type: string
- id: ewr
name: ewr
type: string
- id: fra
name: fra
type: string
- id: gdl
name: gdl
type: string
- id: gig
name: gig
type: string
- id: gru
name: gru
type: string
- id: hkg
name: hkg
type: string
- id: iad
name: iad
type: string
- id: jnb
name: jnb
type: string
- id: lax
name: lax
type: string
- id: lhr
name: lhr
type: string
- id: maa
name: maa
type: string
- id: mad
name: mad
type: string
- id: mia
name: mia
type: string
- id: nrt
name: nrt
type: string
- id: ord
name: ord
type: string
- id: otp
name: otp
type: string
- id: qro
name: qro
type: string
- id: scl
name: scl
type: string
- id: sea
name: sea
type: string
- id: sin
name: sin
type: string
- id: sjc
name: sjc
type: string
- id: syd
name: syd
type: string
- id: waw
name: waw
type: string
- id: yul
name: yul
type: string
- id: yyz
name: yyz
type: string
- id: experimental
name: --experimental
description: enable experimental features
required: true
default_value: 'false'
+174 -22
View File
@@ -210,28 +210,126 @@
"title": "Inspect",
"items": [
{
"id": "supabase-inspect",
"id": "supabase-inspect-db",
"title": "Inspect database statistics",
"slug": "supabase-inspect",
"type": "cli-command"
},
{
"id": "supabase-inspect-db:cache-hit",
"title": "Show cache hit rates for tables and indexes",
"slug": "supabase-inspect-db:cache-hit",
"type": "cli-command"
},
{
"id": "supabase-inspect-db:index-usage",
"title": "Show usage efficiency of indexes",
"slug": "supabase-inspect-db:index-usage",
"type": "cli-command"
},
{
"id": "supabase-inspect-db:replication-slots",
"title": "Show information about replication slots",
"slug": "supabase-inspect-db:replication-slots",
"type": "cli-command"
"slug": "supabase-inspect-db",
"type": "cli-command",
"items": [
{
"id": "supabase-inspect-db-calls",
"title": "Show most frequently run queries",
"slug": "supabase-inspect-db-calls",
"type": "cli-command"
},
{
"id": "supabase-inspect-db-long-running-queries",
"title": "Show long running queries",
"slug": "supabase-inspect-db-long-running-queries",
"type": "cli-command"
},
{
"id": "supabase-inspect-db-outliers",
"title": "Show queries ordered by total execution time",
"slug": "supabase-inspect-db-outliers",
"type": "cli-command"
},
{
"id": "supabase-inspect-db-blocking",
"title": "Show queries waiting and holding locks",
"slug": "supabase-inspect-db-blocking",
"type": "cli-command"
},
{
"id": "supabase-inspect-db-locks",
"title": "Show queries taking exclusive locks",
"slug": "supabase-inspect-db-locks",
"type": "cli-command"
},
{
"id": "supabase-inspect-db-total-index-size",
"title": "Show total size of all indexes",
"slug": "supabase-inspect-db-total-index-size",
"type": "cli-command"
},
{
"id": "supabase-inspect-db-index-sizes",
"title": "Show sizes of individual indexes",
"slug": "supabase-inspect-db-index-sizes",
"type": "cli-command"
},
{
"id": "supabase-inspect-db-index-usage",
"title": "Show information about index efficiency",
"slug": "supabase-inspect-db-index-usage",
"type": "cli-command"
},
{
"id": "supabase-inspect-db-unused-indexes",
"title": "Show indexes with low usage",
"slug": "supabase-inspect-db-unused-indexes",
"type": "cli-command"
},
{
"id": "supabase-inspect-db-total-table-sizes",
"title": "Show total size of all tables",
"slug": "supabase-inspect-db-total-table-sizes",
"type": "cli-command"
},
{
"id": "supabase-inspect-db-table-sizes",
"title": "Show sizes of individual tables",
"slug": "supabase-inspect-db-table-sizes",
"type": "cli-command"
},
{
"id": "supabase-inspect-db-table-index-sizes",
"title": "Show index sizes of individual tables",
"slug": "supabase-inspect-db-table-index-sizes",
"type": "cli-command"
},
{
"id": "supabase-inspect-db-cache-hit",
"title": "Show cache hit rates for tables and indices",
"slug": "supabase-inspect-db-cache-hit",
"type": "cli-command"
},
{
"id": "supabase-inspect-db-table-record-counts",
"title": "Show estimated number of rows per table",
"slug": "supabase-inspect-db-table-record-counts",
"type": "cli-command"
},
{
"id": "supabase-inspect-db-seq-scans",
"title": "Show number of sequential scans for all tables",
"slug": "supabase-inspect-db-seq-scans",
"type": "cli-command"
},
{
"id": "supabase-inspect-db-replication-slots",
"title": "Show information about replication slots",
"slug": "supabase-inspect-db-replication-slots",
"type": "cli-command"
},
{
"id": "supabase-inspect-db-role-connections",
"title": "Show number of active connections",
"slug": "supabase-inspect-db-role-connections",
"type": "cli-command"
},
{
"id": "supabase-inspect-db-bloat",
"title": "Show estimated database bloat",
"slug": "supabase-inspect-db-bloat",
"type": "cli-command"
},
{
"id": "supabase-inspect-db-vacuum-stats",
"title": "Show statistics related to vacuum operations",
"slug": "supabase-inspect-db-vacuum-stats",
"type": "cli-command"
}
]
}
]
},
@@ -259,12 +357,60 @@
},
{
"id": "supabase-projects-api-keys",
"title": "Show API keys for a Supabase project",
"title": "Show API keys for a project",
"slug": "supabase-projects-api-keys",
"type": "cli-command"
}
]
},
{
"type": "category",
"title": "Branches",
"items": [
{
"id": "supabase-branches",
"title": "Manage preview branches",
"slug": "supabase-branches",
"type": "cli-command"
},
{
"id": "supabase-branches-create",
"title": "Create a preview branch",
"slug": "supabase-branches-create",
"type": "cli-command"
},
{
"id": "supabase-branches-list",
"title": "List all preview branches",
"slug": "supabase-branches-list",
"type": "cli-command"
},
{
"id": "supabase-branches-get",
"title": "Show connection details for a preview branch",
"slug": "supabase-branches-get",
"type": "cli-command"
},
{
"id": "supabase-branches-update",
"title": "Update a preview branch",
"slug": "supabase-branches-update",
"type": "cli-command"
},
{
"id": "supabase-branches-delete",
"title": "Delete a preview branch",
"slug": "supabase-branches-delete",
"type": "cli-command"
},
{
"id": "supabase-branches-disable",
"title": "Disable preview branching",
"slug": "supabase-branches-disable",
"type": "cli-command"
}
]
},
{
"type": "category",
"title": "Organizations",
@@ -275,6 +421,12 @@
"slug": "supabase-orgs",
"type": "cli-command"
},
{
"id": "supabase-orgs-create",
"title": "Create an organization",
"slug": "supabase-orgs-create",
"type": "cli-command"
},
{
"id": "supabase-orgs-list",
"title": "List all organizations",
+14 -5
View File
@@ -1768,13 +1768,22 @@ functions:
- id: conditional-chaining
name: Conditional Chaining
description: |
Filters can be built up one step at a time and then executed. For example:
Filters can be built up one step at a time and then executed as shown in the example code.
code: |
```dart
final data = await supabase
.from('users')
.select()
.eq('address->postcode', 90210);
final filterByName = null;
final filterPopLow = 1000;
final filterPopHigh = 10000;
var query = supabase
.from('cities')
.select('name, country_id');
if (filterByName != null) { query = query.eq('name', filterByName); }
if (filterPopLow != null) { query = query.gte('population', filterPopLow); }
if (filterPopHigh != null) { query = query.lt('population', filterPopHigh); }
final data = await query;
```
- id: filter-by-value-within-json-column
name: Filter by values within a JSON column
@@ -25,7 +25,7 @@ const AccessTokenList = observer(() => {
return (
<>
<div className="max-w-7xl">
<div>
<Table
head={[
<Table.th key="header-token">Token</Table.th>,
@@ -1,26 +1,12 @@
import { FC } from 'react'
import Link from 'next/link'
import IncidentBanner from 'components/layouts/AppLayout/IncidentBanner'
import { useFlag } from 'hooks'
import { IconExternalLink } from 'ui'
import { PropsWithChildren } from 'react'
const AppBannerWrapper: FC = ({ children }) => {
const AppBannerWrapper = ({ children }: PropsWithChildren<{}>) => {
const ongoingIncident = useFlag('ongoingIncident')
return (
<div className="min-h-full flex flex-col">
{ongoingIncident && (
<Link href="https://status.supabase.com">
<a target="_blank" rel="noreferrer">
<div className="flex cursor-pointer items-center justify-center space-x-2 bg-green-900 py-3 text-scale-400 transition hover:bg-green-1000 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>
)}
{ongoingIncident && <IncidentBanner />}
{children}
</div>
@@ -4,16 +4,17 @@ import { PropsWithChildren, useEffect } from 'react'
import { useOrganizationsQuery } from 'data/organizations/organizations-query'
import { useProjectsQuery } from 'data/projects/projects-query'
import { useStore } from 'hooks'
import { useFlag, useStore } from 'hooks'
import useLatest from 'hooks/misc/useLatest'
import { useParams } from 'common'
import { LOCAL_STORAGE_KEYS } from 'lib/constants'
// Ideally these could all be within a _middleware when we use Next 12
const RouteValidationWrapper = ({ children }: PropsWithChildren<{}>) => {
const { ui } = useStore()
const router = useRouter()
const projectRef = router.query.ref
const orgSlug = router.query.slug
const { ref, slug } = useParams()
const navLayoutV2 = useFlag('navigationLayoutV2')
/**
* Array of urls/routes that should be ignored
@@ -43,14 +44,14 @@ const RouteValidationWrapper = ({ children }: PropsWithChildren<{}>) => {
// check if current route is excempted from route validation check
if (isExceptUrl()) return
if (orgsInitialized && orgSlug) {
if (orgsInitialized && slug) {
// Check validity of organization that user is trying to access
const organizations = organizationsRef.current ?? []
const isValidOrg = organizations.some((org) => org.slug === orgSlug)
const isValidOrg = organizations.some((org) => org.slug === slug)
if (!isValidOrg) {
ui.setNotification({ category: 'error', message: 'This organization does not exist' })
router.push('/projects')
router.push(navLayoutV2 ? `/org/${organizations[0].slug}` : '/projects')
return
}
}
@@ -63,19 +64,43 @@ const RouteValidationWrapper = ({ children }: PropsWithChildren<{}>) => {
// check if current route is excempted from route validation check
if (isExceptUrl()) return
if (projectsInitialized && projectRef) {
if (projectsInitialized && ref) {
// Check validity of project that the user is trying to access
const projects = projectsRef.current ?? []
const isValidProject = projects.some((project) => project.ref === projectRef)
const isValidProject = projects.some((project) => project.ref === ref)
if (!isValidProject) {
ui.setNotification({ category: 'error', message: 'This project does not exist' })
router.push('/projects')
router.push(navLayoutV2 ? `/org/${organizations?.[0].slug}` : '/projects')
return
}
}
}, [projectsInitialized])
useEffect(() => {
if (orgsInitialized && slug) {
// Save organization slug to local storage
const organizations = organizationsRef.current ?? []
const organization = organizations.find((org) => org.slug === slug)
if (organization) {
localStorage.setItem(LOCAL_STORAGE_KEYS.RECENTLY_VISITED_ORGANIZATION, organization.slug)
}
}
}, [slug, orgsInitialized])
useEffect(() => {
if (projectsInitialized && ref) {
// Save organization slug to local storage
const projects = projectsRef.current ?? []
const project = projects.find((project) => project.ref === ref)
const organizationId = project?.organization_id
const organization = organizations?.find((organization) => organization.id === organizationId)
if (organization) {
localStorage.setItem(LOCAL_STORAGE_KEYS.RECENTLY_VISITED_ORGANIZATION, organization.slug)
}
}
}, [ref, projectsInitialized])
return <>{children}</>
}
@@ -30,6 +30,10 @@ export interface AuthConfig {
EXTERNAL_FACEBOOK_CLIENT_ID: string | null
EXTERNAL_FACEBOOK_SECRET: string | null
EXTERNAL_FIGMA_ENABLED: boolean
EXTERNAL_FIGMA_CLIENT_ID: string | null
EXTERNAL_FIGMA_SECRET: string | null
EXTERNAL_GITHUB_ENABLED: false
EXTERNAL_GITHUB_CLIENT_ID: string | null
EXTERNAL_GITHUB_SECRET: string | null
@@ -1,6 +1,6 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { observer } from 'mobx-react-lite'
import { useEffect, useState } from 'react'
import { useEffect } from 'react'
import { Form, Input } from 'ui'
import { boolean, number, object, string } from 'yup'
@@ -10,17 +10,14 @@ import {
FormPanel,
FormSection,
FormSectionContent,
FormSectionLabel,
} from 'components/ui/Forms'
import { useCheckPermissions, useStore } from 'hooks'
import { useCheckPermissions, useSelectedProject, useStore } from 'hooks'
const SiteUrl = observer(() => {
const { authConfig, ui } = useStore()
const { isLoaded } = authConfig
const formId = 'auth-config-general-form'
const [hidden, setHidden] = useState(true)
const canUpdateConfig = useCheckPermissions(PermissionAction.UPDATE, 'custom_config_gotrue')
const INITIAL_VALUES = {
@@ -1,24 +1,20 @@
import * as Tooltip from '@radix-ui/react-tooltip'
import clsx from 'clsx'
import { useParams } from 'common'
import ShimmeringLoader from 'components/ui/ShimmeringLoader'
import SparkBar from 'components/ui/SparkBar'
import { useUpcomingInvoiceQuery } from 'data/invoices/invoice-upcoming-query'
import { useProjectSubscriptionV2Query } from 'data/subscriptions/project-subscription-v2-query'
import {
ProjectUsageResponse,
UsageMetric,
useProjectUsageQuery,
} from 'data/usage/project-usage-query'
import { useProjectUsageQuery } from 'data/usage/project-usage-query'
import dayjs from 'dayjs'
import { USAGE_APPROACHING_THRESHOLD } from 'lib/constants'
import { formatBytes } from 'lib/helpers'
import { partition } from 'lodash'
import Link from 'next/link'
import { useState } from 'react'
import { useSubscriptionPageStateSnapshot } from 'state/subscription-page'
import { Alert, Button, Collapsible, IconAlertTriangle, IconChevronRight, IconInfo } from 'ui'
import { BILLING_BREAKDOWN_METRICS } from './Subscription.constants'
import { useSubscriptionPageStateSnapshot } from 'state/subscription-page'
import Link from 'next/link'
import * as Tooltip from '@radix-ui/react-tooltip'
export interface BillingBreakdownProps {}
@@ -127,22 +123,18 @@ const BillingBreakdown = ({}: BillingBreakdownProps) => {
) : (
<div className="grid grid-cols-12">
{BILLING_BREAKDOWN_METRICS.map((metric, i) => {
const usageMeta =
(usage?.[metric.key as keyof ProjectUsageResponse] as UsageMetric) ?? undefined
const usageRatio =
typeof usageMeta !== 'number'
? (usageMeta?.usage ?? 0) / (usageMeta?.limit ?? 0)
: 0
const usageMeta = usage?.[metric.key]
const usageRatio = (usageMeta?.usage ?? 0) / (usageMeta?.limit ?? 0)
const hasLimit = usageMeta.limit > 0
const hasLimit = (usageMeta?.limit ?? 0) > 0
const usageCurrentLabel =
metric.units === 'bytes'
? formatBytes(usageMeta.usage)
: usageMeta.usage?.toLocaleString()
? formatBytes(usageMeta?.usage)
: usageMeta?.usage?.toLocaleString()
const usageLimitLabel =
metric.units === 'bytes'
? formatBytes(usageMeta.limit)
: usageMeta.limit.toLocaleString()
? formatBytes(usageMeta?.limit)
: usageMeta?.limit.toLocaleString()
const usageLabel = `${usageCurrentLabel} ${hasLimit ? `of ${usageLimitLabel}` : ''}`
const percentageLabel = `${(usageRatio * 100).toFixed(2)}%`
@@ -282,44 +274,55 @@ const BillingBreakdown = ({}: BillingBreakdownProps) => {
</Tooltip.Root>
)}
</div>
{usageMeta.available_in_plan ? (
<SparkBar
type="horizontal"
// If the limit is 0, it means that the usage is unlimited and not billed
// By setting "1" as max, the bar is only filled if the metric has any usage
// This is only the case for Enterprise plans
max={usageMeta.limit || 1}
value={usageMeta.usage ?? 0}
barClass={
!hasLimit && usageMeta.usage > 0
? 'bg-scale-1100'
: isExceededLimit && !isUsageBillingEnabled
? 'bg-red-900'
: isApproachingLimit && !isUsageBillingEnabled
? 'bg-amber-900'
: 'bg-scale-1100'
}
bgClass="bg-gray-300 dark:bg-gray-600"
labelBottom={usageLabel}
labelBottomClass="!text-scale-1000"
labelTop={hasLimit ? percentageLabel : undefined}
labelTopClass={
!hasLimit
? ''
: isExceededLimit && !isUsageBillingEnabled
? '!text-red-900'
: isApproachingLimit && !isUsageBillingEnabled
? '!text-amber-900'
: ''
}
/>
{usageMeta ? (
usageMeta.available_in_plan ? (
<SparkBar
type="horizontal"
// If the limit is 0, it means that the usage is unlimited and not billed
// By setting "1" as max, the bar is only filled if the metric has any usage
// This is only the case for Enterprise plans
max={usageMeta.limit || 1}
value={usageMeta.usage ?? 0}
barClass={
!hasLimit && usageMeta.usage > 0
? 'bg-scale-1100'
: isExceededLimit && !isUsageBillingEnabled
? 'bg-red-900'
: isApproachingLimit && !isUsageBillingEnabled
? 'bg-amber-900'
: 'bg-scale-1100'
}
bgClass="bg-gray-300 dark:bg-gray-600"
labelBottom={usageLabel}
labelBottomClass="!text-scale-1000"
labelTop={hasLimit ? percentageLabel : undefined}
labelTopClass={
!hasLimit
? ''
: isExceededLimit && !isUsageBillingEnabled
? '!text-red-900'
: isApproachingLimit && !isUsageBillingEnabled
? '!text-amber-900'
: ''
}
/>
) : (
// [Joshen] Needs a better CTA here
<div className="flex items-center justify-between flex-grow">
<p className="text-sm text-scale-1000">Unavailable in your plan</p>
<Button
type="default"
onClick={() => snap.setPanelKey('subscriptionPlan')}
>
Upgrade
</Button>
</div>
)
) : (
// [Joshen] Needs a better CTA here
<div className="flex items-center justify-between flex-grow">
<p className="text-sm text-scale-1000">Unavailable in your plan</p>
<Button type="default" onClick={() => snap.setPanelKey('subscriptionPlan')}>
Upgrade
</Button>
<p className="text-sm text-scale-1000">
The usage for this metric is missing.
</p>
</div>
)}
</div>
@@ -102,4 +102,4 @@ export const BILLING_BREAKDOWN_METRICS = [
category: 'Edge Functions',
unitName: 'function'
},
]
] as const
@@ -307,13 +307,7 @@ const TablesGraph = ({ tables }: { tables: PostgresTable[] }) => {
nodeTypes={nodeTypes}
fitView
maxZoom={1.5}
proOptions={{
/**
* TODO: Sponsor Reactflow before using it in production! 😇
* https://pro.reactflow.dev/
*/
hideAttribution: false,
}}
proOptions={{ hideAttribution: true }}
>
<Background gap={16} color={backgroundPatternColor} variant={BackgroundVariant.Lines} />
</ReactFlow>
@@ -24,7 +24,7 @@ const ProjectCard = ({ project, rewriteHref }: ProjectCardProps) => {
const isRestoring = project.status === PROJECT_STATUS.RESTORING
return (
<li className="col-span-1">
<li className="col-span-1 list-none">
<CardButton
linkHref={rewriteHref ? rewriteHref : `/project/${projectRef}`}
title={
@@ -35,7 +35,7 @@ const HandleIcon = ({ type, className }: { type: HandleIconType; className?: str
viewBox="0 0 512 512"
className={cn('w-3.5', className)}
>
<path fill-rule="evenodd" d="M256,48,496,464H16Z" />
<path fillRule="evenodd" d="M256,48,496,464H16Z" />
</svg>
)
@@ -79,7 +79,7 @@ const IntegrationInstallation = React.forwardRef<HTMLLIElement, IntegrationInsta
<li
ref={ref}
key={integration.id}
className="bg border shadow-sm flex justify-between items-center px-8 py-4 rounded-lg"
className="bg-surface-100 border shadow-sm flex justify-between items-center px-8 py-4 rounded-lg"
{...props}
>
<div className="flex gap-6 items-center">
@@ -146,7 +146,7 @@ const IntegrationConnection = React.forwardRef<HTMLLIElement, IntegrationConnect
className={cn('ml-6 pl-8 pb-2 border-l border-scale-600 dark:border-scale-400', 'relative')}
>
<div className="absolute w-8 rounded-bl-full border-b border-l border-scale-600 dark:border-scale-400 h-10 -left-px"></div>
<div className="bg border shadow-sm flex justify-between items-center px-8 py-4 rounded-lg">
<div className="bg-surface-100 border shadow-sm flex justify-between items-center px-8 py-4 rounded-lg">
<div className="flex flex-col gap-1">
<div className="flex gap-2 items-center">
<HandleIcon type={'Supabase'} />
@@ -194,7 +194,9 @@ const IntegrationConnectionOption = React.forwardRef<HTMLLIElement, IntegrationC
ref={ref}
key={connection.id}
{...props}
className={cn('bg border shadow-sm flex justify-between items-center px-8 py-4 rounded-lg')}
className={cn(
'bg-surface-100 border shadow-sm flex justify-between items-center px-8 py-4 rounded-lg'
)}
>
<div className="flex flex-col gap-1">
<div className="flex gap-2 items-center">
@@ -1,10 +1,23 @@
import { ReactNode, useState } from 'react'
import { ReactNode, useRef, useState } from 'react'
import { IntegrationProjectConnection } from 'data/integrations/integrations.types'
import { IntegrationConnectionsCreateVariables } from 'data/integrations/types'
import { useSelectedOrganization } from 'hooks'
import { BASE_PATH } from 'lib/constants'
import { Button, Listbox, cn } from 'ui'
import {
Button,
CommandEmpty_Shadcn_,
CommandGroup_Shadcn_,
CommandInput_Shadcn_,
CommandItem_Shadcn_,
CommandList_Shadcn_,
Command_Shadcn_,
IconChevronDown,
PopoverContent_Shadcn_,
PopoverTrigger_Shadcn_,
Popover_Shadcn_,
cn,
} from 'ui'
export interface Project {
id: string
@@ -28,10 +41,10 @@ export interface ProjectLinkerProps {
getForeignProjectIcon?: (project: ForeignProject) => ReactNode
choosePrompt?: string
onSkip?: () => void
loadingForeignProjects?: boolean
loadingSupabaseProjects?: boolean
}
const UNDEFINED_SELECT_VALUE = 'undefined'
const ProjectLinker = ({
organizationIntegrationId,
foreignProjects,
@@ -43,25 +56,47 @@ const ProjectLinker = ({
getForeignProjectIcon,
choosePrompt = 'Choose a project',
onSkip,
loadingForeignProjects,
loadingSupabaseProjects,
}: ProjectLinkerProps) => {
const [supabaseProjectsComboBoxOpen, setSupabaseProjectsComboboxOpen] = useState(false)
const [foreignProjectsComboBoxOpen, setForeignProjectsComboboxOpen] = useState(false)
const supabaseProjectsComboBoxRef = useRef<HTMLButtonElement>(null)
const foreignProjectsComboBoxRef = useRef<HTMLButtonElement>(null)
const selectedOrganization = useSelectedOrganization()
const [supabaseProjectRef, setSupabaseProjectRef] = useState(UNDEFINED_SELECT_VALUE)
const [foreignProjectId, setForeignProjectId] = useState(UNDEFINED_SELECT_VALUE)
const [supabaseProjectRef, setSupabaseProjectRef] = useState<string | undefined>(undefined)
const [foreignProjectId, setForeignProjectId] = useState<string | undefined>(undefined)
// create a flat array of foreign project ids. ie, ["prj_MlkO6AiLG5ofS9ojKrkS3PhhlY3f", ..]
const flatInstalledConnectionsIds = new Set(installedConnections.map((x) => x.foreign_project_id))
// check that foreign project is not already installed
const filteredForeignProjects = foreignProjects.filter((foreignProject) => {
return !flatInstalledConnectionsIds.has(foreignProject.id)
})
const selectedSupabaseProject = supabaseProjectRef
? supabaseProjects.find((x) => x.ref?.toLowerCase() === supabaseProjectRef?.toLowerCase())
: undefined
const selectedForeignProject = foreignProjectId
? filteredForeignProjects.find((x) => x.id?.toLowerCase() === foreignProjectId?.toLowerCase())
: undefined
function onCreateConnections() {
const projectDetails = foreignProjects.filter((x) => x.id === foreignProjectId)[0]
const projectDetails = selectedForeignProject
if (!organizationIntegrationId) {
console.error('No integration ID set')
return
}
if (!organizationIntegrationId) return console.error('No integration ID set')
if (!selectedForeignProject?.id) return console.error('No Foreign project ID set')
if (!selectedSupabaseProject?.ref) return console.error('No Supabase project ref set')
_onCreateConnections({
organizationIntegrationId,
connection: {
foreign_project_id: foreignProjectId,
supabase_project_ref: supabaseProjectRef,
foreign_project_id: selectedForeignProject?.id,
supabase_project_ref: selectedSupabaseProject?.ref,
metadata: {
...projectDetails,
},
@@ -74,7 +109,7 @@ const ProjectLinker = ({
return (
<div
className={cn(
'flex flex-col gap-6 px-5 mx-auto w-full justify-center items-center',
'flex flex-col grow gap-6 px-5 mx-auto w-full justify-center items-center',
className
)}
{...props}
@@ -84,14 +119,6 @@ const ProjectLinker = ({
)
}
// create a flat array of foreign project ids. ie, ["prj_MlkO6AiLG5ofS9ojKrkS3PhhlY3f", ..]
const flatInstalledConnectionsIds = new Set(installedConnections.map((x) => x.foreign_project_id))
// check that foreign project is not already installed
const filteredForeignProjects = foreignProjects.filter((foreignProject) => {
return !flatInstalledConnectionsIds.has(foreignProject.id)
})
return (
<div className="flex flex-col gap-4">
<div className="relative border rounded-lg p-12 bg shadow">
@@ -104,66 +131,149 @@ const ProjectLinker = ({
<div className="bg-white shadow border rounded p-1 w-12 h-12 flex justify-center items-center">
<img src={`${BASE_PATH}/img/supabase-logo.svg`} alt="Supabase" className="w-6" />
</div>
<Listbox
className="w-full"
value={supabaseProjectRef ?? UNDEFINED_SELECT_VALUE}
onChange={(e) => setSupabaseProjectRef(e)}
<Popover_Shadcn_
open={supabaseProjectsComboBoxOpen}
onOpenChange={setSupabaseProjectsComboboxOpen}
>
<Listbox.Option value={UNDEFINED_SELECT_VALUE} label="Choose a project" disabled>
Choose project
</Listbox.Option>
{supabaseProjects.map((project) => (
<Listbox.Option
key={project.id}
value={project.ref}
label={project.name}
addOnBefore={() => {
return (
<>
<div className="bg-white shadow border rounded p-1 w-6 h-6 flex justify-center items-center">
<img
src={`${BASE_PATH}/img/supabase-logo.svg`}
alt="Supabase"
className="w-4"
/>
</div>
</>
)
}}
<PopoverTrigger_Shadcn_ asChild>
<Button
ref={supabaseProjectsComboBoxRef}
type="default"
size="medium"
block
disabled={loadingSupabaseProjects}
loading={loadingSupabaseProjects}
className="justify-start"
icon={
<div className="bg-white shadow border rounded p-1 w-6 h-6 flex justify-center items-center">
<img
src={`${BASE_PATH}/img/supabase-logo.svg`}
alt="Supabase"
className="w-4"
/>
</div>
}
iconRight={
<span className="grow flex justify-end">
<IconChevronDown className={''} />
</span>
}
>
{project.name}
</Listbox.Option>
))}
</Listbox>
{selectedSupabaseProject ? selectedSupabaseProject.name : 'Choose Project'}
</Button>
</PopoverTrigger_Shadcn_>
<PopoverContent_Shadcn_
className="p-0 w-full"
side="bottom"
align="center"
style={{ width: supabaseProjectsComboBoxRef.current?.offsetWidth }}
>
<Command_Shadcn_>
<CommandInput_Shadcn_ placeholder="Search organization..." />
<CommandList_Shadcn_ className="!max-h-[170px]">
<CommandEmpty_Shadcn_>No results found.</CommandEmpty_Shadcn_>
<CommandGroup_Shadcn_>
{supabaseProjects.map((project) => {
return (
<CommandItem_Shadcn_
value={project.ref}
key={project.ref}
className="flex gap-2 items-center"
onSelect={(ref) => {
if (ref) setSupabaseProjectRef(ref)
setSupabaseProjectsComboboxOpen(false)
}}
>
<div className="bg-white shadow border rounded p-1 w-6 h-6 flex justify-center items-center">
<img
src={`${BASE_PATH}/img/supabase-logo.svg`}
alt="Supabase"
className="w-4"
/>
</div>
<span>{project.name}</span>
</CommandItem_Shadcn_>
)
})}
</CommandGroup_Shadcn_>
</CommandList_Shadcn_>
</Command_Shadcn_>
</PopoverContent_Shadcn_>
</Popover_Shadcn_>
</Panel>
<div className="border border-scale-1000 h-px w-16 border-dashed self-end mb-5"></div>
<Panel>
<div className="bg-black shadow rounded p-1 w-12 h-12 flex justify-center items-center">
{integrationIcon}
<svg
xmlns="http://www.w3.org/2000/svg"
fill="white"
viewBox="0 0 512 512"
className="w-6"
>
<path fillRule="evenodd" d="M256,48,496,464H16Z" />
</svg>
</div>
<Listbox
className="w-full"
value={foreignProjectId ?? UNDEFINED_SELECT_VALUE}
onChange={(e) => setForeignProjectId(e)}
<Popover_Shadcn_
open={foreignProjectsComboBoxOpen}
onOpenChange={setForeignProjectsComboboxOpen}
>
<Listbox.Option value={UNDEFINED_SELECT_VALUE} label={choosePrompt} disabled>
{choosePrompt}
</Listbox.Option>
{filteredForeignProjects.map((project) => {
return (
<Listbox.Option
key={project.id}
value={project.id}
label={project.name}
addOnBefore={() => {
return getForeignProjectIcon?.(project) ?? null
}}
>
{project.name}
</Listbox.Option>
)
})}
</Listbox>
<PopoverTrigger_Shadcn_ asChild>
<Button
ref={foreignProjectsComboBoxRef}
type="default"
size="medium"
block
disabled={loadingForeignProjects}
loading={loadingForeignProjects}
className="justify-start"
icon={
selectedForeignProject
? getForeignProjectIcon?.(selectedForeignProject)
: integrationIcon
}
iconRight={
<span className="grow flex justify-end">
<IconChevronDown className={''} />
</span>
}
>
{(selectedForeignProject && selectedForeignProject.name) ?? choosePrompt}
</Button>
</PopoverTrigger_Shadcn_>
<PopoverContent_Shadcn_
className="p-0 w-full"
side="bottom"
align="center"
style={{ width: foreignProjectsComboBoxRef.current?.offsetWidth }}
>
<Command_Shadcn_>
<CommandInput_Shadcn_ placeholder="Search organization..." />
<CommandList_Shadcn_ className="!max-h-[170px]">
<CommandEmpty_Shadcn_>No results found.</CommandEmpty_Shadcn_>
<CommandGroup_Shadcn_>
{filteredForeignProjects.map((project) => {
return (
<CommandItem_Shadcn_
value={project.id}
key={project.id}
className="flex gap-2 items-center"
onSelect={(id) => {
if (id) setForeignProjectId(id)
setForeignProjectsComboboxOpen(false)
}}
>
{getForeignProjectIcon?.(project) ?? integrationIcon}
<span>{project.name}</span>
</CommandItem_Shadcn_>
)
})}
</CommandGroup_Shadcn_>
</CommandList_Shadcn_>
</Command_Shadcn_>
</PopoverContent_Shadcn_>
</Popover_Shadcn_>
</Panel>
</div>
</div>
@@ -184,7 +294,15 @@ const ProjectLinker = ({
className="self-end"
onClick={onCreateConnections}
loading={isLoading}
disabled={isLoading}
disabled={
// data loading states
loadingForeignProjects ||
loadingSupabaseProjects ||
isLoading ||
// check wether both project types are not undefined
!selectedSupabaseProject ||
!selectedForeignProject
}
>
Connect project
</Button>
@@ -7,6 +7,7 @@ import { useEffect, useState } from 'react'
import { Collapsible, Form, IconChevronRight, Input, Toggle } from 'ui'
import { useParams } from 'common/hooks'
import { ScaffoldContainerLegacy } from 'components/layouts/Scaffold'
import {
FormActions,
FormPanel,
@@ -18,9 +19,8 @@ import { invalidateOrganizationsQuery } from 'data/organizations/organizations-q
import { useCheckPermissions, useFlag, useSelectedOrganization, useStore } from 'hooks'
import { isResponseOk, patch } from 'lib/common/fetch'
import { API_URL } from 'lib/constants'
import OrganizationDeletePanel from './OrganizationDeletePanel'
import { ScaffoldContainerLegacy } from 'components/layouts/Scaffold'
import { Organization } from 'types'
import OrganizationDeletePanel from './OrganizationDeletePanel'
const GeneralSettings = () => {
const queryClient = useQueryClient()
@@ -82,7 +82,7 @@ const InfrastructureInfo = () => {
label="Current version"
actions={[
isOnLatestVersion && (
<Tooltip.Root delayDuration={0}>
<Tooltip.Root key="tooltip-latest" delayDuration={0}>
<Tooltip.Trigger>
<Badge color="green" className="mr-1">
Latest
@@ -8,6 +8,7 @@ import { useSignOut } from 'lib/auth'
import { IS_PLATFORM } from 'lib/constants'
import { SidebarSection } from './AccountLayout.types'
import WithSidebar from './WithSidebar'
import SettingsLayout from '../SettingsLayout/SettingsLayout'
export interface AccountLayoutProps {
title: string
@@ -23,12 +24,12 @@ const AccountLayout = ({ children, title, breadcrumbs }: PropsWithChildren<Accou
const selectedOrganization = useSelectedOrganization()
const ongoingIncident = useFlag('ongoingIncident')
const navLayoutV2 = useFlag('navigationLayoutV2')
const maxHeight = ongoingIncident ? 'calc(100vh - 44px)' : '100vh'
const signOut = useSignOut()
const onClickLogout = async () => {
await signOut()
await router.push('/sign-in')
}
@@ -126,6 +127,10 @@ const AccountLayout = ({ children, title, breadcrumbs }: PropsWithChildren<Accou
: []),
]
if (navLayoutV2) {
return <SettingsLayout>{children}</SettingsLayout>
}
return (
<>
<Head>
@@ -137,7 +142,12 @@ const AccountLayout = ({ children, title, breadcrumbs }: PropsWithChildren<Accou
style={{ height: maxHeight, maxHeight }}
className="flex flex-col flex-1 w-full overflow-y-auto"
>
<WithSidebar title={title} breadcrumbs={breadcrumbs} sections={sectionsWithHeaders}>
<WithSidebar
hideSidebar={navLayoutV2}
title={title}
breadcrumbs={breadcrumbs}
sections={sectionsWithHeaders}
>
{children}
</WithSidebar>
</main>
@@ -1,12 +1,12 @@
import { useFlag } from 'hooks'
import { isUndefined } from 'lodash'
import Link from 'next/link'
import { FC, ReactNode } from 'react'
import { isUndefined } from 'lodash'
import { Menu, IconArrowUpRight, Badge, IconLogOut } from 'ui'
import { useFlag } from 'hooks'
import { Badge, IconArrowUpRight, IconLogOut, Menu } from 'ui'
import LayoutHeader from '../ProjectLayout/LayoutHeader'
import { SidebarLink, SidebarSection } from './AccountLayout.types'
interface Props {
interface WithSidebarProps {
title: string
breadcrumbs: any[]
sections: SidebarSection[]
@@ -27,7 +27,7 @@ The information hierarchy for WithSidebar is:
SidebarItem
SidebarLink
*/
const WithSidebar: FC<Props> = ({
const WithSidebar = ({
title,
header,
breadcrumbs = [],
@@ -37,9 +37,10 @@ const WithSidebar: FC<Props> = ({
subitemsParentKey,
hideSidebar = false,
customSidebarContent,
}) => {
}: WithSidebarProps) => {
const noContent = !sections && !customSidebarContent
const ongoingIncident = useFlag('ongoingIncident')
const navLayoutV2 = useFlag('navigationLayoutV2')
const maxHeight = ongoingIncident ? 'calc(100vh - 44px)' : '100vh'
return (
@@ -89,7 +90,7 @@ const WithSidebar: FC<Props> = ({
</div>
)}
<div className="flex flex-1 flex-col">
<LayoutHeader breadcrumbs={breadcrumbs} />
{!navLayoutV2 && <LayoutHeader breadcrumbs={breadcrumbs} />}
<div className="flex-1 flex-grow overflow-auto">{children}</div>
</div>
</div>
@@ -0,0 +1,50 @@
import Link from 'next/link'
import { useRouter } from 'next/router'
import { useParams } from 'common'
import { useSelectedOrganization } from 'hooks'
import FeedbackDropdown from '../ProjectLayout/LayoutHeader/FeedbackDropdown'
import HelpPopover from '../ProjectLayout/LayoutHeader/HelpPopover'
import NotificationsPopover from '../ProjectLayout/LayoutHeader/NotificationsPopover'
import OrganizationDropdown from './OrganizationDropdown'
import ProjectDropdown from './ProjectDropdown'
import SettingsButton from './SettingsButton'
import UserSettingsDropdown from './UserSettingsDropdown'
const AppHeader = () => {
const router = useRouter()
const { ref } = useParams()
const organization = useSelectedOrganization()
return (
<div className="flex items-center justify-between px-4 py-1 bg-scale-200 border-b">
<div className="flex items-center space-x-4">
<Link href={organization !== undefined ? `/org/${organization?.slug}` : '/'}>
<a className="block">
<img
src={`${router.basePath}/img/supabase-logo.svg`}
alt="Supabase"
className="mx-auto h-[40px] w-6 cursor-pointer rounded"
/>
</a>
</Link>
<OrganizationDropdown />
{ref !== undefined && <ProjectDropdown />}
</div>
<div className="flex items-center gap-4">
<div className="flex items-center gap-3">
<FeedbackDropdown alt />
<NotificationsPopover alt />
<HelpPopover alt />
<SettingsButton />
</div>
<div className="flex items-center gap-3">
<UserSettingsDropdown />
</div>
</div>
</div>
)
}
export default AppHeader
@@ -0,0 +1,32 @@
import { PropsWithChildren } from 'react'
import AppHeader from './AppHeader'
import { useFlag } from 'hooks'
const AppLayout = ({ children }: PropsWithChildren<{}>) => {
const navLayoutV2 = useFlag('navigationLayoutV2')
const ongoingIncident = useFlag('ongoingIncident')
const appHeaderHeight = navLayoutV2 ? 49 : 0
const incidentBannerHeight = 44
return (
<div>
{navLayoutV2 && <AppHeader />}
<div
style={{
height: ongoingIncident
? `calc(100vh - ${incidentBannerHeight}px - ${appHeaderHeight}px)`
: `calc(100vh - ${appHeaderHeight}px)`,
maxHeight: ongoingIncident
? `calc(100vh - ${incidentBannerHeight}px - ${appHeaderHeight}px)`
: `calc(100vh - ${appHeaderHeight}px)`,
}}
>
{children}
</div>
</div>
)
}
export default AppLayout
@@ -0,0 +1,19 @@
import Link from 'next/link'
import { IconExternalLink } from 'ui'
const IncidentBanner = () => {
return (
<Link href="https://status.supabase.com">
<a target="_blank" rel="noreferrer">
<div className="flex cursor-pointer items-center justify-center space-x-2 bg-green-900 py-3 text-scale-400 transition hover:bg-green-1000 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>
)
}
export default IncidentBanner
@@ -0,0 +1,129 @@
import Link from 'next/link'
import { useRouter } from 'next/router'
import { useRef, useState } from 'react'
import {
Badge,
Button,
CommandEmpty_Shadcn_,
CommandGroup_Shadcn_,
CommandInput_Shadcn_,
CommandItem_Shadcn_,
CommandList_Shadcn_,
Command_Shadcn_,
IconCheck,
IconCode,
IconPlus,
PopoverContent_Shadcn_,
PopoverTrigger_Shadcn_,
Popover_Shadcn_,
} from 'ui'
import ShimmeringLoader from 'components/ui/ShimmeringLoader'
import { useOrganizationsQuery } from 'data/organizations/organizations-query'
import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query'
import { useFlag, useSelectedOrganization } from 'hooks'
const OrganizationDropdown = () => {
const router = useRouter()
const orgCreationV2 = useFlag('orgcreationv2')
const orgNameRef = useRef<HTMLAnchorElement>(null)
const selectedOrganization = useSelectedOrganization()
const { data: organizations, isLoading: isLoadingOrganizations } = useOrganizationsQuery()
const slug = selectedOrganization?.slug
const orgName = selectedOrganization?.name
const isOrgBilling = !!selectedOrganization?.subscription_id
const { data: subscription, isSuccess } = useOrgSubscriptionQuery(
{ orgSlug: slug },
{ enabled: isOrgBilling }
)
const [open, setOpen] = useState(false)
const popoverOffset = (orgNameRef.current?.offsetWidth ?? 0) + 12
if (isLoadingOrganizations) {
return <ShimmeringLoader className="w-[90px]" />
}
return (
<div className="flex items-center space-x-2 px-2">
<Link passHref href={slug ? `/org/${slug}` : '/'}>
<a ref={orgNameRef} className="flex items-center space-x-2">
<p className="text-sm">{orgName}</p>
{isSuccess && <Badge color="scale">{subscription?.plan.name}</Badge>}
</a>
</Link>
<Popover_Shadcn_ open={open} onOpenChange={setOpen} modal={false}>
<PopoverTrigger_Shadcn_ asChild>
<Button
type="text"
className="px-1"
icon={<IconCode className="text-scale-1100 rotate-90" strokeWidth={2} size={12} />}
/>
</PopoverTrigger_Shadcn_>
<PopoverContent_Shadcn_
className="p-0"
side="bottom"
align="start"
style={{ marginLeft: `-${popoverOffset}px` }}
>
<Command_Shadcn_>
<CommandInput_Shadcn_ placeholder="Find organization..." />
<CommandList_Shadcn_>
<CommandEmpty_Shadcn_>No organizations found</CommandEmpty_Shadcn_>
<CommandGroup_Shadcn_>
{organizations?.map((org) => {
const href = router.pathname.includes('[slug]')
? router.pathname.replace('[slug]', org.slug)
: router.pathname.includes('[ref]/settings')
? `/org/${org.slug}/general`
: `/org/${org.slug}`
return (
<Link passHref href={href} key={org.slug}>
<CommandItem_Shadcn_
asChild
value={org.name}
className="cursor-pointer w-full flex items-center justify-between"
onSelect={() => {
setOpen(false)
router.push(href)
}}
onClick={() => setOpen(false)}
>
<a>
{org.name}
{org.slug === slug && <IconCheck />}
</a>
</CommandItem_Shadcn_>
</Link>
)
})}
</CommandGroup_Shadcn_>
<CommandGroup_Shadcn_ className="border-t">
<Link passHref href={orgCreationV2 ? `/new-with-subscription` : `/new`}>
<CommandItem_Shadcn_
asChild
className="cursor-pointer flex items-center space-x-2 w-full"
onSelect={(e) => {
setOpen(false)
router.push(orgCreationV2 ? `/new-with-subscription` : `/new`)
}}
onClick={() => setOpen(false)}
>
<a>
<IconPlus size={14} strokeWidth={1.5} />
<p>New organization</p>
</a>
</CommandItem_Shadcn_>
</Link>
</CommandGroup_Shadcn_>
</CommandList_Shadcn_>
</Command_Shadcn_>
</PopoverContent_Shadcn_>
</Popover_Shadcn_>
</div>
)
}
export default OrganizationDropdown
@@ -0,0 +1,200 @@
import Link from 'next/link'
import { useRouter } from 'next/router'
import { ParsedUrlQuery } from 'querystring'
import { useRef, useState } from 'react'
import {
Badge,
Button,
CommandEmpty_Shadcn_,
CommandGroup_Shadcn_,
CommandInput_Shadcn_,
CommandItem_Shadcn_,
CommandList_Shadcn_,
Command_Shadcn_,
IconCheck,
IconCode,
IconPlus,
PopoverContent_Shadcn_,
PopoverTrigger_Shadcn_,
Popover_Shadcn_,
} from 'ui'
import ShimmeringLoader from 'components/ui/ShimmeringLoader'
import { useProjectsQuery } from 'data/projects/projects-query'
import { useProjectSubscriptionV2Query } from 'data/subscriptions/project-subscription-v2-query'
import { useSelectedOrganization, useSelectedProject } from 'hooks'
import { IS_PLATFORM, PROJECT_STATUS } from 'lib/constants'
import { Organization, Project } from 'types'
import { useParams } from 'common'
// [Fran] the idea is to let users change projects without losing the current page,
// but at the same time we need to redirect correctly between urls that might be
// unique to a project e.g. '/project/projectRef/editor/tableId'
// Right now, I'm gonna assume that any router query after the projectId,
// is a unique project id/marker so we'll redirect the user to the
// highest common route with just projectRef in the router queries.
export const sanitizeRoute = (route: string, routerQueries: ParsedUrlQuery) => {
const queryArray = Object.entries(routerQueries)
if (queryArray.length > 1) {
// [Joshen] Ideally we shouldn't use hard coded numbers, but temp workaround
// for storage bucket route since its longer
const isStorageBucketRoute = 'bucketId' in routerQueries
return route
.split('/')
.slice(0, isStorageBucketRoute ? 5 : 4)
.join('/')
} else {
return route
}
}
const ProjectLink = ({
project,
organization,
setOpen,
}: {
project: Project
organization?: Organization
setOpen: (value: boolean) => void
}) => {
const router = useRouter()
const { ref } = useParams()
const sanitizedRoute = sanitizeRoute(router.route, router.query)
const isOrgBilling = !!organization?.subscription_id
// [Joshen] Temp while we're interim between v1 and v2 billing
let href = sanitizedRoute?.replace('[ref]', project.ref) ?? `/project/${project.ref}`
if (href.endsWith('settings/addons') && !isOrgBilling) {
href = href.replace('settings/addons', 'settings/billing/subscription')
} else if (href.endsWith('settings/billing/subscription') && isOrgBilling) {
href = href.replace('settings/billing/subscription', 'settings/addons')
} else if (href.endsWith('settings/infrastructure') && !isOrgBilling) {
href = href.replace('settings/infrastructure', 'settings/billing/usage')
} else if (href.endsWith('settings/billing/usage') && !isOrgBilling) {
href = href.replace('settings/billing/usage', 'settings/infrastructure')
}
return (
<Link passHref href={href}>
<CommandItem_Shadcn_
asChild
key={project.ref}
value={project.name}
className="cursor-pointer w-full flex items-center justify-between"
onSelect={() => {
router.push(href)
setOpen(false)
}}
onClick={() => setOpen(false)}
>
<a>
{project.name}
{project.ref === ref && <IconCheck />}
</a>
</CommandItem_Shadcn_>
</Link>
)
}
const ProjectDropdown = () => {
const router = useRouter()
const { ref } = useParams()
const projectDetails = useSelectedProject()
const selectedOrganization = useSelectedOrganization()
const projectNameRef = useRef<HTMLAnchorElement>(null)
const { data: allProjects, isLoading: isLoadingProjects } = useProjectsQuery()
const isOrgBilling = !!selectedOrganization?.subscription_id
const { data: subscription, isSuccess } = useProjectSubscriptionV2Query(
{ projectRef: ref },
{ enabled: !isOrgBilling }
)
const projects = allProjects
?.filter((x) => x.status !== PROJECT_STATUS.INACTIVE)
.filter((x) => x.organization_id === selectedOrganization?.id)
.sort((a, b) => a.name.localeCompare(b.name))
const selectedProject = projectDetails || projects?.find((project) => project.ref === ref)
const [open, setOpen] = useState(false)
const popoverOffset = (projectNameRef.current?.offsetWidth ?? 0) + 12
if (isLoadingProjects) {
return <ShimmeringLoader className="w-[90px]" />
}
return IS_PLATFORM ? (
<div className="flex items-center space-x-2 px-2">
<Link href={`/project/${selectedProject?.ref}`}>
<a ref={projectNameRef} className="flex items-center space-x-2">
<p className="text-sm">{selectedProject?.name}</p>
{isSuccess && !isOrgBilling && <Badge color="slate">{subscription?.plan.name}</Badge>}
</a>
</Link>
<Popover_Shadcn_ open={open} onOpenChange={setOpen} modal={false}>
<PopoverTrigger_Shadcn_ asChild>
<Button
type="text"
className="px-1"
icon={<IconCode className="text-scale-1100 rotate-90" strokeWidth={2} size={12} />}
/>
</PopoverTrigger_Shadcn_>
<PopoverContent_Shadcn_
className="p-0"
side="bottom"
align="start"
style={{ marginLeft: `-${popoverOffset}px` }}
>
<Command_Shadcn_>
<CommandInput_Shadcn_ placeholder="Find project..." />
<CommandList_Shadcn_>
<CommandEmpty_Shadcn_>No projects found</CommandEmpty_Shadcn_>
<CommandGroup_Shadcn_>
{projects?.map((project) => (
<ProjectLink
key={project.ref}
project={project}
organization={selectedOrganization}
setOpen={setOpen}
/>
))}
</CommandGroup_Shadcn_>
<CommandGroup_Shadcn_ className="border-t">
<Link
passHref
href={`/new/${selectedOrganization?.slug}`}
onClick={() => {
setOpen(false)
}}
>
<CommandItem_Shadcn_
asChild
className="cursor-pointer flex items-center space-x-2 w-full"
onSelect={() => {
setOpen(false)
router.push(`/new/${selectedOrganization?.slug}`)
}}
onClick={() => setOpen(false)}
>
<a>
<IconPlus size={14} strokeWidth={1.5} />
<p>New project</p>
</a>
</CommandItem_Shadcn_>
</Link>
</CommandGroup_Shadcn_>
</CommandList_Shadcn_>
</Command_Shadcn_>
</PopoverContent_Shadcn_>
</Popover_Shadcn_>
</div>
) : (
<Button type="text">
<span className="text-sm">{selectedProject?.name}</span>
</Button>
)
}
export default ProjectDropdown
@@ -0,0 +1,36 @@
import * as Tooltip from '@radix-ui/react-tooltip'
import { useSelectedOrganization } from 'hooks'
import Link from 'next/link'
import { IconSettings } from 'ui'
const SettingsButton = () => {
const selectedOrganization = useSelectedOrganization()
const slug = selectedOrganization?.slug
return (
<Tooltip.Root delayDuration={0}>
<Link href={slug ? `/org/${slug}/general` : '/'} passHref>
<Tooltip.Trigger type="button" asChild className="px-1">
<a>
<IconSettings size={18} strokeWidth={1.5} className="text-scale-1100" />
</a>
</Tooltip.Trigger>
</Link>
<Tooltip.Portal>
<Tooltip.Content side="bottom">
<Tooltip.Arrow className="radix-tooltip-arrow" />
<div
className={[
'rounded bg-scale-100 py-1 px-2 leading-none shadow',
'border border-scale-200',
].join(' ')}
>
<span className="text-xs text-scale-1200">Settings</span>
</div>
</Tooltip.Content>
</Tooltip.Portal>
</Tooltip.Root>
)
}
export default SettingsButton
@@ -0,0 +1,120 @@
import Link from 'next/link'
import { useRouter } from 'next/router'
import {
DropdownMenuCheckboxItem_Shadcn_,
DropdownMenuContent_Shadcn_,
DropdownMenuGroup_Shadcn_,
DropdownMenuItem_Shadcn_,
DropdownMenuLabel_Shadcn_,
DropdownMenuRadioGroup_Shadcn_,
DropdownMenuRadioItem_Shadcn_,
DropdownMenuSeparator_Shadcn_,
DropdownMenuShortcut_Shadcn_,
DropdownMenuSubContent_Shadcn_,
DropdownMenuSubTrigger_Shadcn_,
DropdownMenuSub_Shadcn_,
DropdownMenuTrigger_Shadcn_,
DropdownMenu_Shadcn_,
IconCheck,
IconUser,
useCommandMenu,
} from 'ui'
import { useTheme } from 'common'
import { useSignOut } from 'lib/auth'
import { useProfile } from 'lib/profile'
import { useState } from 'react'
const UserSettingsDropdown = () => {
const signOut = useSignOut()
const router = useRouter()
const [open, setOpen] = useState(false)
const { profile } = useProfile()
const { setIsOpen: setCommandMenuOpen } = useCommandMenu()
const { isDarkMode, toggleTheme } = useTheme()
const onClickLogout = async () => {
await signOut()
await router.push('/sign-in')
}
return (
<DropdownMenu_Shadcn_ open={open} onOpenChange={() => setOpen(!open)} modal={false}>
<DropdownMenuTrigger_Shadcn_ className="flex items-center justify-center border font-bold rounded-full h-7 w-7 text-scale-1100 bg-surface-100">
{profile?.first_name ? profile?.first_name?.[0] : <IconUser size={14} strokeWidth={2} />}
</DropdownMenuTrigger_Shadcn_>
<DropdownMenuContent_Shadcn_ align="end" className="w-60">
<DropdownMenuGroup_Shadcn_>
<div key="profile" className="px-2 py-1.5">
<p className="text-sm text-scale-1200">
{profile?.first_name} {profile?.last_name}
</p>
<p className="text-sm text-scale-1000 truncate">{profile?.primary_email}</p>
</div>
<DropdownMenuSeparator_Shadcn_ />
<Link passHref href="/account/me">
<DropdownMenuItem_Shadcn_
className="cursor-pointer"
onSelect={() => {
router.push('/account/me')
}}
onClick={() => setOpen(false)}
asChild
>
<a>Preferences</a>
</DropdownMenuItem_Shadcn_>
</Link>
<Link passHref href="/account/tokens">
<DropdownMenuItem_Shadcn_
className="cursor-pointer"
onSelect={() => {
router.push('/account/tokens')
}}
onClick={() => setOpen(false)}
asChild
>
<a>Access tokens</a>
</DropdownMenuItem_Shadcn_>
</Link>
<DropdownMenuSeparator_Shadcn_ />
<DropdownMenuItem_Shadcn_
className="cursor-pointer"
onSelect={() => {
setOpen(false)
setCommandMenuOpen(true)
}}
>
<span>Command menu</span>
<DropdownMenuShortcut_Shadcn_>⌘K</DropdownMenuShortcut_Shadcn_>
</DropdownMenuItem_Shadcn_>
<DropdownMenuSeparator_Shadcn_ />
<DropdownMenuLabel_Shadcn_>Theme</DropdownMenuLabel_Shadcn_>
<DropdownMenuRadioGroup_Shadcn_
value={isDarkMode ? 'dark' : 'light'}
onValueChange={(x) => {
const dark = x === 'dark'
toggleTheme(Boolean(dark))
}}
>
<DropdownMenuRadioItem_Shadcn_ value={'dark'}>Dark</DropdownMenuRadioItem_Shadcn_>
<DropdownMenuRadioItem_Shadcn_ value={'light'}>Light</DropdownMenuRadioItem_Shadcn_>
</DropdownMenuRadioGroup_Shadcn_>
<DropdownMenuSeparator_Shadcn_ />
<DropdownMenuItem_Shadcn_
className="cursor-pointer"
onClick={() => {
onClickLogout()
setOpen(false)
}}
>
<span>Log out</span>
</DropdownMenuItem_Shadcn_>
</DropdownMenuGroup_Shadcn_>
</DropdownMenuContent_Shadcn_>
</DropdownMenu_Shadcn_>
)
}
export default UserSettingsDropdown
@@ -14,7 +14,7 @@ export interface AuthLayoutProps {
}
const AuthLayout = ({ title, children }: PropsWithChildren<AuthLayoutProps>) => {
const { ui, meta } = useStore()
const { ui, meta, authConfig } = useStore()
const { isInitialized, isLoading, error } = meta.tables
const { ref: projectRef = 'default' } = useParams()
@@ -29,6 +29,7 @@ const AuthLayout = ({ title, children }: PropsWithChildren<AuthLayoutProps>) =>
meta.tables.load()
meta.roles.load()
meta.schemas.load()
authConfig.load()
}
}, [ui.selectedProjectRef])
@@ -28,6 +28,7 @@ const DatabaseLayout = ({ title, children }: PropsWithChildren<DatabaseLayoutPro
const isVaultEnabled = vaultExtension !== undefined && vaultExtension.installed_version !== null
const foreignDataWrappersEnabled = useFlag('foreignDataWrappers')
const pgNetExtensionExists = meta.extensions.byId('pg_net') !== undefined
const schemaVisualizerEnabled = useFlag('schemaVisualizer')
const isLoading = isSchemasLoading || (isVaultEnabled && isVaultLoading)
const [loaded, setLoaded] = useState<boolean>(isInitialized)
@@ -78,7 +79,11 @@ const DatabaseLayout = ({ title, children }: PropsWithChildren<DatabaseLayoutPro
productMenu={
<ProductMenu
page={page}
menu={generateDatabaseMenu(project, foreignDataWrappersEnabled, pgNetExtensionExists)}
menu={generateDatabaseMenu(project, {
foreignDataWrappersEnabled,
pgNetExtensionExists,
schemaVisualizerEnabled,
})}
/>
}
>
@@ -4,22 +4,30 @@ import { IS_PLATFORM } from 'lib/constants'
export const generateDatabaseMenu = (
project?: Project,
foreignDataWrappersEnabled: boolean = false,
pgNetExtensionExists: boolean = false
flags?: {
foreignDataWrappersEnabled: boolean
pgNetExtensionExists: boolean
schemaVisualizerEnabled: boolean
}
): ProductMenuGroup[] => {
const ref = project?.ref ?? 'default'
const { foreignDataWrappersEnabled, pgNetExtensionExists, schemaVisualizerEnabled } = flags || {}
return [
{
title: 'Database',
items: [
{ name: 'Tables', key: 'tables', url: `/project/${ref}/database/tables`, items: [] },
{
name: 'Schema Visualizer',
key: 'schemas',
url: `/project/${ref}/database/schemas`,
items: [],
},
...(!!schemaVisualizerEnabled
? [
{
name: 'Schema Visualizer',
key: 'schemas',
url: `/project/${ref}/database/schemas`,
items: [],
},
]
: []),
{
name: 'Triggers',
key: 'triggers',
@@ -45,7 +53,7 @@ export const generateDatabaseMenu = (
url: `/project/${ref}/database/replication`,
items: [],
},
...(pgNetExtensionExists
...(!!pgNetExtensionExists
? [
{
name: 'Webhooks',
@@ -55,7 +63,7 @@ export const generateDatabaseMenu = (
},
]
: []),
...(foreignDataWrappersEnabled
...(!!foreignDataWrappersEnabled
? [
{
name: 'Wrappers',
@@ -5,16 +5,23 @@ import { PropsWithChildren } from 'react'
import { Tabs } from 'ui'
import { AccountLayout } from './'
import { ScaffoldContainer, ScaffoldDivider, ScaffoldHeader, ScaffoldTitle } from './Scaffold'
import SettingsLayout from './SettingsLayout/SettingsLayout'
const OrganizationLayout = ({ children }: PropsWithChildren<{}>) => {
const selectedOrganization = useSelectedOrganization()
const router = useRouter()
const { slug } = useParams()
const id = router.asPath.split('/').at(-1)?.split('?')[0]?.split('#')[0]
const isOrgBilling = !!selectedOrganization?.subscription_id
const navLayoutV2 = useFlag('navigationLayoutV2')
const showOAuthApps = useFlag('oauthApps')
const showAuditLogs = useFlag('auditLogs')
const showIntegrationsV2 = useFlag('integrationsV2')
const isOrgBilling = !!selectedOrganization?.subscription_id
if (navLayoutV2) {
return <SettingsLayout>{children}</SettingsLayout>
}
return (
<AccountLayout
@@ -1,8 +1,19 @@
import { useState } from 'react'
import { Button, IconMessageCircle, Popover } from 'ui'
import {
Button,
IconMessageCircle,
Popover,
PopoverContent_Shadcn_,
PopoverTrigger_Shadcn_,
Popover_Shadcn_,
} from 'ui'
import FeedbackWidget from './FeedbackWidget'
const FeedbackDropdown = () => {
interface FeedbackDropdownProps {
alt?: boolean
}
const FeedbackDropdown = ({ alt = false }: FeedbackDropdownProps) => {
const [isOpen, setIsOpen] = useState(false)
const [feedback, setFeedback] = useState('')
const [screenshot, setScreenshot] = useState<string>()
@@ -18,16 +29,29 @@ const FeedbackDropdown = () => {
}
return (
<Popover
<Popover_Shadcn_
modal={false}
open={isOpen}
onOpenChange={(e) => {
setIsOpen(e)
if (!e) setScreenshot(undefined)
}}
size="content"
side="bottom"
align="end"
overlay={
>
<PopoverTrigger_Shadcn_ asChild>
<Button
asChild
onClick={onOpen}
type="default"
icon={
alt ? null : (
<IconMessageCircle size={16} strokeWidth={1.5} className="text-scale-900" />
)
}
>
<span className="hidden md:flex">Feedback</span>
</Button>
</PopoverTrigger_Shadcn_>
<PopoverContent_Shadcn_ side="bottom" align="end" className="w-full p-0">
<FeedbackWidget
onClose={onClose}
feedback={feedback}
@@ -35,17 +59,8 @@ const FeedbackDropdown = () => {
screenshot={screenshot}
setScreenshot={setScreenshot}
/>
}
>
<Button
asChild
onClick={onOpen}
type="default"
icon={<IconMessageCircle size={16} strokeWidth={1.5} className="text-scale-900" />}
>
<span className="hidden md:flex">Feedback</span>
</Button>
</Popover>
</PopoverContent_Shadcn_>
</Popover_Shadcn_>
)
}
@@ -2,7 +2,21 @@ import { toPng } from 'html-to-image'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { ChangeEvent, FC, useEffect, useRef, useState } from 'react'
import { Button, Dropdown, IconCamera, IconImage, IconUpload, IconX, Input, Popover } from 'ui'
import {
Button,
Dropdown,
DropdownMenuContent_Shadcn_,
DropdownMenuItem_Shadcn_,
DropdownMenuSeparator_Shadcn_,
DropdownMenuTrigger_Shadcn_,
DropdownMenu_Shadcn_,
IconCamera,
IconImage,
IconUpload,
IconX,
Input,
Popover,
} from 'ui'
import { useParams } from 'common'
import { useSendFeedbackMutation } from 'data/feedback/feedback-send'
@@ -125,13 +139,12 @@ const FeedbackWidget: FC<Props> = ({
value={feedback}
onChange={onFeedbackChange}
/>
<Popover.Separator />
<div className="w-full h-px bg-border" />
<div className="w-80 space-y-3 px-3 py-2 pb-4">
<div className="flex justify-between space-x-2">
<Button type="default" onClick={onClose} className="hover:border-gray-500">
<Button type="default" onClick={onClose}>
Cancel
</Button>
<div className="flex items-center space-x-2">
{screenshot !== undefined ? (
<div
@@ -143,7 +156,7 @@ const FeedbackWidget: FC<Props> = ({
}}
className="cursor-pointer rounded h-[26px] w-[30px] border border-scale-600 relative bg-cover bg-center bg-no-repeat"
>
<div
<button
className={[
'cursor-pointer rounded-full bg-red-900 h-3 w-3',
'flex items-center justify-center absolute -top-1 -right-1',
@@ -154,42 +167,43 @@ const FeedbackWidget: FC<Props> = ({
}}
>
<IconX size={8} strokeWidth={3} />
</div>
</button>
</div>
) : (
<Dropdown
className="feedback-dropdown"
size="small"
overlay={[
<Dropdown.Item
<DropdownMenu_Shadcn_>
<DropdownMenuTrigger_Shadcn_>
<Button
asChild
type="default"
disabled={isSavingScreenshot}
loading={isSavingScreenshot}
className="px-2 py-1.5"
icon={<IconImage size={14} />}
>
<span></span>
</Button>
</DropdownMenuTrigger_Shadcn_>
<DropdownMenuContent_Shadcn_ side="bottom" align="end">
<DropdownMenuItem_Shadcn_
className="flex gap-2"
key="upload-screenshot"
icon={<IconUpload size={14} />}
onClick={() => {
onSelect={() => {
if (uploadButtonRef.current) (uploadButtonRef.current as any).click()
}}
>
<IconUpload size={14} />
Upload screenshot
</Dropdown.Item>,
<Dropdown.Item
</DropdownMenuItem_Shadcn_>
<DropdownMenuItem_Shadcn_
className="flex gap-2"
key="capture-screenshot"
icon={<IconCamera size={14} />}
onClick={() => captureScreenshot()}
onSelect={() => captureScreenshot()}
>
<IconCamera size={14} />
Capture screenshot
</Dropdown.Item>,
]}
>
<Button
asChild
type="default"
disabled={isSavingScreenshot}
loading={isSavingScreenshot}
className="px-2 py-1.5"
icon={<IconImage size={14} />}
>
<span></span>
</Button>
</Dropdown>
</DropdownMenuItem_Shadcn_>
</DropdownMenuContent_Shadcn_>
</DropdownMenu_Shadcn_>
)}
<input
type="file"
@@ -1,148 +1,172 @@
import * as Tooltip from '@radix-ui/react-tooltip'
import Image from 'next/image'
import Link from 'next/link'
import { useRouter } from 'next/router'
import SVG from 'react-inlinesvg'
import {
Button,
IconActivity,
IconBookOpen,
IconHelpCircle,
IconMail,
IconMessageCircle,
Popover,
IconBookOpen,
IconActivity,
IconTool,
Popover,
PopoverContent_Shadcn_,
PopoverTrigger_Shadcn_,
Popover_Shadcn_,
} from 'ui'
import { useRouter } from 'next/router'
import { FC } from 'react'
import Link from 'next/link'
import Image from 'next/image'
import SVG from 'react-inlinesvg'
interface HelpPopoverProps {
alt?: boolean
}
interface Props {}
const HelpPopover: FC<Props> = () => {
const HelpPopover = ({ alt = false }: HelpPopoverProps) => {
const router = useRouter()
const projectRef = router.query.ref
const supportUrl = `/support/new${projectRef ? `?ref=${projectRef}` : ''}`
return (
<Popover
size="content"
align="end"
side="bottom"
sideOffset={8}
overlay={
<div className="my-4 w-[400px] space-y-4">
<div className="my-5 space-y-4 px-5">
<h5 className="text-scale-1200">Need help with your project?</h5>
<p className="text-sm text-scale-900">
For issues with your project hosted on supabase.com, or other inquiries about our
hosted services.
</p>
<div className="space-x-1">
<Link passHref href="https://supabase.com/docs/guides/platform/troubleshooting">
<Button asChild type="default" icon={<IconTool />}>
<a target="_blank" rel="noreferrer">
Troubleshooting
</a>
</Button>
</Link>
<Link passHref href="https://supabase.com/docs/">
<Button asChild type="text" size="tiny" icon={<IconBookOpen />}>
<a target="_blank" rel="noreferrer">
Docs
</a>
</Button>
</Link>
<Link passHref href="https://status.supabase.com/">
<Button asChild type="text" size="tiny" icon={<IconActivity />}>
<a target="_blank" rel="noreferrer">
Supabase Status
</a>
</Button>
</Link>
<Popover_Shadcn_>
<Tooltip.Root delayDuration={0}>
<PopoverTrigger_Shadcn_ asChild>
<Tooltip.Trigger asChild>
<div className="relative flex items-center">
<Button
type={alt ? 'text' : 'default'}
className={alt ? 'px-1' : ''}
icon={
<IconHelpCircle
size={alt ? 18 : 16}
strokeWidth={1.5}
className={alt ? 'text-scale-1100' : 'text-scale-900'}
/>
}
>
{!alt && <span>Help</span>}
</Button>
</div>
</Tooltip.Trigger>
</PopoverTrigger_Shadcn_>
{alt ? (
<Tooltip.Portal>
<Tooltip.Content side="bottom">
<Tooltip.Arrow className="radix-tooltip-arrow" />
<div
className={[
'rounded bg-scale-100 py-1 px-2 leading-none shadow',
'space-y-2 border border-scale-200',
].join(' ')}
>
<p className="text-xs text-scale-1200">Help</p>
</div>
</Tooltip.Content>
</Tooltip.Portal>
) : null}
</Tooltip.Root>
<PopoverContent_Shadcn_ className="w-[400px] space-y-4 p-0 py-5" align="end" side="bottom">
<div className="mb-5 space-y-4 px-5">
<h5 className="text-scale-1200">Need help with your project?</h5>
<p className="text-sm text-scale-900">
For issues with your project hosted on supabase.com, or other inquiries about our hosted
services.
</p>
<div className="space-x-1">
<Link passHref href="https://supabase.com/docs/guides/platform/troubleshooting">
<Button asChild type="default" icon={<IconTool />}>
<a target="_blank" rel="noreferrer">
Troubleshooting
</a>
</Button>
</Link>
<Link passHref href="https://supabase.com/docs/">
<Button asChild type="text" size="tiny" icon={<IconBookOpen />}>
<a target="_blank" rel="noreferrer">
Docs
</a>
</Button>
</Link>
<Link passHref href="https://status.supabase.com/">
<Button asChild type="text" size="tiny" icon={<IconActivity />}>
<a target="_blank" rel="noreferrer">
Supabase Status
</a>
</Button>
</Link>
</div>
<p className="text-sm text-scale-900">
Expected response time is based on your billing plan. Pro and Pay as You Go plans are
prioritized.
</p>
<div>
<Link passHref href={supportUrl}>
<Button asChild type="default" icon={<IconMail />}>
<a>Contact Support</a>
</Button>
</Link>
</div>
</div>
<Popover.Separator />
<div className="mb-4 space-y-2">
<div className="mb-4 px-5">
<h5 className={'mb-2'}>Reach out to the community</h5>
<p className="text-sm text-scale-900">
Expected response time is based on your billing plan. Pro and Pay as You Go plans are
prioritized.
For other support, including questions on our client libraries, advice, or best
practices.
</p>
<div>
<Link passHref href={supportUrl}>
<Button asChild type="default" icon={<IconMail />}>
<a>Contact Support</a>
</div>
<div className="px-5">
<div
className="relative space-y-2 overflow-hidden rounded px-5 py-4 pb-12 shadow-md"
style={{ background: '#404EED' }}
>
<a
href="https://discord.supabase.com"
target="_blank"
rel="noreferrer"
className="dark block cursor-pointer"
>
<Image
className="absolute left-0 top-0 opacity-50"
src={`${router.basePath}/img/support/discord-bg-small.jpg`}
layout="fill"
objectFit="cover"
alt="discord illustration header"
/>
<Button
type="secondary"
icon={<SVG src={`${router.basePath}/img/discord-icon.svg`} className="h-4 w-4" />}
>
<span style={{ color: '#404EED' }}>Join Discord server</span>
</Button>
</Link>
</a>
</div>
</div>
<Popover.Separator />
<div className="mb-4 space-y-2">
<div className="mb-4 px-5">
<h5 className={'mb-2'}>Reach out to the community</h5>
<p className="text-sm text-scale-900">
For other support, including questions on our client libraries, advice, or best
practices.
</p>
</div>
<div className="px-5">
<div
className="relative space-y-2 overflow-hidden rounded px-5 py-4 pb-12 shadow-md"
style={{ background: '#404EED' }}
<div className="px-5">
<div className="relative space-y-2 overflow-hidden rounded px-5 py-4 pb-12 shadow-md">
<a
href="https://github.com/supabase/supabase/discussions"
target="_blank"
rel="noreferrer"
className="block cursor-pointer"
>
<a
href="https://discord.supabase.com"
target="_blank"
rel="noreferrer"
className="dark block cursor-pointer"
>
<Image
className="absolute left-0 top-0 opacity-50"
src={`${router.basePath}/img/support/discord-bg-small.jpg`}
layout="fill"
objectFit="cover"
alt="discord illustration header"
/>
<Button
type="secondary"
icon={
<SVG src={`${router.basePath}/img/discord-icon.svg`} className="h-4 w-4" />
}
>
<span style={{ color: '#404EED' }}>Join Discord server</span>
</Button>
</a>
</div>
</div>
<div className="px-5">
<div className="relative space-y-2 overflow-hidden rounded px-5 py-4 pb-12 shadow-md">
<a
href="https://github.com/supabase/supabase/discussions"
target="_blank"
rel="noreferrer"
className="block cursor-pointer"
>
<Image
className="absolute left-0 top-0 opacity-50"
src={`${router.basePath}/img/support/github-bg.jpg?v-1`}
layout="fill"
objectFit="cover"
alt="discord illustration header"
/>
<Button type="secondary" icon={<IconMessageCircle />}>
GitHub Discussions
</Button>
</a>
</div>
<Image
className="absolute left-0 top-0 opacity-50"
src={`${router.basePath}/img/support/github-bg.jpg?v-1`}
layout="fill"
objectFit="cover"
alt="discord illustration header"
/>
<Button type="secondary" icon={<IconMessageCircle />}>
GitHub Discussions
</Button>
</a>
</div>
</div>
</div>
}
>
<Button
asChild
type="default"
icon={<IconHelpCircle size={16} strokeWidth={1.5} className="text-scale-900" />}
>
<span>Help</span>
</Button>
</Popover>
</PopoverContent_Shadcn_>
</Popover_Shadcn_>
)
}
@@ -12,15 +12,19 @@ import { Fragment, useState } from 'react'
import ConfirmModal from 'components/ui/Dialogs/ConfirmDialog'
import { useNotificationsQuery } from 'data/notifications/notifications-query'
import { getProjectDetail } from 'data/projects/project-detail-query'
import { invalidateProjectsQuery, setProjectPostgrestStatus } from 'data/projects/projects-query'
import { setProjectPostgrestStatus } from 'data/projects/projects-query'
import { useStore } from 'hooks'
import { delete_, patch, post } from 'lib/common/fetch'
import { API_URL } from 'lib/constants'
import { Project } from 'types'
import { Alert, Button, IconArrowRight, IconBell, Popover } from 'ui'
import { Alert, Button, IconArrowRight, IconBell, IconInbox, Popover } from 'ui'
import NotificationRow from './NotificationRow'
const NotificationsPopover = () => {
interface NotificationsPopoverProps {
alt?: boolean
}
const NotificationsPopover = ({ alt = false }: NotificationsPopoverProps) => {
const queryClient = useQueryClient()
const router = useRouter()
const { meta, ui } = useStore()
@@ -33,11 +37,12 @@ const NotificationsPopover = () => {
const [targetNotification, setTargetNotification] = useState<Notification>()
if (!notifications) return <></>
if (!notifications || !Array.isArray(notifications)) return null
const hasNewNotifications = notifications?.some(
const newNotifications = notifications?.filter(
(notification) => notification.notification_status === NotificationStatus.New
)
const hasNewNotifications = newNotifications.length > 0
const onOpenChange = async (open: boolean) => {
// TODO(alaister): move this to a mutation
@@ -232,21 +237,47 @@ const NotificationsPopover = () => {
>
<Tooltip.Root delayDuration={0}>
<Tooltip.Trigger asChild>
<div className="relative flex">
<div className="relative flex items-center">
{hasNewNotifications && (
<>
{alt ? null : (
<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>
)}
</>
)}
<Button
asChild
id="notification-button"
type="default"
icon={<IconBell size={16} strokeWidth={1.5} className="text-scale-1200" />}
type={alt ? 'text' : 'default'}
className={alt ? 'px-1' : ''}
icon={
hasNewNotifications ? (
<div className="-mr-3.5 z-10 h-4 w-4 flex items-center justify-center rounded-full bg-white">
<p className="text-xs text-scale-100">{newNotifications.length}</p>
</div>
) : alt ? (
<IconInbox size={18} strokeWidth={1.5} className="text-scale-1100" />
) : (
<IconBell size={16} strokeWidth={1.5} className="text-scale-1200" />
)
}
iconRight={
hasNewNotifications ? (
<>
{alt ? (
<IconInbox size={18} strokeWidth={1.5} className="text-scale-1100" />
) : (
<IconBell size={16} strokeWidth={1.5} className="text-scale-1200" />
)}
</>
) : null
}
>
<span></span>
</Button>
{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>
</Tooltip.Trigger>
<Tooltip.Portal>
@@ -1,13 +1,15 @@
import Link from 'next/link'
import { useRouter } from 'next/router'
import { ParsedUrlQuery } from 'querystring'
import { Button, Dropdown, IconPlus, Popover } from 'ui'
import { Badge, Button, Dropdown, IconCode, IconPlus, Popover } from 'ui'
import { useProjectsQuery } from 'data/projects/projects-query'
import { useSelectedOrganization, useSelectedProject } from 'hooks'
import { IS_PLATFORM, PROJECT_STATUS } from 'lib/constants'
import ShimmeringLoader from 'components/ui/ShimmeringLoader'
import { Organization, Project } from 'types'
import { useOrganizationsQuery } from 'data/organizations/organizations-query'
import { useProjectSubscriptionV2Query } from 'data/subscriptions/project-subscription-v2-query'
// [Fran] the idea is to let users change projects without losing the current page,
// but at the same time we need to redirect correctly between urls that might be
@@ -71,13 +73,23 @@ const ProjectLink = ({
)
}
const ProjectDropdown = () => {
const ProjectDropdown = ({ alt }: { alt?: boolean }) => {
const selectedProject = useSelectedProject()
const selectedOrganization = useSelectedOrganization()
const { data: allProjects } = useProjectsQuery()
const { data: allProjects, isLoading: isLoadingProjects } = useProjectsQuery()
const { data: allOrganizations } = useOrganizationsQuery()
const selectedOrganizationSlug = selectedOrganization?.slug
const isOrgBilling = !!selectedOrganization?.subscription_id
const { data: subscription, isSuccess } = useProjectSubscriptionV2Query(
{ projectRef: selectedProject?.ref },
{ enabled: alt && !isOrgBilling }
)
if (isLoadingProjects && alt) {
return <ShimmeringLoader className="w-[90px]" />
}
return IS_PLATFORM ? (
<Dropdown
side="bottom"
@@ -100,13 +112,23 @@ const ProjectDropdown = () => {
</>
}
>
<Button asChild type="text" size="tiny" className="my-1">
<span>{selectedProject?.name}</span>
<Button
type="text"
iconRight={
alt ? <IconCode className="text-scale-1100 rotate-90" strokeWidth={2} size={12} /> : null
}
>
<span className="text-sm">{selectedProject?.name}</span>
{alt && isSuccess && !isOrgBilling && (
<Badge color="slate" className="ml-2">
{subscription?.plan.name}
</Badge>
)}
</Button>
</Dropdown>
) : (
<Button asChild type="text" size="tiny">
<span>{selectedProject?.name}</span>
<Button type="text">
<span className="text-sm">{selectedProject?.name}</span>
</Button>
)
}
@@ -2,7 +2,6 @@ import * as Tooltip from '@radix-ui/react-tooltip'
import { isUndefined } from 'lodash'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { FC } from 'react'
import { useTheme } from 'common'
import { useParams } from 'common/hooks'
@@ -27,15 +26,13 @@ import {
} from './NavigationBar.utils'
import NavigationIconButton from './NavigationIconButton'
interface Props {}
const NavigationBar: FC<Props> = ({}) => {
const NavigationBar = ({}) => {
const router = useRouter()
const { isDarkMode, toggleTheme } = useTheme()
const { ref: projectRef } = useParams()
const { project } = useProjectContext()
const ongoingIncident = useFlag('ongoingIncident')
const navLayoutV2 = useFlag('navigationLayoutV2')
const activeRoute = router.pathname.split('/')[3]
const toolRoutes = generateToolRoutes(projectRef, project)
@@ -46,22 +43,23 @@ const NavigationBar: FC<Props> = ({}) => {
const { setIsOpen } = useCommandMenu()
return (
<div
style={{ height: ongoingIncident ? 'calc(100vh - 44px)' : '100vh' }}
className={[
'flex w-14 flex-col justify-between overflow-y-hidden p-2',
'border-r bg-body border-scale-500',
].join(' ')}
>
<ul className="flex flex-col space-y-2">
<Link href="/projects">
<a className="block">
<img
src={`${router.basePath}/img/supabase-logo.svg`}
alt="Supabase"
className="mx-auto h-[40px] w-6 cursor-pointer rounded"
/>
</a>
</Link>
{!navLayoutV2 && (
<Link href="/projects">
<a className="block">
<img
src={`${router.basePath}/img/supabase-logo.svg`}
alt="Supabase"
className="mx-auto h-[40px] w-6 cursor-pointer rounded"
/>
</a>
</Link>
)}
<NavigationIconButton
isActive={isUndefined(activeRoute) && !isUndefined(router.query.ref)}
route={{
@@ -71,7 +69,7 @@ const NavigationBar: FC<Props> = ({}) => {
link: `/project/${projectRef}`,
}}
/>
<div className="bg-scale-500 h-px w-full"></div>
<div className="bg-scale-500 h-px w-full" />
{toolRoutes.map((route) => (
<NavigationIconButton
key={route.key}
@@ -97,77 +95,82 @@ const NavigationBar: FC<Props> = ({}) => {
/>
))}
</ul>
<ul className="flex flex-col space-y-4 items-center">
{IS_PLATFORM && showCmdkHelper && (
<Tooltip.Root delayDuration={0}>
<Tooltip.Trigger asChild>
<Button
type="text"
size="tiny"
onClick={() => setIsOpen(true)}
className="border-none"
>
<div className="py-1">
<IconSearch size={18} strokeWidth={2} className="text-scale-900" />
</div>
</Button>
</Tooltip.Trigger>
<Tooltip.Portal>
<Tooltip.Content side="right">
<Tooltip.Arrow className="radix-tooltip-arrow" />
<div
className={[
'rounded py-1 px-2 leading-none shadow',
'border border-scale-200 flex items-center space-x-1',
].join(' ')}
{!navLayoutV2 && (
<ul className="flex flex-col space-y-4 items-center">
{IS_PLATFORM && showCmdkHelper && (
<Tooltip.Root delayDuration={0}>
<Tooltip.Trigger asChild>
<Button
type="text"
size="tiny"
onClick={() => setIsOpen(true)}
className="border-none"
>
{os === 'macos' ? (
<IconCommand size={11.5} strokeWidth={1.5} className="text-scale-1200" />
) : (
<p className="text-xs">CTRL</p>
)}
<p className="text-xs">K</p>
</div>
</Tooltip.Content>
</Tooltip.Portal>
</Tooltip.Root>
)}
<Dropdown
side="right"
align="start"
overlay={
<>
{IS_PLATFORM && (
<>
<Link href="/account/me">
<Dropdown.Item key="header" icon={<IconSettings size={14} strokeWidth={1.5} />}>
Account Preferences
</Dropdown.Item>
</Link>
<Dropdown.Separator />
</>
)}
<Dropdown.Label>Theme</Dropdown.Label>
<Dropdown.RadioGroup
key="theme"
value={isDarkMode ? 'dark' : 'light'}
onChange={(e: any) => toggleTheme(e === 'dark')}
>
{/* [Joshen] Removing system default for now, needs to be supported in useTheme from common packages */}
{/* <Dropdown.Radio value="system">System default</Dropdown.Radio> */}
<Dropdown.Radio value="dark">Dark</Dropdown.Radio>
<Dropdown.Radio value="light">Light</Dropdown.Radio>
</Dropdown.RadioGroup>
</>
}
>
<Button asChild type="text" size="tiny">
<span className="py-1 h-10 border-none">
<IconUser size={18} strokeWidth={2} className="text-scale-900" />
</span>
</Button>
</Dropdown>
</ul>
<div className="py-1">
<IconSearch size={18} strokeWidth={2} className="text-scale-900" />
</div>
</Button>
</Tooltip.Trigger>
<Tooltip.Portal>
<Tooltip.Content side="right">
<Tooltip.Arrow className="radix-tooltip-arrow" />
<div
className={[
'rounded py-1 px-2 leading-none shadow',
'border border-scale-200 flex items-center space-x-1',
].join(' ')}
>
{os === 'macos' ? (
<IconCommand size={11.5} strokeWidth={1.5} className="text-scale-1200" />
) : (
<p className="text-xs">CTRL</p>
)}
<p className="text-xs">K</p>
</div>
</Tooltip.Content>
</Tooltip.Portal>
</Tooltip.Root>
)}
<Dropdown
side="right"
align="start"
overlay={
<>
{IS_PLATFORM && (
<>
<Link href="/account/me">
<Dropdown.Item
key="header"
icon={<IconSettings size={14} strokeWidth={1.5} />}
>
Account Preferences
</Dropdown.Item>
</Link>
<Dropdown.Separator />
</>
)}
<Dropdown.Label>Theme</Dropdown.Label>
<Dropdown.RadioGroup
key="theme"
value={isDarkMode ? 'dark' : 'light'}
onChange={(e: any) => toggleTheme(e === 'dark')}
>
{/* [Joshen] Removing system default for now, needs to be supported in useTheme from common packages */}
{/* <Dropdown.Radio value="system">System default</Dropdown.Radio> */}
<Dropdown.Radio value="dark">Dark</Dropdown.Radio>
<Dropdown.Radio value="light">Light</Dropdown.Radio>
</Dropdown.RadioGroup>
</>
}
>
<Button asChild type="text" size="tiny">
<span className="py-1 h-10 border-none">
<IconUser size={18} strokeWidth={2} className="text-scale-900" />
</span>
</Button>
</Dropdown>
</ul>
)}
</div>
)
}
@@ -1,18 +1,12 @@
import { FC, ReactNode } from 'react'
import { useFlag } from 'hooks'
import { PropsWithChildren } from 'react'
interface Props {
interface ProductMenuBarProps {
title: string
children: ReactNode
}
const ProductMenuBar: FC<Props> = ({ title, children }) => {
const ongoingIncident = useFlag('ongoingIncident')
const maxHeight = ongoingIncident ? 'calc(100vh - 44px)' : '100vh'
const ProductMenuBar = ({ title, children }: PropsWithChildren<ProductMenuBarProps>) => {
return (
<div
style={{ height: maxHeight, maxHeight }}
className={[
'hide-scrollbar flex w-64 flex-col border-r', // Layout
'bg-scale-200', // Light mode
@@ -25,7 +19,9 @@ const ProductMenuBar: FC<Props> = ({ title, children }) => {
>
<h4 className="text-lg">{title}</h4>
</div>
<div className="flex-grow overflow-y-auto">{children}</div>
<div className="flex-grow overflow-y-auto" style={{ maxHeight: 'calc(100vh - 96px)' }}>
{children}
</div>
</div>
)
}
@@ -16,6 +16,7 @@ import { ProjectContextProvider } from './ProjectContext'
import ProjectPausedState from './ProjectPausedState'
import RestoringState from './RestoringState'
import UpgradingState from './UpgradingState'
import AppLayout from '../AppLayout/AppLayout'
// [Joshen] This is temporary while we unblock users from managing their project
// if their project is not responding well for any reason. Eventually needs a bit of an overhaul
@@ -59,61 +60,61 @@ const ProjectLayout = ({
}: PropsWithChildren<ProjectLayoutProps>) => {
const router = useRouter()
const { ref: projectRef } = useParams()
const ongoingIncident = useFlag('ongoingIncident')
const selectedOrganization = useSelectedOrganization()
const selectedProject = useSelectedProject()
const projectName = selectedProject?.name
const organizationName = selectedOrganization?.name
const navLayoutV2 = useFlag('navigationLayoutV2')
const isPaused = selectedProject?.status === PROJECT_STATUS.INACTIVE
const ignorePausedState =
router.pathname === '/project/[ref]' || router.pathname.includes('/project/[ref]/settings')
const showPausedState = isPaused && !ignorePausedState
return (
<ProjectContextProvider projectRef={projectRef}>
<Head>
<title>
{title
? `${title} | Supabase`
: selectedTable
? `${selectedTable} | ${projectName} | ${organizationName} | Supabase`
: projectName
? `${projectName} | ${organizationName} | Supabase`
: organizationName
? `${organizationName} | Supabase`
: 'Supabase'}
</title>
<meta name="description" content="Supabase Studio" />
</Head>
<div className="flex h-full">
{/* Left-most navigation side bar to access products */}
{!hideIconBar && <NavigationBar />}
<AppLayout>
<ProjectContextProvider projectRef={projectRef}>
<Head>
<title>
{title
? `${title} | Supabase`
: selectedTable
? `${selectedTable} | ${projectName} | ${organizationName} | Supabase`
: projectName
? `${projectName} | ${organizationName} | Supabase`
: organizationName
? `${organizationName} | Supabase`
: 'Supabase'}
</title>
<meta name="description" content="Supabase Studio" />
</Head>
<div className="flex h-full">
{/* Left-most navigation side bar to access products */}
{!hideIconBar && <NavigationBar />}
{/* Product menu bar */}
{!showPausedState && (
<MenuBarWrapper isLoading={isLoading} productMenu={productMenu}>
<ProductMenuBar title={product}>{productMenu}</ProductMenuBar>
</MenuBarWrapper>
)}
<main
className="flex flex-col flex-1 w-full overflow-x-hidden"
style={{ height: ongoingIncident ? 'calc(100vh - 44px)' : '100vh' }}
>
{!hideHeader && <LayoutHeader />}
{showPausedState ? (
<div className="mx-auto my-16 w-full h-full max-w-7xl flex items-center">
<div className="w-full">
<ProjectPausedState product={product} />
</div>
</div>
) : (
<ContentWrapper isLoading={isLoading}>{children}</ContentWrapper>
{/* Product menu bar */}
{!showPausedState && (
<MenuBarWrapper isLoading={isLoading} productMenu={productMenu}>
<ProductMenuBar title={product}>{productMenu}</ProductMenuBar>
</MenuBarWrapper>
)}
</main>
</div>
</ProjectContextProvider>
<main className="flex flex-col flex-1 w-full overflow-x-hidden">
{!navLayoutV2 && !hideHeader && <LayoutHeader />}
{showPausedState ? (
<div className="mx-auto my-16 w-full h-full max-w-7xl flex items-center">
<div className="w-full">
<ProjectPausedState product={product} />
</div>
</div>
) : (
<ContentWrapper isLoading={isLoading}>{children}</ContentWrapper>
)}
</main>
</div>
</ProjectContextProvider>
</AppLayout>
)
}
@@ -210,44 +211,44 @@ export const ProjectLayoutNonBlocking = ({
const selectedProject = useSelectedProject()
const router = useRouter()
const { ref: projectRef } = useParams()
const ongoingIncident = useFlag('ongoingIncident')
const isPaused = selectedProject?.status === PROJECT_STATUS.INACTIVE
const ignorePausedState =
router.pathname === '/project/[ref]' || router.pathname.includes('/project/[ref]/settings')
const showPausedState = isPaused && !ignorePausedState
const navLayoutV2 = useFlag('navigationLayoutV2')
return (
<ProjectContextProvider projectRef={projectRef}>
<Head>
<title>{title ? `${title} | Supabase` : 'Supabase'}</title>
<meta name="description" content="Supabase Studio" />
<link rel="icon" href="/favicon.ico" />
</Head>
<div className="flex h-full">
{/* Left-most navigation side bar to access products */}
{!hideIconBar && <NavigationBar />}
<AppLayout>
<ProjectContextProvider projectRef={projectRef}>
<Head>
<title>{title ? `${title} | Supabase` : 'Supabase'}</title>
<meta name="description" content="Supabase Studio" />
<link rel="icon" href="/favicon.ico" />
</Head>
<div className="flex h-full">
{/* Left-most navigation side bar to access products */}
{!hideIconBar && <NavigationBar />}
{/* Product menu bar */}
{productMenu && !showPausedState && (
<ProductMenuBar title={product}>{productMenu}</ProductMenuBar>
)}
<main
className="flex w-full flex-1 flex-col overflow-x-hidden"
style={{ height: ongoingIncident ? 'calc(100vh - 44px)' : '100vh' }}
>
{!hideHeader && <LayoutHeader />}
{showPausedState ? (
<div className="mx-auto my-16 w-full h-full max-w-7xl flex items-center">
<div className="w-full">
<ProjectPausedState product={product} />
</div>
</div>
) : (
children
{/* Product menu bar */}
{productMenu && !showPausedState && (
<ProductMenuBar title={product}>{productMenu}</ProductMenuBar>
)}
</main>
</div>
</ProjectContextProvider>
<main className="flex w-full flex-1 flex-col overflow-x-hidden">
{!navLayoutV2 && !hideHeader && <LayoutHeader />}
{showPausedState ? (
<div className="mx-auto my-16 w-full h-full max-w-7xl flex items-center">
<div className="w-full">
<ProjectPausedState product={product} />
</div>
</div>
) : (
children
)}
</main>
</div>
</ProjectContextProvider>
</AppLayout>
)
}
@@ -0,0 +1,51 @@
import { useFlag, useSelectedOrganization, useSelectedProject, useStore, withAuth } from 'hooks'
import { observer } from 'mobx-react-lite'
import { useRouter } from 'next/router'
import { PropsWithChildren, useEffect } from 'react'
import { generateSettingsMenu } from './SettingsMenu.utils'
import ProductMenu from 'components/ui/ProductMenu'
import ProjectLayout from '..'
import { useParams } from 'common'
interface SettingsLayoutProps {
title?: string
}
const SettingsLayout = ({ title, children }: PropsWithChildren<SettingsLayoutProps>) => {
const router = useRouter()
const { ref } = useParams()
const { ui, meta } = useStore()
const project = useSelectedProject()
const organization = useSelectedOrganization()
const isOrgBilling = !!organization?.subscription_id
// billing pages live under /billing/invoices and /billing/subscription, etc
// so we need to pass the [5]th part of the url to the menu
const page = router.pathname.includes('billing')
? router.pathname.split('/')[5]
: router.pathname.split('/')[4]
const isVaultEnabled = useFlag('vaultExtension')
const menuRoutes = generateSettingsMenu(ref, project, isVaultEnabled, isOrgBilling)
useEffect(() => {
if (ui.selectedProjectRef) {
meta.extensions.load()
}
}, [ui.selectedProjectRef])
return (
<ProjectLayout
title={title || 'Settings'}
product="Settings"
productMenu={<ProductMenu page={page} menu={menuRoutes} />}
>
<main style={{ maxHeight: '100vh' }} className="flex-1 overflow-y-auto">
{children}
</main>
</ProjectLayout>
)
}
export default withAuth(observer(SettingsLayout))
+1 -1
View File
@@ -53,7 +53,7 @@ const ScaffoldColumn = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTM
<div
ref={ref}
{...props}
className={cn('flex flex-col gap-3 py-6', maxWidthClassesColumn, className)}
className={cn('flex flex-col gap-3', maxWidthClassesColumn, className)}
/>
)
}
@@ -0,0 +1,28 @@
import { useRouter } from 'next/router'
import SettingsMenuItem from './SettingsMenuItem'
const AccountSettingsMenu = () => {
const router = useRouter()
const accountSettings = [
{ label: 'Preferences', pathname: `/account/me` },
{ label: 'Access Tokens', pathname: `/account/tokens` },
]
return (
<div className="space-y-10">
<div className="space-y-2">
<p className="text-sm font-medium">Account Settings</p>
{accountSettings.map((link) => (
<SettingsMenuItem
key={link.label}
label={link.label}
href={link.pathname}
isActive={link.pathname === router.pathname}
/>
))}
</div>
</div>
)
}
export default AccountSettingsMenu
@@ -0,0 +1,59 @@
import { useRouter } from 'next/router'
import { useSelectedOrganization } from 'hooks'
import SettingsMenuItem from './SettingsMenuItem'
const OrganizationSettingsMenu = () => {
const router = useRouter()
const organization = useSelectedOrganization()
const organizationSettings = [
{ label: 'General', pathname: `/org/[slug]/general` },
{ label: 'Members', pathname: `/org/[slug]/team` },
{ label: 'Integrations', pathname: `/org/[slug]/integrations` },
{ label: 'Billing', pathname: `/org/[slug]/billing` },
{ label: 'Usage', pathname: `/org/[slug]/usage` },
{ label: 'Invoices', pathname: `/org/[slug]/invoices` },
{ label: 'OAuth Apps', pathname: `/org/[slug]/apps` },
{ label: 'Audit Logs', pathname: `/org/[slug]/audit` },
]
// const { data: allProjects } = useProjectsQuery()
// const projects = allProjects?.filter((project) => project.organization_id === organization?.id)
return (
<div className="space-y-10">
<div className="space-y-2">
<p className="text-sm font-medium">Organization</p>
{organizationSettings.map((link) => (
<SettingsMenuItem
key={link.label}
label={link.label}
href={link.pathname.replace('[slug]', organization?.slug ?? '')}
isActive={link.pathname === router.pathname}
/>
))}
</div>
{/* [Joshen] Hiding below until we figure out a better UX for consolidating settings on projects */}
{/* <div className="space-y-2">
<p className="text-sm font-medium">Projects</p>
<Accordion_Shadcn_ type="single" collapsible className="w-full">
{projects?.map((project) => (
<ProjectSettingsMenuItem key={project.ref} project={project} />
))}
</Accordion_Shadcn_>
</div>
<div>
<Link href={`/new/${organization?.slug}`}>
<a>
<Button type="default" size="small">
Create new project
</Button>
</a>
</Link>
</div> */}
</div>
)
}
export default OrganizationSettingsMenu
@@ -0,0 +1,71 @@
import { useRouter } from 'next/router'
import {
AccordionContent_Shadcn_,
AccordionItem_Shadcn_,
AccordionTrigger_Shadcn_,
IconChevronRight,
} from 'ui'
import { useSelectedOrganization } from 'hooks'
import { Project } from 'types'
import SettingsMenuItem from './SettingsMenuItem'
interface ProjectSettingsMenuItemProps {
project: Project
}
const ProjectSettingsMenuItem = ({ project }: ProjectSettingsMenuItemProps) => {
const router = useRouter()
const organization = useSelectedOrganization()
const isOrgBilling = !!organization?.subscription_id
// [Joshen] Links need to be updated once we start implementing these
const projectSettings = isOrgBilling
? [
{ label: 'General', pathname: `/project/[ref]/settings/general` },
{ label: 'Infrastructure', pathname: `/project/[ref]/settings/infrastructure` },
{ label: 'Add Ons', pathname: `/project/[ref]/settings/addons` },
]
: [
{ label: 'General', pathname: `/project/[ref]/settings/general` },
{ label: 'Subscription', pathname: `/project/[ref]/settings/billing/subscription` },
{ label: 'Usage', pathname: `/project/[ref]/settings/billing/usage` },
{ label: 'Invoices', pathname: `/project/[ref]/settings/billing/invoices` },
]
return (
<AccordionItem_Shadcn_ value={project.ref} className="!border-none">
<AccordionTrigger_Shadcn_
hideIcon
className="py-1.5 hover:!no-underline [&[data-state=open]>div>svg]:!rotate-90 [&[data-state=open]>div>p]:text-scale-1200"
>
<div className="flex items-center space-x-2">
<IconChevronRight
className="h-4 w-4 transition-transform duration-200"
strokeWidth={1.5}
/>
<p className="text-scale-1100 text-sm font-normal transition duration-200">
{project.name}
</p>
</div>
</AccordionTrigger_Shadcn_>
<AccordionContent_Shadcn_>
<div className="space-y-2 pl-6 mt-1">
{projectSettings.map((link) => {
const href = link.pathname.replace('[ref]', project.ref)
return (
<SettingsMenuItem
key={link.label}
label={link.label}
href={href}
isActive={href === router.asPath}
/>
)
})}
</div>
</AccordionContent_Shadcn_>
</AccordionItem_Shadcn_>
)
}
export default ProjectSettingsMenuItem
@@ -1,51 +1,30 @@
import { useFlag, useSelectedOrganization, useSelectedProject, useStore, withAuth } from 'hooks'
import { observer } from 'mobx-react-lite'
import { useRouter } from 'next/router'
import { PropsWithChildren, useEffect } from 'react'
import { generateSettingsMenu } from './SettingsMenu.utils'
import { PropsWithChildren } from 'react'
import AppLayout from '../AppLayout/AppLayout'
import { useParams } from 'common'
import ProductMenu from 'components/ui/ProductMenu'
import ProjectLayout from '../'
interface SettingsLayoutProps {
title?: string
}
const SettingsLayout = ({ title, children }: PropsWithChildren<SettingsLayoutProps>) => {
const { ref } = useParams()
const { ui, meta } = useStore()
const project = useSelectedProject()
const organization = useSelectedOrganization()
const isOrgBilling = !!organization?.subscription_id
import OrganizationSettingsMenu from './OrganizationSettingsMenu'
import AccountSettingsMenu from './AccountSettingsMenu'
import { useRouter } from 'next/router'
const SettingsLayout = ({ children }: PropsWithChildren<{}>) => {
const router = useRouter()
// billing pages live under /billing/invoices and /billing/subscription, etc
// so we need to pass the [5]th part of the url to the menu
const page = router.pathname.includes('billing')
? router.pathname.split('/')[5]
: router.pathname.split('/')[4]
const isVaultEnabled = useFlag('vaultExtension')
const menuRoutes = generateSettingsMenu(ref, project, isVaultEnabled, isOrgBilling)
useEffect(() => {
if (ui.selectedProjectRef) {
meta.extensions.load()
}
}, [ui.selectedProjectRef])
const { ref, slug } = useParams()
return (
<ProjectLayout
title={title || 'Settings'}
product="Settings"
productMenu={<ProductMenu page={page} menu={menuRoutes} />}
>
<main style={{ maxHeight: '100vh' }} className="flex-1 overflow-y-auto">
{children}
</main>
</ProjectLayout>
<AppLayout>
<div className="flex h-full">
{router.pathname !== '/projects' && (
<div className="h-full overflow-y-auto min-w-[280px] border-r px-8 py-8">
{slug === undefined && ref === undefined ? (
<AccountSettingsMenu />
) : (
<OrganizationSettingsMenu />
)}
</div>
)}
<div className="h-full overflow-y-auto flex-grow">{children}</div>
</div>
</AppLayout>
)
}
export default withAuth(observer(SettingsLayout))
export default SettingsLayout
@@ -0,0 +1,27 @@
import clsx from 'clsx'
import Link from 'next/link'
interface SettingsMenuItemProps {
href: string
label: string
isActive: boolean
}
const SettingsMenuItem = ({ href, label, isActive }: SettingsMenuItemProps) => {
return (
<div>
<Link href={href}>
<a
className={clsx(
'text-sm',
isActive ? 'text-scale-1200' : 'text-scale-1100 hover:text-scale-1200 transition'
)}
>
{label}
</a>
</Link>
</div>
)
}
export default SettingsMenuItem
@@ -2,6 +2,7 @@ import { useMemo, useState } from 'react'
import { noop, partition } from 'lodash'
import { observer } from 'mobx-react-lite'
import {
Alert,
Button,
Dropdown,
IconCheck,
@@ -83,7 +84,6 @@ const TableEditorMenu = ({
)
const schemas: PostgresSchema[] = meta.schemas.list()
const schema = schemas.find((schema) => schema.name === selectedSchema)
const canCreateTables = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'tables')
@@ -110,6 +110,13 @@ const TableEditorMenu = ({
<IconLoader className="animate-spin" size={12} />
<span className="text-xs text-scale-900">Loading schemas...</span>
</div>
) : meta.schemas.hasError ? (
<Alert variant="warning" title="Failed to load schemas" className="!px-3 !py-3">
<p className="mb-2">Error: {meta.schemas?.error?.message}</p>
<Button type="default" size="tiny" onClick={() => meta.schemas.load()}>
Reload schemas
</Button>
</Alert>
) : (
<Listbox
size="tiny"
+1 -1
View File
@@ -9,7 +9,7 @@ import TableEditorLayout from './TableEditorLayout/TableEditorLayout'
import SQLEditorLayout from './SQLEditorLayout/SQLEditorLayout'
import DatabaseLayout from './DatabaseLayout/DatabaseLayout'
import DocsLayout from './DocsLayout/DocsLayout'
import SettingsLayout from './SettingsLayout/SettingsLayout'
import SettingsLayout from './ProjectSettingsLayout/SettingsLayout'
import StorageLayout from './StorageLayout/StorageLayout'
import AccountLayout from './AccountLayout/AccountLayout'
import { AccountLayoutWithoutAuth } from './AccountLayout/AccountLayout'
+1 -3
View File
@@ -1,5 +1,3 @@
import { FC } from 'react'
/**
* Render keyMap
*
@@ -13,7 +11,7 @@ const customKeyChars: any = {
enter: '↵',
}
const KeyMap: FC<any> = ({ keyMap }) => {
const KeyMap = ({ keyMap }: any) => {
if (!keyMap) return null
const keys = keyMap.split('+')
@@ -39,7 +39,7 @@ const ProductEmptyState: FC<Props> = ({
<div className="flex items-center space-x-2">
{ctaButtonLabel && onClickCta && (
<Tooltip.Root delayDuration={0}>
<Tooltip.Trigger>
<Tooltip.Trigger asChild>
<Button type="primary" onClick={onClickCta} disabled={disabled}>
{ctaButtonLabel}
</Button>
@@ -9,8 +9,11 @@ export async function getOrganizations(signal?: AbortSignal): Promise<Organizati
const data = await get(`${API_URL}/organizations`, { signal })
if (data.error) throw data.error
const sorted = (data as Organization[]).sort((a, b) => a.name.localeCompare(b.name))
if (!Array.isArray(data)) {
return []
}
const sorted = (data as Organization[]).sort((a, b) => a.name.localeCompare(b.name))
return sorted
}
+5 -1
View File
@@ -15,7 +15,11 @@ export function doPermissionsCheck(
data?: object,
organizationId?: number
) {
return (permissions ?? [])
if (!permissions || !Array.isArray(permissions)) {
return false
}
return permissions
.filter(
(permission) =>
permission.organization_id === organizationId &&
+14 -4
View File
@@ -3,15 +3,25 @@ import { useOrganizationsQuery } from 'data/organizations/organizations-query'
import { useMemo } from 'react'
import { useSelectedProject } from './useSelectedProject'
import { LOCAL_STORAGE_KEYS } from 'lib/constants'
export function useSelectedOrganization({ enabled = true } = {}) {
const { slug } = useParams()
const { data } = useOrganizationsQuery({ enabled })
const selectedProject = useSelectedProject()
const localStorageSlug = useMemo(() => {
return typeof window !== 'undefined'
? localStorage.getItem(LOCAL_STORAGE_KEYS.RECENTLY_VISITED_ORGANIZATION)
: null
}, [])
return useMemo(() => {
return data?.find(
(org) => org.slug === slug || (selectedProject && org.id === selectedProject.organization_id)
)
}, [data, selectedProject, slug])
return data?.find((org) => {
if (slug !== undefined) return org.slug === slug
if (selectedProject !== undefined) return org.id === selectedProject.organization_id
if (localStorageSlug !== undefined) return org.slug === localStorageSlug
return undefined
})
}, [data, selectedProject, slug, localStorageSlug])
}
-1
View File
@@ -4,6 +4,5 @@ import { useProjectDetailQuery } from 'data/projects/project-detail-query'
export function useSelectedProject() {
const { ref } = useParams()
const { data } = useProjectDetailQuery({ ref })
return data
}
+22 -4
View File
@@ -1,6 +1,7 @@
import { auth } from 'lib/gotrue'
import { isUndefined } from 'lodash'
import { SupaResponse } from 'types/base'
import { Session } from '@supabase/gotrue-js'
export function handleError<T>(e: any, requestId: string): SupaResponse<T> {
const message = e?.message ? `An error has occurred: ${e.message}` : 'An error has occurred'
@@ -86,15 +87,32 @@ export async function handleResponseError<T = unknown>(
}
}
let currentSession: Session | null = null
auth.onAuthStateChange((event, session) => {
currentSession = session
})
export async function getAccessToken() {
// ignore if server-side
if (typeof window === 'undefined') return ''
const {
data: { session },
} = await auth.getSession()
const aboutToExpire = currentSession?.expires_at
? currentSession.expires_at - Math.ceil(Date.now() / 1000) < 60
: false
return session?.access_token
if (!currentSession || aboutToExpire) {
const {
data: { session },
} = await auth.getSession()
currentSession = session
}
// using memory version as gotrue-js can be a bit slow when using
// #getSession() as it has to read directly from local storage
return currentSession?.access_token
}
export async function constructHeaders(requestId: string, optionHeaders?: { [prop: string]: any }) {
+4
View File
@@ -85,3 +85,7 @@ export const STRIPE_PUBLIC_KEY =
process.env.NEXT_PUBLIC_STRIPE_PUBLIC_KEY || 'pk_test_XVwg5IZH3I9Gti98hZw6KRzd00v5858heG'
export const USAGE_APPROACHING_THRESHOLD = 0.75
export const LOCAL_STORAGE_KEYS = {
RECENTLY_VISITED_ORGANIZATION: 'supabase-organization',
}
+5
View File
@@ -62,6 +62,11 @@ dayjs.extend(relativeTime)
dart(Prism)
// [Joshen TODO] Once we settle on the new nav layout - we'll need a lot of clean up in terms of our layout components
// a lot of them are unnecessary and introduce way too many cluttered CSS especially with the height styles that make
// debugging way too difficult. Ideal scenario is we just have one AppLayout to control the height and scroll areas of
// the dashboard, all other layout components should not be doing that
function CustomApp({ Component, pageProps }: AppPropsWithLayout) {
const queryClient = useRootQueryClient()
const [rootStore] = useState(() => new RootStore())
+3
View File
@@ -64,6 +64,9 @@ const handleGetAll = async (req: NextApiRequest, res: NextApiResponse) => {
EXTERNAL_FACEBOOK_ENABLED: false,
EXTERNAL_FACEBOOK_CLIENT_ID: null,
EXTERNAL_FACEBOOK_SECRET: null,
EXTERNAL_FIGMA_ENABLED: false,
EXTERNAL_FIGMA_CLIENT_ID: null,
EXTERNAL_FIGMA_SECRET: null,
EXTERNAL_GITHUB_ENABLED: false,
EXTERNAL_GITHUB_CLIENT_ID: null,
EXTERNAL_GITHUB_SECRET: null,
@@ -1,25 +1,24 @@
import { keyBy } from 'lodash'
import { useCallback, useMemo } from 'react'
import { toast } from 'react-hot-toast'
import { useParams } from 'common'
import { ENV_VAR_RAW_KEYS } from 'components/interfaces/Integrations/Integrations-Vercel.constants'
import ProjectLinker, { ForeignProject } from 'components/interfaces/Integrations/ProjectLinker'
import { Markdown } from 'components/interfaces/Markdown'
import { VercelIntegrationLayout } from 'components/layouts'
import { ScaffoldContainer, ScaffoldDivider } from 'components/layouts/Scaffold'
import { ScaffoldColumn, ScaffoldContainer, ScaffoldDivider } from 'components/layouts/Scaffold'
import { vercelIcon } from 'components/to-be-cleaned/ListIcons'
import { useOrgIntegrationsQuery } from 'data/integrations/integrations-query-org-only'
import { useIntegrationsVercelConnectionSyncEnvsMutation } from 'data/integrations/integrations-vercel-connection-sync-envs-mutation'
import { useIntegrationVercelConnectionsCreateMutation } from 'data/integrations/integrations-vercel-connections-create-mutation'
import { useVercelProjectsQuery } from 'data/integrations/integrations-vercel-projects-query'
import { useOrganizationsQuery } from 'data/organizations/organizations-query'
import { useProjectsQuery } from 'data/projects/projects-query'
import { BASE_PATH } from 'lib/constants'
import { EMPTY_ARR } from 'lib/void'
import { NextPageWithLayout, Organization } from 'types'
import { IconBook, IconLifeBuoy, LoadingLine } from 'ui'
import { ENV_VAR_RAW_KEYS } from 'components/interfaces/Integrations/Integrations-Vercel.constants'
import { useIntegrationsVercelConnectionSyncEnvsMutation } from 'data/integrations/integrations-vercel-connection-sync-envs-mutation'
import { toast } from 'react-hot-toast'
const VERCEL_ICON = (
<svg xmlns="http://www.w3.org/2000/svg" fill="white" viewBox="0 0 512 512" className="w-6">
@@ -52,9 +51,11 @@ const VercelIntegration: NextPageWithLayout = () => {
x.metadata?.configuration_id === configurationId
)
const { data: supabaseProjectsData } = useProjectsQuery({
enabled: integration?.id !== undefined,
})
const { data: supabaseProjectsData, isLoading: isLoadingSupabaseProjectsData } = useProjectsQuery(
{
enabled: integration?.id !== undefined,
}
)
const supabaseProjects = useMemo(
() =>
@@ -65,12 +66,13 @@ const VercelIntegration: NextPageWithLayout = () => {
[organization?.id, supabaseProjectsData]
)
const { data: vercelProjectsData } = useVercelProjectsQuery(
{
organization_integration_id: integration?.id,
},
{ enabled: integration?.id !== undefined }
)
const { data: vercelProjectsData, isLoading: isLoadingVercelProjectsData } =
useVercelProjectsQuery(
{
organization_integration_id: integration?.id,
},
{ enabled: integration?.id !== undefined }
)
const vercelProjects = useMemo(() => vercelProjectsData ?? EMPTY_ARR, [vercelProjectsData])
const vercelProjectsById = useMemo(() => keyBy(vercelProjects, 'id'), [vercelProjects])
@@ -131,39 +133,43 @@ const VercelIntegration: NextPageWithLayout = () => {
return (
<>
<LoadingLine loading={isCreatingConnection} />
<main className="overflow-auto flex flex-col h-full">
<LoadingLine loading={isCreatingConnection} />
<>
<ScaffoldContainer className="flex flex-col gap-6 grow py-8">
<header>
<h1 className="text-xl text-scale-1200">
Link a Supabase project to a Vercel project
</h1>
<Markdown
className="text-scale-900"
content={`
<ScaffoldColumn className="!max-w-[900px] mx-auto w-full">
<header>
<h1 className="text-xl text-scale-1200">
Link a Supabase project to a Vercel project
</h1>
<Markdown
className="text-scale-900"
content={`
This Supabase integration manages your environment variables automatically to provide the latest keys in the unlikely event that you will need to refresh your JWT token.
`}
/>
</header>
<ProjectLinker
organizationIntegrationId={integration?.id}
foreignProjects={vercelProjects}
supabaseProjects={supabaseProjects}
onCreateConnections={onCreateConnections}
installedConnections={integration?.connections}
isLoading={isCreatingConnection}
integrationIcon={VERCEL_ICON}
getForeignProjectIcon={getForeignProjectIcon}
choosePrompt="Choose Vercel Project"
onSkip={() => {
if (next) {
window.location.href = next
}
}}
loadingForeignProjects={isLoadingVercelProjectsData}
loadingSupabaseProjects={isLoadingSupabaseProjectsData}
/>
</header>
<ProjectLinker
organizationIntegrationId={integration?.id}
foreignProjects={vercelProjects}
supabaseProjects={supabaseProjects}
onCreateConnections={onCreateConnections}
installedConnections={integration?.connections}
isLoading={isCreatingConnection}
integrationIcon={VERCEL_ICON}
getForeignProjectIcon={getForeignProjectIcon}
choosePrompt="Choose Vercel Project"
onSkip={() => {
if (next) {
window.location.href = next
}
}}
/>
<Markdown
content={`
<Markdown
content={`
The following environment variables will be added:
${ENV_VAR_RAW_KEYS.map((x) => {
@@ -173,7 +179,8 @@ ${ENV_VAR_RAW_KEYS.map((x) => {
`
})}
`}
/>
/>
</ScaffoldColumn>
</ScaffoldContainer>
</>
+170 -96
View File
@@ -1,3 +1,6 @@
import { useRouter } from 'next/router'
import { useEffect, useRef, useState } from 'react'
import { useParams } from 'common'
import { Markdown } from 'components/interfaces/Markdown'
import VercelIntegrationWindowLayout from 'components/layouts/IntegrationsLayout/VercelIntegrationWindowLayout'
@@ -7,10 +10,30 @@ import { IntegrationName } from 'data/integrations/integrations.types'
import { useVercelIntegrationCreateMutation } from 'data/integrations/vercel-integration-create-mutation'
import { useOrganizationsQuery } from 'data/organizations/organizations-query'
import { useStore } from 'hooks'
import { useRouter } from 'next/router'
import { useState } from 'react'
import { NextPageWithLayout, Organization } from 'types'
import { Alert, Badge, Button, IconBook, IconHexagon, IconLifeBuoy, Listbox, LoadingLine } from 'ui'
import {
AlertDescription_Shadcn_,
AlertTitle_Shadcn_,
Alert_Shadcn_,
Badge,
Button,
CommandEmpty_Shadcn_,
CommandGroup_Shadcn_,
CommandInput_Shadcn_,
CommandItem_Shadcn_,
CommandList_Shadcn_,
Command_Shadcn_,
IconAlertTriangle,
IconBook,
IconChevronDown,
IconHexagon,
IconInfo,
IconLifeBuoy,
LoadingLine,
PopoverContent_Shadcn_,
PopoverTrigger_Shadcn_,
Popover_Shadcn_,
} from 'ui'
/**
* Organization type with `installationInstalled` added
@@ -33,7 +56,9 @@ const VercelIntegration: NextPageWithLayout = () => {
const router = useRouter()
const { ui } = useStore()
const { code, configurationId, teamId, source, externalId } = useParams()
const [selectedOrg, setSelectedOrg] = useState<Organization | null>(null)
const [selectedOrg, setSelectedOrg] = useState<OrganizationsResponseWithInstalledData | null>(
null
)
const [organizationIntegrationId, setOrganizationIntegrationId] = useState<string | null>(null)
/**
@@ -43,27 +68,44 @@ const VercelIntegration: NextPageWithLayout = () => {
*/
const { data: integrationData } = useIntegrationsQuery()
const { data: organizationsData, isLoading: isLoadingOrganizationsQuery } = useOrganizationsQuery(
{
onSuccess(organizations) {
const firstOrg = organizations?.[0]
if (firstOrg && selectedOrg === null) {
setSelectedOrg(firstOrg)
router.query.organizationSlug = firstOrg.slug
}
},
const { data: organizationsData, isLoading: isLoadingOrganizationsQuery } =
useOrganizationsQuery()
useEffect(() => {
if (organizationsData !== undefined && integrationData !== undefined) {
const firstOrg = organizationsWithInstalls(organizationsData)?.[0]
if (firstOrg && selectedOrg === null) {
setSelectedOrg(firstOrg)
router.query.organizationSlug = firstOrg.slug
}
}
)
}, [organizationsData, integrationData])
/**
* Flat array of org slugs that have integration installed
* Find installedConnections
*/
const flatInstalledConnectionsIds: string[] | [] =
integrationData && integrationData.length > 0
? integrationData
.filter((x) => x.integration.name === 'Vercel')
.map((x) => x.organization.slug)
function organizationsWithInstalls(organizations?: OrganizationsResponseWithInstalledData[]) {
/**
* Flat array of org slugs that have integration installed
*
*/
const flatInstalledConnectionsIds: string[] | [] =
integrationData && integrationData.length > 0
? integrationData
.filter((x) => x.integration.name === 'Vercel')
.map((x) => x.organization.slug)
: []
return organizations
? organizations?.map((org) => {
return {
...org,
installationInstalled: flatInstalledConnectionsIds.includes(org.slug) ? true : false,
}
})
: []
}
/**
* Organizations with extra `installationInstalled` attribute
@@ -71,14 +113,8 @@ const VercelIntegration: NextPageWithLayout = () => {
* Used to show label/badge and allow/disallow installing
*
*/
const organizationsWithInstalledData: OrganizationsResponseWithInstalledData[] = organizationsData
? organizationsData.map((org) => {
return {
...org,
installationInstalled: flatInstalledConnectionsIds.includes(org.slug) ? true : false,
}
})
: []
const organizationsWithInstalledData: OrganizationsResponseWithInstalledData[] =
organizationsWithInstalls(organizationsData)
/**
* Handle the correct route change based on whether the vercel integration
@@ -161,6 +197,13 @@ const VercelIntegration: NextPageWithLayout = () => {
const dataLoading = isLoadingVercelIntegrationCreateMutation || isLoadingOrganizationsQuery
const disableInstallationForm =
(isLoadingVercelIntegrationCreateMutation && !dataLoading) ||
// disables installation button if integration is already installed and it is Marketplace flow
(selectedOrg && selectedOrg?.installationInstalled && source === 'marketplace' && !dataLoading)
? true
: false
return (
<>
<main className="overflow-auto flex flex-col h-full bg-scale-400">
@@ -168,24 +211,40 @@ const VercelIntegration: NextPageWithLayout = () => {
{organizationIntegrationId === null && (
<>
<ScaffoldContainer className="flex flex-col gap-6 grow py-8">
<ScaffoldColumn className="mx-auto">
<ScaffoldColumn className="mx-auto w-full max-w-md">
<h1 className="text-xl text-scale-1200">Choose organization</h1>
<>
<Markdown content={`Choose the Supabase organization you wish to install in`} />
<OrganizationPicker
integrationName="Vercel"
organizationsWithInstalledData={organizationsWithInstalledData}
onSelectedOrgChange={(e) => {
router.query.organizationSlug = e.slug
setSelectedOrg(e)
onSelectedOrgChange={(e: string) => {
const org = organizationsWithInstalledData?.find((org) => org.slug === e)
if (org) {
setSelectedOrg(org)
router.query.organizationSlug = org.slug
}
}}
selectedOrg={selectedOrg}
dataLoading={dataLoading}
/>
{disableInstallationForm && (
<Alert_Shadcn_ variant="warning">
<IconAlertTriangle className="h-4 w-4" strokeWidth={2} />
<AlertTitle_Shadcn_>
Vercel Integration is already installed.
</AlertTitle_Shadcn_>
<AlertDescription_Shadcn_>
You will need to choose another organization to install the integration.
</AlertDescription_Shadcn_>
</Alert_Shadcn_>
)}
<div className="flex flex-row w-full justify-end">
<Button
size="medium"
className="self-end"
disabled={isLoadingVercelIntegrationCreateMutation}
disabled={disableInstallationForm || isLoadingVercelIntegrationCreateMutation}
loading={isLoadingVercelIntegrationCreateMutation}
onClick={onInstall}
>
@@ -196,22 +255,22 @@ const VercelIntegration: NextPageWithLayout = () => {
</ScaffoldColumn>
</ScaffoldContainer>
<ScaffoldContainer className="flex flex-col gap-6 py-3">
<Alert
withIcon
variant="info"
title="You can uninstall this Integration at any time."
>
<Markdown
content={`Remove this integration at any time via Vercel or the Supabase dashboard.`}
/>
</Alert>
<Alert_Shadcn_ variant="default">
<IconInfo className="h-4 w-4" strokeWidth={2} />
<AlertTitle_Shadcn_>
You can uninstall this Integration at any time.
</AlertTitle_Shadcn_>
<AlertDescription_Shadcn_>
Remove this integration at any time via Vercel or the Supabase dashboard.
</AlertDescription_Shadcn_>
</Alert_Shadcn_>
</ScaffoldContainer>
</>
)}
<ScaffoldDivider />
</main>
<ScaffoldContainer className="bg-body flex flex-row gap-6 py-6 border-t">
<ScaffoldContainer className="bg-body flex flex-row gap-6 py-6">
<div className="flex items-center gap-2 text-xs text-scale-900">
<IconBook size={16} /> Docs
</div>
@@ -229,73 +288,88 @@ VercelIntegration.getLayout = (page) => (
export interface OrganizationPickerProps {
label?: string
onSelectedOrgChange?: (org: Organization) => void
onSelectedOrgChange: (slug: string) => void
integrationName: IntegrationName
dataLoading: boolean
organizationsWithInstalledData: OrganizationsResponseWithInstalledData[]
selectedOrg: OrganizationsResponseWithInstalledData | null
}
const OrganizationPicker = ({
label = 'Choose an organization',
onSelectedOrgChange,
integrationName,
selectedOrg,
dataLoading,
organizationsWithInstalledData,
}: OrganizationPickerProps) => {
const { ui } = useStore()
const [selectedOrg, setSelectedOrg] = useState<Organization | null>(null)
const { data, isLoading } = useOrganizationsQuery({
onSuccess(organizations) {
const firstOrg = organizations?.[0]
if (firstOrg && selectedOrg === null) {
setSelectedOrg(firstOrg)
onSelectedOrgChange?.(firstOrg)
}
},
})
function _onSelectedOrgChange(slug: string) {
const org = data?.find((org) => org.slug === slug)
if (org) {
setSelectedOrg(org)
onSelectedOrgChange?.(org)
}
}
if (dataLoading) {
return (
<Listbox label={label} value="loading" disabled>
<Listbox.Option key="loading" value="loading" label="Loading...">
Loading...
</Listbox.Option>
</Listbox>
)
}
const [open, setOpen] = useState(false)
const ref = useRef<HTMLButtonElement>(null)
return (
<Listbox label={label} value={selectedOrg?.slug} onChange={_onSelectedOrgChange}>
{organizationsWithInstalledData?.map((org) => {
const label = (
<div className="flex gap-3 items-center">
{org.name}
{org.installationInstalled && <Badge color="scale">Integration Installed</Badge>}
</div>
)
return (
<Listbox.Option
key={org.id}
value={org.slug}
// @ts-expect-error
label={label}
addOnBefore={({ active, selected }: any) => <IconHexagon />}
<>
<Popover_Shadcn_ open={open} onOpenChange={setOpen}>
<PopoverTrigger_Shadcn_ asChild>
<Button
ref={ref}
type="default"
size="medium"
block
className="justify-start"
icon={<IconHexagon />}
loading={dataLoading}
disabled={dataLoading}
iconRight={
<span className="grow flex justify-end">
<IconChevronDown className={''} />
</span>
}
>
{label}
</Listbox.Option>
)
})}
</Listbox>
<span className="flex gap-2">
<span className="truncate">{selectedOrg?.name}</span>
{selectedOrg?.installationInstalled && (
<Badge color="scale">Integration Installed</Badge>
)}
</span>
</Button>
</PopoverTrigger_Shadcn_>
<PopoverContent_Shadcn_
className="p-0 w-full"
side="bottom"
align="center"
style={{ width: ref.current?.offsetWidth }}
>
<Command_Shadcn_>
<CommandInput_Shadcn_ placeholder="Search organization..." />
<CommandList_Shadcn_>
<CommandEmpty_Shadcn_>No results found.</CommandEmpty_Shadcn_>
<CommandGroup_Shadcn_>
{organizationsWithInstalledData?.map((org) => {
return (
<CommandItem_Shadcn_
value={org.slug}
key={org.slug}
className="flex gap-2 items-center"
onSelect={(slug) => {
if (slug) onSelectedOrgChange(slug)
setOpen(false)
}}
>
<IconHexagon />
<span className="truncate">{org.name}</span>{' '}
{org?.installationInstalled && (
<Badge color="scale" className="!flex-none">
Integration Installed
</Badge>
)}
</CommandItem_Shadcn_>
)
})}
</CommandGroup_Shadcn_>
</CommandList_Shadcn_>
</Command_Shadcn_>
</PopoverContent_Shadcn_>
</Popover_Shadcn_>
</>
)
}
+2 -2
View File
@@ -1,4 +1,4 @@
import { GeneralSettings } from 'components/interfaces/Organization'
import { GeneralSettings as GeneralSettingsLegacy } from 'components/interfaces/Organization'
import { OrganizationLayout } from 'components/layouts'
import Loading from 'components/ui/Loading'
import { usePermissionsQuery } from 'data/permissions/permissions-query'
@@ -14,7 +14,7 @@ const OrgGeneralSettings: NextPageWithLayout = () => {
{selectedOrganization === undefined && isLoadingPermissions ? (
<Loading />
) : (
<GeneralSettings />
<GeneralSettingsLegacy />
)}
</>
)
Loaded 100 of 112 files, more files were not shown because too many files have changed in this diff. Show more