diff --git a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts index 8ff54ba58c6..71029382f99 100644 --- a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts +++ b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts @@ -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', diff --git a/apps/docs/data/authProviders.ts b/apps/docs/data/authProviders.ts index 9694f1daf12..6a9a4b4d30c 100644 --- a/apps/docs/data/authProviders.ts +++ b/apps/docs/data/authProviders.ts @@ -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', diff --git a/apps/docs/pages/guides/ai/choosing-compute-addon.mdx b/apps/docs/pages/guides/ai/choosing-compute-addon.mdx index 1a27b1e52cd..4ab4e13e808 100644 --- a/apps/docs/pages/guides/ai/choosing-compute-addon.mdx +++ b/apps/docs/pages/guides/ai/choosing-compute-addon.mdx @@ -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). -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 -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. - + -Emdeddings of 960 dimensions from gist-960 dataset with 1,000,000 vectors. +
+ multi database + multi database +
+ +### 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. + + + | 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 | - + -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. + + + | 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. - -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. - @@ -141,7 +159,34 @@ It is possible to upload more vectors to a single table if Memory allows it (for -## 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. + +
+ multi database + multi database +
+ +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. diff --git a/apps/docs/pages/guides/ai/examples/building-chatgpt-plugins.mdx b/apps/docs/pages/guides/ai/examples/building-chatgpt-plugins.mdx index 1f34a54c413..80d80a6cc65 100644 --- a/apps/docs/pages/guides/ai/examples/building-chatgpt-plugins.mdx +++ b/apps/docs/pages/guides/ai/examples/building-chatgpt-plugins.mdx @@ -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 }) => export default Page diff --git a/apps/docs/pages/guides/auth/auth-helpers/nextjs.mdx b/apps/docs/pages/guides/auth/auth-helpers/nextjs.mdx index c66d2e48e4c..cbb1ae76649 100644 --- a/apps/docs/pages/guides/auth/auth-helpers/nextjs.mdx +++ b/apps/docs/pages/guides/auth/auth-helpers/nextjs.mdx @@ -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) diff --git a/apps/docs/pages/guides/auth/social-login/auth-figma.mdx b/apps/docs/pages/guides/auth/social-login/auth-figma.mdx new file mode 100644 index 00000000000..1b7ce0b5305 --- /dev/null +++ b/apps/docs/pages/guides/auth/social-login/auth-figma.mdx @@ -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 + + + +## 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 + + + +## 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 }) => + +export default Page diff --git a/apps/docs/pages/guides/getting-started/tutorials/with-nextjs.mdx b/apps/docs/pages/guides/getting-started/tutorials/with-nextjs.mdx index 29c255f5a4c..4bf3351ce31 100644 --- a/apps/docs/pages/guides/getting-started/tutorials/with-nextjs.mdx +++ b/apps/docs/pages/guides/getting-started/tutorials/with-nextjs.mdx @@ -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) diff --git a/apps/docs/pages/guides/resources/examples.mdx b/apps/docs/pages/guides/resources/examples.mdx index 32c45d50c01..5cded86479b 100644 --- a/apps/docs/pages/guides/resources/examples.mdx +++ b/apps/docs/pages/guides/resources/examples.mdx @@ -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) + + + Build a Twitter Clone with the Next.js App Router and Supabase + + ### 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 diff --git a/apps/docs/public/img/ai/instance-type/lists-for-1m--dark.png b/apps/docs/public/img/ai/instance-type/lists-for-1m--dark.png new file mode 100644 index 00000000000..713ae4f4459 Binary files /dev/null and b/apps/docs/public/img/ai/instance-type/lists-for-1m--dark.png differ diff --git a/apps/docs/public/img/ai/instance-type/lists-for-1m--light.png b/apps/docs/public/img/ai/instance-type/lists-for-1m--light.png new file mode 100644 index 00000000000..20356170933 Binary files /dev/null and b/apps/docs/public/img/ai/instance-type/lists-for-1m--light.png differ diff --git a/apps/docs/public/img/guides/auth-figma/figma_app_credentials.png b/apps/docs/public/img/guides/auth-figma/figma_app_credentials.png new file mode 100755 index 00000000000..e4d582ab9fb Binary files /dev/null and b/apps/docs/public/img/guides/auth-figma/figma_app_credentials.png differ diff --git a/apps/docs/public/img/guides/auth-figma/figma_create_app.png b/apps/docs/public/img/guides/auth-figma/figma_create_app.png new file mode 100755 index 00000000000..7caeb2c0057 Binary files /dev/null and b/apps/docs/public/img/guides/auth-figma/figma_create_app.png differ diff --git a/apps/docs/public/img/guides/auth-figma/figma_developers_page.png b/apps/docs/public/img/guides/auth-figma/figma_developers_page.png new file mode 100755 index 00000000000..8ceee422d79 Binary files /dev/null and b/apps/docs/public/img/guides/auth-figma/figma_developers_page.png differ diff --git a/apps/docs/public/img/icons/figma-icon.svg b/apps/docs/public/img/icons/figma-icon.svg new file mode 100644 index 00000000000..38539ba64cd --- /dev/null +++ b/apps/docs/public/img/icons/figma-icon.svg @@ -0,0 +1 @@ + diff --git a/apps/www/components/Announcement/Announcement.tsx b/apps/www/components/Announcement/Announcement.tsx new file mode 100644 index 00000000000..f31fd0a75cc --- /dev/null +++ b/apps/www/components/Announcement/Announcement.tsx @@ -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) => { + 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 ( +
+ {!isLaunchWeekSection && ( +
+ +
+ )} + {children} + + + +
+ ) + } +} + +export default Announcement diff --git a/apps/www/components/Announcement/data/Announcement.json b/apps/www/components/Announcement/data/Announcement.json new file mode 100644 index 00000000000..abb5cec8362 --- /dev/null +++ b/apps/www/components/Announcement/data/Announcement.json @@ -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" +} diff --git a/apps/www/components/Hero/Hero.tsx b/apps/www/components/Hero/Hero.tsx index 1cc9750aafc..6aba7c1539c 100644 --- a/apps/www/components/Hero/Hero.tsx +++ b/apps/www/components/Hero/Hero.tsx @@ -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 = () => {
- Get you ticket + Get your ticket
- Launch Week 8 - August 7th + Launch Week 8 - August 7-11 { -
- - starts background +
+
+ launch week 8 shape +
+
+ stars background +
-
+
| 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 ? ( <> diff --git a/apps/www/components/Nav/index.tsx b/apps/www/components/Nav/index.tsx index 1869751d9f2..2439d76df00 100644 --- a/apps/www/components/Nav/index.tsx +++ b/apps/www/components/Nav/index.tsx @@ -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 ( <> - +
diff --git a/apps/www/package.json b/apps/www/package.json index 089c687effe..55aacb7ebbb 100644 --- a/apps/www/package.json +++ b/apps/www/package.json @@ -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", diff --git a/apps/www/pages/careers/index.tsx b/apps/www/pages/careers/index.tsx index 1769d9f5a4b..a303e9f2e9a 100644 --- a/apps/www/pages/careers/index.tsx +++ b/apps/www/pages/careers/index.tsx @@ -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', }, { diff --git a/apps/www/pages/index.tsx b/apps/www/pages/index.tsx index 6b5d1b04f81..fc580b738dd 100644 --- a/apps/www/pages/index.tsx +++ b/apps/www/pages/index.tsx @@ -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) } }, []) diff --git a/apps/www/pages/launch-week/index.tsx b/apps/www/pages/launch-week/index.tsx index da8706c61d2..a44ac87bf85 100644 --- a/apps/www/pages/launch-week/index.tsx +++ b/apps/www/pages/launch-week/index.tsx @@ -51,6 +51,7 @@ export default function TicketHome({ users }: Props) { const [supabase, setSupabase] = useState(null) const [session, setSession] = useState(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) } }, []) diff --git a/apps/www/pages/launch-week/tickets/[username].tsx b/apps/www/pages/launch-week/tickets/[username].tsx index c246ce203c9..37808e679e4 100644 --- a/apps/www/pages/launch-week/tickets/[username].tsx +++ b/apps/www/pages/launch-week/tickets/[username].tsx @@ -44,6 +44,7 @@ export default function UsernamePage({ user, users, ogImageUrl }: Props) { const [supabase, setSupabase] = useState(null) const [session, setSession] = useState(null) const { isDarkMode, toggleTheme } = useTheme() + const [initialDarkMode] = useState(isDarkMode) const [pageState, setPageState] = useState('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) } }, []) diff --git a/apps/www/pages/launch-week/tickets/index.tsx b/apps/www/pages/launch-week/tickets/index.tsx index b0423f3180b..6feffbfbc68 100644 --- a/apps/www/pages/launch-week/tickets/index.tsx +++ b/apps/www/pages/launch-week/tickets/index.tsx @@ -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) } }, []) diff --git a/apps/www/public/images/launchweek/8/lw8-visual.png b/apps/www/public/images/launchweek/8/lw8-visual.png new file mode 100644 index 00000000000..5098a5da360 Binary files /dev/null and b/apps/www/public/images/launchweek/8/lw8-visual.png differ diff --git a/apps/www/public/planetpg-egor_romanov-rss.xml b/apps/www/public/planetpg-egor_romanov-rss.xml new file mode 100644 index 00000000000..6eddea8089c --- /dev/null +++ b/apps/www/public/planetpg-egor_romanov-rss.xml @@ -0,0 +1,19 @@ + + + + 'Postgres | Supabase Blog + https://supabase.com/blog + Latest Postgres news from Egor Romanov at Supabase + en + Wed, 12 Jul 2023 22:00:00 GMT + + + https://supabase.com/blog/pgvector-performance + pgvector 0.4.0 performance + https://supabase.com/blog/pgvector-performance + There'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. + Wed, 12 Jul 2023 22:00:00 GMT + + + + diff --git a/apps/www/public/planetpg-pavel-rss.xml b/apps/www/public/planetpg-pavel-rss.xml index c5be079063a..b0e33cbb033 100644 --- a/apps/www/public/planetpg-pavel-rss.xml +++ b/apps/www/public/planetpg-pavel-rss.xml @@ -5,9 +5,16 @@ https://supabase.com/blog Latest Postgres news from Pavel Borisov at Supabase en - Thu, 15 Dec 2022 23:00:00 GMT + Wed, 12 Jul 2023 22:00:00 GMT + https://supabase.com/blog/pgvector-performance + pgvector 0.4.0 performance + https://supabase.com/blog/pgvector-performance + There'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. + Wed, 12 Jul 2023 22:00:00 GMT + + https://supabase.com/blog/new-in-postgres-15 What's new in Postgres 15? https://supabase.com/blog/new-in-postgres-15 diff --git a/apps/www/public/rss.xml b/apps/www/public/rss.xml index d04b0a987a4..536de2d4ecd 100644 --- a/apps/www/public/rss.xml +++ b/apps/www/public/rss.xml @@ -5,9 +5,23 @@ https://supabase.com Latest news from Supabase en - Tue, 11 Jul 2023 22:00:00 GMT + Mon, 17 Jul 2023 22:00:00 GMT + https://supabase.com/blog/flutter-authentication + Getting started with Flutter authentication + https://supabase.com/blog/flutter-authentication + Learn how authentication on Flutter works through Google sign in with Supabase auth. + Mon, 17 Jul 2023 22:00:00 GMT + + + https://supabase.com/blog/pgvector-performance + pgvector 0.4.0 performance + https://supabase.com/blog/pgvector-performance + There'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. + Wed, 12 Jul 2023 22:00:00 GMT + + https://supabase.com/blog/postgrest-11-1-release What is new in PostgREST v11.1? https://supabase.com/blog/postgrest-11-1-release @@ -400,9 +414,9 @@ Wed, 23 Nov 2022 23:00:00 GMT - https://supabase.com/blog/flutter-authentication-and-authorization-with-rls - Flutter Authentication and Authorization with RLS - https://supabase.com/blog/flutter-authentication-and-authorization-with-rls + https://supabase.com/blog/flutter-authorization-with-rls + Flutter Authorization with RLS + https://supabase.com/blog/flutter-authorization-with-rls Learn how you can secure your Flutter app using Supabase Row Level Security. Mon, 21 Nov 2022 23:00:00 GMT diff --git a/package-lock.json b/package-lock.json index 41dacf05e10..b0c58cc3eca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -311,7 +311,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", @@ -417,6 +416,15 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/@adobe/css-tools": { "version": "4.2.0", "dev": true, @@ -2681,35 +2689,6 @@ "version": "6.0.2", "license": "MIT" }, - "node_modules/@chevrotain/cst-dts-gen": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-10.5.0.tgz", - "integrity": "sha512-lhmC/FyqQ2o7pGK4Om+hzuDrm9rhFYIJ/AXoQBeongmn870Xeb0L6oGEiuR8nohFNL5sMaQEJWCxr1oIVIVXrw==", - "dependencies": { - "@chevrotain/gast": "10.5.0", - "@chevrotain/types": "10.5.0", - "lodash": "4.17.21" - } - }, - "node_modules/@chevrotain/gast": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-10.5.0.tgz", - "integrity": "sha512-pXdMJ9XeDAbgOWKuD1Fldz4ieCs6+nLNmyVhe2gZVqoO7v8HXuHYs5OV2EzUtbuai37TlOAQHrTDvxMnvMJz3A==", - "dependencies": { - "@chevrotain/types": "10.5.0", - "lodash": "4.17.21" - } - }, - "node_modules/@chevrotain/types": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-10.5.0.tgz", - "integrity": "sha512-f1MAia0x/pAVPWH/T73BJVyO2XU5tI4/iE7cnxb7tqdNTNhQI3Uq3XkqcoteTmD4t1aM0LbHCJOhgIDn07kl2A==" - }, - "node_modules/@chevrotain/utils": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-10.5.0.tgz", - "integrity": "sha512-hBzuU5+JjB2cqNZyszkDHZgOSrUUT8V3dhgRl8Q9Gp6dAj/H5+KILGjbhDpc3Iy9qmqlm/akuOI2ut9VUtzJxQ==" - }, "node_modules/@cnakazawa/watch": { "version": "1.0.4", "dev": true, @@ -6698,102 +6677,6 @@ "version": "2.0.0", "license": "MIT" }, - "node_modules/@react-spring/animated": { - "version": "9.7.3", - "resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-9.7.3.tgz", - "integrity": "sha512-5CWeNJt9pNgyvuSzQH+uy2pvTg8Y4/OisoscZIR8/ZNLIOI+CatFBhGZpDGTF/OzdNFsAoGk3wiUYTwoJ0YIvw==", - "dependencies": { - "@react-spring/shared": "~9.7.3", - "@react-spring/types": "~9.7.3" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/@react-spring/core": { - "version": "9.7.3", - "resolved": "https://registry.npmjs.org/@react-spring/core/-/core-9.7.3.tgz", - "integrity": "sha512-IqFdPVf3ZOC1Cx7+M0cXf4odNLxDC+n7IN3MDcVCTIOSBfqEcBebSv+vlY5AhM0zw05PDbjKrNmBpzv/AqpjnQ==", - "dependencies": { - "@react-spring/animated": "~9.7.3", - "@react-spring/shared": "~9.7.3", - "@react-spring/types": "~9.7.3" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/react-spring/donate" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/@react-spring/shared": { - "version": "9.7.3", - "resolved": "https://registry.npmjs.org/@react-spring/shared/-/shared-9.7.3.tgz", - "integrity": "sha512-NEopD+9S5xYyQ0pGtioacLhL2luflh6HACSSDUZOwLHoxA5eku1UPuqcJqjwSD6luKjjLfiLOspxo43FUHKKSA==", - "dependencies": { - "@react-spring/types": "~9.7.3" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/@react-spring/three": { - "version": "9.7.3", - "resolved": "https://registry.npmjs.org/@react-spring/three/-/three-9.7.3.tgz", - "integrity": "sha512-Q1p512CqUlmMK8UMBF/Rj79qndhOWq4XUTayxMP9S892jiXzWQuj+xC3Xvm59DP/D4JXusXpxxqfgoH+hmOktA==", - "dependencies": { - "@react-spring/animated": "~9.7.3", - "@react-spring/core": "~9.7.3", - "@react-spring/shared": "~9.7.3", - "@react-spring/types": "~9.7.3" - }, - "peerDependencies": { - "@react-three/fiber": ">=6.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "three": ">=0.126" - } - }, - "node_modules/@react-spring/types": { - "version": "9.7.3", - "resolved": "https://registry.npmjs.org/@react-spring/types/-/types-9.7.3.tgz", - "integrity": "sha512-Kpx/fQ/ZFX31OtlqVEFfgaD1ACzul4NksrvIgYfIFq9JpDHFwQkMVZ10tbo0FU/grje4rcL4EIrjekl3kYwgWw==" - }, - "node_modules/@react-three/drei": { - "version": "8.20.2", - "resolved": "https://registry.npmjs.org/@react-three/drei/-/drei-8.20.2.tgz", - "integrity": "sha512-V2P/YHYg2+N58eWtIqmOJf0t5nWGTcmpT+ci4tB5gt3CsWU2vuoeQFhvBmqEF+cXwZD5ZYKPfQDf/06NzTx/lw==", - "dependencies": { - "@babel/runtime": "^7.11.2", - "@react-spring/three": "^9.3.1", - "@use-gesture/react": "^10.2.0", - "detect-gpu": "^4.0.14", - "glsl-noise": "^0.0.0", - "lodash.omit": "^4.5.0", - "lodash.pick": "^4.4.0", - "meshline": "^2.0.4", - "react-composer": "^5.0.2", - "react-merge-refs": "^1.1.0", - "stats.js": "^0.17.0", - "suspend-react": "^0.0.8", - "three-mesh-bvh": "^0.5.7", - "three-stdlib": "^2.8.9", - "troika-three-text": "^0.46.3", - "utility-types": "^3.10.0", - "zustand": "^3.5.13" - }, - "peerDependencies": { - "@react-three/fiber": ">=6.0", - "react": ">=17.0", - "react-dom": ">=17.0", - "three": ">=0.137" - }, - "peerDependenciesMeta": { - "react-dom": { - "optional": true - } - } - }, "node_modules/@react-three/fiber": { "version": "7.0.29", "resolved": "https://registry.npmjs.org/@react-three/fiber/-/fiber-7.0.29.tgz", @@ -14694,11 +14577,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/draco3d": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/@types/draco3d/-/draco3d-1.4.2.tgz", - "integrity": "sha512-goh23EGr6CLV6aKPwN1p8kBD/7tT5V/bLpToSbarKrwVejqNrspVrv8DhliteYkkhZYrlq/fwKZRRUzH4XN88w==" - }, "node_modules/@types/ejs": { "version": "3.1.2", "dev": true, @@ -14957,11 +14835,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/offscreencanvas": { - "version": "2019.7.0", - "resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.7.0.tgz", - "integrity": "sha512-PGcyveRIpL1XIqK8eBsmRBt76eFgtzuPiSTyKHZxnGemp2yzGzWpjYKAfK3wIMiU7eH+851yEpiuP8JZerTmWg==" - }, "node_modules/@types/papaparse": { "version": "5.3.7", "dev": true, @@ -15300,7 +15173,8 @@ "node_modules/@types/webxr": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.2.tgz", - "integrity": "sha512-szL74BnIcok9m7QwYtVmQ+EdIKwbjPANudfuvDrAF8Cljg9MKUlIoc1w5tjj9PMpeSH3U1Xnx//czQybJ0EfSw==" + "integrity": "sha512-szL74BnIcok9m7QwYtVmQ+EdIKwbjPANudfuvDrAF8Cljg9MKUlIoc1w5tjj9PMpeSH3U1Xnx//czQybJ0EfSw==", + "dev": true }, "node_modules/@types/yargs": { "version": "15.0.15", @@ -15520,22 +15394,6 @@ "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@use-gesture/core": { - "version": "10.2.27", - "resolved": "https://registry.npmjs.org/@use-gesture/core/-/core-10.2.27.tgz", - "integrity": "sha512-V4XV7hn9GAD2MYu8yBBVi5iuWBsAMfjPRMsEVzoTNGYH72tf0kFP+OKqGKc8YJFQIJx6yj+AOqxmEHOmx2/MEA==" - }, - "node_modules/@use-gesture/react": { - "version": "10.2.27", - "resolved": "https://registry.npmjs.org/@use-gesture/react/-/react-10.2.27.tgz", - "integrity": "sha512-7E5vnWCxeslWlxwZ8uKIcnUZVMTRMZ8cvSnLLKF1NkyNb3PnNiAzoXM4G1vTKJKRhgOTeI6wK1YsEpwo9ABV5w==", - "dependencies": { - "@use-gesture/core": "10.2.27" - }, - "peerDependencies": { - "react": ">= 16.8.0" - } - }, "node_modules/@webassemblyjs/ast": { "version": "1.9.0", "license": "MIT", @@ -17071,14 +16929,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/bidi-js": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.2.tgz", - "integrity": "sha512-rzSy/k7WdX5zOyeHHCOixGXbCHkyogkxPKL2r8QtzHmVQDiWCXUWa18bLdMWT9CYMLOYTjWpTHawuev2ouYJVw==", - "dependencies": { - "require-from-string": "^2.0.2" - } - }, "node_modules/big-integer": { "version": "1.6.51", "license": "Unlicense", @@ -17897,19 +17747,6 @@ "node": "*" } }, - "node_modules/chevrotain": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-10.5.0.tgz", - "integrity": "sha512-Pkv5rBY3+CsHOYfV5g/Vs5JY9WTHHDEKOlohI2XeygaZhUeqhAlldZ8Hz9cRmxu709bvS08YzxHdTPHhffc13A==", - "dependencies": { - "@chevrotain/cst-dts-gen": "10.5.0", - "@chevrotain/gast": "10.5.0", - "@chevrotain/types": "10.5.0", - "@chevrotain/utils": "10.5.0", - "lodash": "4.17.21", - "regexp-to-ast": "0.5.0" - } - }, "node_modules/chokidar": { "version": "3.5.3", "funding": [ @@ -18219,11 +18056,13 @@ } }, "node_modules/cmdk-supabase": { - "version": "0.2.2", - "license": "MIT", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cmdk-supabase/-/cmdk-supabase-1.0.7.tgz", + "integrity": "sha512-5Ts9dZTLSqNwCEFzps/dtNK6Q1baje1JuA+5qjEvtfkk+SGZG0S5D1uqxbAk7YQqF/ctGZHxe8O+0+vNPYMk2A==", "dependencies": { - "@radix-ui/react-dialog": "1.0.0", + "@radix-ui/react-dialog": "^1.0.4", "@radix-ui/react-id": "^0.0.6", + "@radix-ui/react-slot": "1.0.2", "command-score": "0.1.2", "use-sync-external-store": "^1.2.0" }, @@ -18232,112 +18071,10 @@ "react-dom": "^17.0" } }, - "node_modules/cmdk-supabase/node_modules/@radix-ui/primitive": { - "version": "1.0.0", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" - } - }, - "node_modules/cmdk-supabase/node_modules/@radix-ui/react-compose-refs": { - "version": "1.0.0", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "react": "^16.8 || ^17.0 || ^18.0" - } - }, - "node_modules/cmdk-supabase/node_modules/@radix-ui/react-context": { - "version": "1.0.0", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "react": "^16.8 || ^17.0 || ^18.0" - } - }, - "node_modules/cmdk-supabase/node_modules/@radix-ui/react-dialog": { - "version": "1.0.0", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.0", - "@radix-ui/react-compose-refs": "1.0.0", - "@radix-ui/react-context": "1.0.0", - "@radix-ui/react-dismissable-layer": "1.0.0", - "@radix-ui/react-focus-guards": "1.0.0", - "@radix-ui/react-focus-scope": "1.0.0", - "@radix-ui/react-id": "1.0.0", - "@radix-ui/react-portal": "1.0.0", - "@radix-ui/react-presence": "1.0.0", - "@radix-ui/react-primitive": "1.0.0", - "@radix-ui/react-slot": "1.0.0", - "@radix-ui/react-use-controllable-state": "1.0.0", - "aria-hidden": "^1.1.1", - "react-remove-scroll": "2.5.4" - }, - "peerDependencies": { - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - } - }, - "node_modules/cmdk-supabase/node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-id": { - "version": "1.0.0", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-use-layout-effect": "1.0.0" - }, - "peerDependencies": { - "react": "^16.8 || ^17.0 || ^18.0" - } - }, - "node_modules/cmdk-supabase/node_modules/@radix-ui/react-dismissable-layer": { - "version": "1.0.0", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.0", - "@radix-ui/react-compose-refs": "1.0.0", - "@radix-ui/react-primitive": "1.0.0", - "@radix-ui/react-use-callback-ref": "1.0.0", - "@radix-ui/react-use-escape-keydown": "1.0.0" - }, - "peerDependencies": { - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - } - }, - "node_modules/cmdk-supabase/node_modules/@radix-ui/react-focus-guards": { - "version": "1.0.0", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "react": "^16.8 || ^17.0 || ^18.0" - } - }, - "node_modules/cmdk-supabase/node_modules/@radix-ui/react-focus-scope": { - "version": "1.0.0", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-compose-refs": "1.0.0", - "@radix-ui/react-primitive": "1.0.0", - "@radix-ui/react-use-callback-ref": "1.0.0" - }, - "peerDependencies": { - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - } - }, "node_modules/cmdk-supabase/node_modules/@radix-ui/react-id": { "version": "0.0.6", - "license": "MIT", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-0.0.6.tgz", + "integrity": "sha512-PzmraF34fYggsYvTIZVJ5S68WMp3aKUN3HkSmGnz4zn9zpRjkAbbg7Xn3ueQI3FQsLWKgyUfnpsmWFDndpcqYg==", "dependencies": { "@babel/runtime": "^7.13.10" }, @@ -18345,119 +18082,6 @@ "react": "^16.8 || ^17.0" } }, - "node_modules/cmdk-supabase/node_modules/@radix-ui/react-portal": { - "version": "1.0.0", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-primitive": "1.0.0" - }, - "peerDependencies": { - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - } - }, - "node_modules/cmdk-supabase/node_modules/@radix-ui/react-presence": { - "version": "1.0.0", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-compose-refs": "1.0.0", - "@radix-ui/react-use-layout-effect": "1.0.0" - }, - "peerDependencies": { - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - } - }, - "node_modules/cmdk-supabase/node_modules/@radix-ui/react-primitive": { - "version": "1.0.0", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-slot": "1.0.0" - }, - "peerDependencies": { - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - } - }, - "node_modules/cmdk-supabase/node_modules/@radix-ui/react-slot": { - "version": "1.0.0", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-compose-refs": "1.0.0" - }, - "peerDependencies": { - "react": "^16.8 || ^17.0 || ^18.0" - } - }, - "node_modules/cmdk-supabase/node_modules/@radix-ui/react-use-callback-ref": { - "version": "1.0.0", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "react": "^16.8 || ^17.0 || ^18.0" - } - }, - "node_modules/cmdk-supabase/node_modules/@radix-ui/react-use-controllable-state": { - "version": "1.0.0", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-use-callback-ref": "1.0.0" - }, - "peerDependencies": { - "react": "^16.8 || ^17.0 || ^18.0" - } - }, - "node_modules/cmdk-supabase/node_modules/@radix-ui/react-use-escape-keydown": { - "version": "1.0.0", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-use-callback-ref": "1.0.0" - }, - "peerDependencies": { - "react": "^16.8 || ^17.0 || ^18.0" - } - }, - "node_modules/cmdk-supabase/node_modules/@radix-ui/react-use-layout-effect": { - "version": "1.0.0", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "react": "^16.8 || ^17.0 || ^18.0" - } - }, - "node_modules/cmdk-supabase/node_modules/react-remove-scroll": { - "version": "2.5.4", - "license": "MIT", - "dependencies": { - "react-remove-scroll-bar": "^2.3.3", - "react-style-singleton": "^2.2.1", - "tslib": "^2.1.0", - "use-callback-ref": "^1.3.0", - "use-sidecar": "^1.1.2" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/co": { "version": "4.6.0", "dev": true, @@ -18600,7 +18224,8 @@ }, "node_modules/command-score": { "version": "0.1.2", - "license": "MIT" + "resolved": "https://registry.npmjs.org/command-score/-/command-score-0.1.2.tgz", + "integrity": "sha512-VtDvQpIJBvBatnONUsPzXYFVKQQAhuf3XTNOAsdBxCNO/QCtUUd8LSgjn0GVarBkCad6aJCZfXgrjYbl/KRr7w==" }, "node_modules/commander": { "version": "6.2.1", @@ -20174,14 +19799,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/detect-gpu": { - "version": "4.0.50", - "resolved": "https://registry.npmjs.org/detect-gpu/-/detect-gpu-4.0.50.tgz", - "integrity": "sha512-T67HE5+ONONN8rPXCBJPupyCg2QT8+l2NUUMuPxAppsMJBDPG/Jg0URLs6GyDzLm2niUE+oncIHSuy3VinoPeQ==", - "dependencies": { - "webgl-constants": "^1.1.1" - } - }, "node_modules/detect-indent": { "version": "6.1.0", "dev": true, @@ -20456,11 +20073,6 @@ "dev": true, "license": "BSD-3-Clause" }, - "node_modules/draco3d": { - "version": "1.5.6", - "resolved": "https://registry.npmjs.org/draco3d/-/draco3d-1.5.6.tgz", - "integrity": "sha512-+3NaRjWktb5r61ZFoDejlykPEFKT5N/LkbXsaddlw6xNSXBanUYpFc2AXXpbJDilPHazcSreU/DpQIaxfX0NfQ==" - }, "node_modules/duplexer": { "version": "0.1.2", "license": "MIT" @@ -22073,7 +21685,8 @@ "node_modules/fflate": { "version": "0.6.10", "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.6.10.tgz", - "integrity": "sha512-IQrh3lEPM93wVCEczc9SaAOvkmcoQn/G8Bo1e8ZPlY3X3bnAxWaBdvTdvM1hP62iZp0BXWDy4vTAy4fF0+Dlpg==" + "integrity": "sha512-IQrh3lEPM93wVCEczc9SaAOvkmcoQn/G8Bo1e8ZPlY3X3bnAxWaBdvTdvM1hP62iZp0BXWDy4vTAy4fF0+Dlpg==", + "dev": true }, "node_modules/figgy-pudding": { "version": "3.5.2", @@ -22931,11 +22544,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/glsl-noise": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/glsl-noise/-/glsl-noise-0.0.0.tgz", - "integrity": "sha512-b/ZCF6amfAUb7dJM/MxRs7AetQEahYzJ8PtgfrmEdtw6uyGOr+ZSGtgjFm6mfsBkxJ4d2W7kg+Nlqzqvn3Bc0w==" - }, "node_modules/goober": { "version": "2.1.13", "license": "MIT", @@ -26789,11 +26397,6 @@ "node": ">= 8" } }, - "node_modules/ktx-parse": { - "version": "0.4.5", - "resolved": "https://registry.npmjs.org/ktx-parse/-/ktx-parse-0.4.5.tgz", - "integrity": "sha512-MK3FOody4TXbFf8Yqv7EBbySw7aPvEcPX++Ipt6Sox+/YMFvR5xaTyhfNSk1AEmMy+RYIw81ctN4IMxCB8OAlg==" - }, "node_modules/language-subtag-registry": { "version": "0.3.22", "license": "CC0-1.0" @@ -27095,16 +26698,6 @@ "version": "4.6.2", "license": "MIT" }, - "node_modules/lodash.omit": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.omit/-/lodash.omit-4.5.0.tgz", - "integrity": "sha512-XeqSp49hNGmlkj2EJlfrQFIzQ6lXdNro9sddtQzcJY8QaoC2GO0DT7xaIokHeyM+mIT0mPMlPvkYzg2xCuHdZg==" - }, - "node_modules/lodash.pick": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.pick/-/lodash.pick-4.4.0.tgz", - "integrity": "sha512-hXt6Ul/5yWjfklSGvLQl8vM//l3FtyHZeuelpzK6mm99pNvN9yTDruNZPEJZD1oWrqo+izBmB7oUfWgcCX7s4Q==" - }, "node_modules/lodash.template": { "version": "4.5.0", "license": "MIT", @@ -28017,14 +27610,6 @@ } } }, - "node_modules/meshline": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/meshline/-/meshline-2.0.4.tgz", - "integrity": "sha512-Jh6DJl/zLqA4xsKvGv5950jr2ukyXQE1wgxs8u94cImHrvL6soVIggqjP+2hVHZXGYaKnWszhtjuCbKNeQyYiw==", - "peerDependencies": { - "three": ">=0.137" - } - }, "node_modules/meshoptimizer": { "version": "0.18.1", "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-0.18.1.tgz", @@ -29006,11 +28591,6 @@ "dev": true, "license": "MIT" }, - "node_modules/mmd-parser": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mmd-parser/-/mmd-parser-1.0.4.tgz", - "integrity": "sha512-Qi0VCU46t2IwfGv5KF0+D/t9cizcDug7qnNoy9Ggk7aucp0tssV8IwTMkBlDbm+VqAf3cdQHTCARKSsuS2MYFg==" - }, "node_modules/mobx": { "version": "6.9.0", "license": "MIT", @@ -31047,21 +30627,6 @@ "opener": "bin/opener-bin.js" } }, - "node_modules/opentype.js": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/opentype.js/-/opentype.js-1.3.4.tgz", - "integrity": "sha512-d2JE9RP/6uagpQAVtJoF0pJJA/fgai89Cc50Yp0EJHk+eLp6QQ7gBoblsnubRULNY132I0J1QKMJ+JTbMqz4sw==", - "dependencies": { - "string.prototype.codepointat": "^0.2.1", - "tiny-inflate": "^1.0.3" - }, - "bin": { - "ot": "bin/ot" - }, - "engines": { - "node": ">= 8.0.0" - } - }, "node_modules/optionator": { "version": "0.9.1", "license": "MIT", @@ -33149,11 +32714,6 @@ "node": ">=0.10.0" } }, - "node_modules/potpack": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/potpack/-/potpack-1.0.2.tgz", - "integrity": "sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==" - }, "node_modules/preact": { "version": "10.15.1", "license": "MIT", @@ -33662,17 +33222,6 @@ "react": "^15.3.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" } }, - "node_modules/react-composer": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/react-composer/-/react-composer-5.0.3.tgz", - "integrity": "sha512-1uWd07EME6XZvMfapwZmc7NgCZqDemcvicRi3wMJzXsQLvZ3L7fTHVyPy1bZdnWXM4iPjYuNE+uJ41MLKeTtnA==", - "dependencies": { - "prop-types": "^15.6.0" - }, - "peerDependencies": { - "react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" - } - }, "node_modules/react-contexify": { "version": "5.0.0", "license": "MIT", @@ -34834,11 +34383,6 @@ "dev": true, "license": "MIT" }, - "node_modules/regexp-to-ast": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/regexp-to-ast/-/regexp-to-ast-0.5.0.tgz", - "integrity": "sha512-tlbJqcMHnPKI9zSrystikWKwHkBqu2a/Sgw01h3zFjvYrMxEDYHzzoMZnUrbIfpTFEsoRnnviOXNCzFiSc54Qw==" - }, "node_modules/regexp.prototype.flags": { "version": "1.5.0", "license": "MIT", @@ -36816,11 +36360,6 @@ "node": ">=0.10.0" } }, - "node_modules/stats.js": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/stats.js/-/stats.js-0.17.0.tgz", - "integrity": "sha512-hNKz8phvYLPEcRkeG1rsGmV5ChMjKDAWU7/OJJdDErPBNChQXxCo3WZurGpnWc6gZhAzEPFad1aVgyOANH1sMw==" - }, "node_modules/statuses": { "version": "2.0.1", "dev": true, @@ -37193,11 +36732,6 @@ "version": "8.0.0", "license": "MIT" }, - "node_modules/string.prototype.codepointat": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/string.prototype.codepointat/-/string.prototype.codepointat-0.2.1.tgz", - "integrity": "sha512-2cBVCj6I4IOvEnjgO/hWqXjqBGsY+zwPmHl12Srk9IXSZ56Jwwmy+66XO5Iut/oQVR7t5ihYdLB0GMa4alEUcg==" - }, "node_modules/string.prototype.matchall": { "version": "4.0.8", "license": "MIT", @@ -37569,14 +37103,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/suspend-react": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/suspend-react/-/suspend-react-0.0.8.tgz", - "integrity": "sha512-ZC3r8Hu1y0dIThzsGw0RLZplnX9yXwfItcvaIzJc2VQVi8TGyGDlu92syMB5ulybfvGLHAI5Ghzlk23UBPF8xg==", - "peerDependencies": { - "react": ">=17.0" - } - }, "node_modules/swiper": { "version": "6.8.4", "funding": [ @@ -37957,35 +37483,6 @@ "resolved": "https://registry.npmjs.org/three/-/three-0.154.0.tgz", "integrity": "sha512-Uzz8C/5GesJzv8i+Y2prEMYUwodwZySPcNhuJUdsVMH2Yn4Nm8qlbQe6qRN5fOhg55XB0WiLfTPBxVHxpE60ug==" }, - "node_modules/three-mesh-bvh": { - "version": "0.5.24", - "resolved": "https://registry.npmjs.org/three-mesh-bvh/-/three-mesh-bvh-0.5.24.tgz", - "integrity": "sha512-VTIgfjz8aFoPKTQoMIQQv9jJD4ybFRZuKKE1/kqy78FQcuHQ0+iIWv7C5cSb2inlvs7bNMVY3yRx3RXGZfrvzQ==", - "peerDependencies": { - "three": ">= 0.123.0" - } - }, - "node_modules/three-stdlib": { - "version": "2.23.13", - "resolved": "https://registry.npmjs.org/three-stdlib/-/three-stdlib-2.23.13.tgz", - "integrity": "sha512-WU4XHs4E6szAyoREzTs5bUAc1WFadiz5jRjNlA7aIIqf+raT4jfA320P0XmlyZz/wJwmpZnOuki4kAFsmadkTQ==", - "dependencies": { - "@types/draco3d": "^1.4.0", - "@types/offscreencanvas": "^2019.6.4", - "@types/webxr": "^0.5.2", - "chevrotain": "^10.1.2", - "draco3d": "^1.4.1", - "fflate": "^0.6.9", - "ktx-parse": "^0.4.5", - "mmd-parser": "^1.0.4", - "opentype.js": "^1.3.3", - "potpack": "^1.0.1", - "zstddec": "^0.0.2" - }, - "peerDependencies": { - "three": ">=0.128.0" - } - }, "node_modules/throat": { "version": "6.0.2", "dev": true, @@ -38020,11 +37517,6 @@ "version": "2.1.0", "license": "MIT" }, - "node_modules/tiny-inflate": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz", - "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==" - }, "node_modules/tiny-invariant": { "version": "1.3.1", "license": "MIT" @@ -38224,33 +37716,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/troika-three-text": { - "version": "0.46.4", - "resolved": "https://registry.npmjs.org/troika-three-text/-/troika-three-text-0.46.4.tgz", - "integrity": "sha512-Qsv0HhUKTZgSmAJs5wvO7YlBoJSP9TGPLmrg+K9pbQq4lseQdcevbno/WI38bwJBZ/qS56hvfqEzY0zUEFzDIw==", - "dependencies": { - "bidi-js": "^1.0.2", - "troika-three-utils": "^0.46.0", - "troika-worker-utils": "^0.46.0", - "webgl-sdf-generator": "1.1.1" - }, - "peerDependencies": { - "three": ">=0.103.0" - } - }, - "node_modules/troika-three-utils": { - "version": "0.46.0", - "resolved": "https://registry.npmjs.org/troika-three-utils/-/troika-three-utils-0.46.0.tgz", - "integrity": "sha512-llHyrXAcwzr0bpg80GxsIp73N7FuImm4WCrKDJkAqcAsWmE5pfP9+Qzw+oMWK1P/AdHQ79eOrOl9NjyW4aOw0w==", - "peerDependencies": { - "three": ">=0.103.0" - } - }, - "node_modules/troika-worker-utils": { - "version": "0.46.0", - "resolved": "https://registry.npmjs.org/troika-worker-utils/-/troika-worker-utils-0.46.0.tgz", - "integrity": "sha512-bzOx5f2ZBxkFhXtIvDJlLn2AI3bzCkGVbCndl/2dL5QZrwHEKl45OEIilCxYQQWJG1rEbOD9O80tMjoYjw19OA==" - }, "node_modules/trough": { "version": "1.0.5", "license": "MIT", @@ -40049,16 +39514,6 @@ "version": "1.2.0", "license": "Apache-2.0" }, - "node_modules/webgl-constants": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/webgl-constants/-/webgl-constants-1.1.1.tgz", - "integrity": "sha512-LkBXKjU5r9vAW7Gcu3T5u+5cvSvh5WwINdr0C+9jpzVB41cjQAP5ePArDtk/WHYdVj0GefCgM73BA7FlIiNtdg==" - }, - "node_modules/webgl-sdf-generator": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/webgl-sdf-generator/-/webgl-sdf-generator-1.1.1.tgz", - "integrity": "sha512-9Z0JcMTFxeE+b2x1LJTdnaT8rT8aEp7MVxkNwoycNmJWwPdzoXzMh0BjJSh/AEFP+KPYZUli814h8bJZFIZ2jA==" - }, "node_modules/webidl-conversions": { "version": "6.1.0", "dev": true, @@ -40845,11 +40300,6 @@ "url": "https://github.com/sponsors/colinhacks" } }, - "node_modules/zstddec": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/zstddec/-/zstddec-0.0.2.tgz", - "integrity": "sha512-DCo0oxvcvOTGP/f5FA6tz2Z6wF+FIcEApSTu0zV5sQgn9hoT5lZ9YRAKUraxt9oP7l4e8TnNdi8IZTCX6WCkwA==" - }, "node_modules/zustand": { "version": "3.7.2", "resolved": "https://registry.npmjs.org/zustand/-/zustand-3.7.2.tgz", @@ -40885,10 +40335,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": "*", @@ -40905,7 +40355,6 @@ }, "packages/common/node_modules/@types/react": { "version": "17.0.50", - "dev": true, "license": "MIT", "dependencies": { "@types/prop-types": "*", @@ -41148,7 +40597,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", @@ -43283,7 +42732,7 @@ "allure-commandline": "^2.17.2", "allure-js-commons": "^2.0.0-beta.14", "cross-fetch": "^3.1.5", - "eslint": "^8.41.0", + "eslint": "^8.45.0", "jest": "^27.4.7", "jest-allure2-adapter": "^0.3.11", "postgres": "^3.0.5", @@ -43292,70 +42741,106 @@ "typescript": "^5.0.4" } }, + "tests/node_modules/@eslint/eslintrc": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.0.tgz", + "integrity": "sha512-Lj7DECXqIVCqnqjjHMPna4vn6GJcMgul/wuS0je9OZ9gsL0zzDpKPVtcG1HaDVc+9y+qgXneTeUMbCqXJNpH1A==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "tests/node_modules/@eslint/js": { + "version": "8.44.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.44.0.tgz", + "integrity": "sha512-Ag+9YM4ocKQx9AarydN0KY2j0ErMHNIocPDrVo8zAE44xLTjEtz81OdR68/cydGtk6m6jDb5Za3r2useMzYmSw==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, "tests/node_modules/@humanwhocodes/config-array": { - "version": "0.10.7", + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.10.tgz", + "integrity": "sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==", "dev": true, - "license": "Apache-2.0", "dependencies": { "@humanwhocodes/object-schema": "^1.2.1", "debug": "^4.1.1", - "minimatch": "^3.0.4" + "minimatch": "^3.0.5" }, "engines": { "node": ">=10.10.0" } }, - "tests/node_modules/array-union": { - "version": "2.1.0", + "tests/node_modules/acorn": { + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", + "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", "dev": true, - "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, "engines": { - "node": ">=8" + "node": ">=0.4.0" } }, "tests/node_modules/eslint": { - "version": "8.24.0", + "version": "8.45.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.45.0.tgz", + "integrity": "sha512-pd8KSxiQpdYRfYa9Wufvdoct3ZPQQuVuU5O6scNgMuOMYuxvH0IGaYK0wUFjo4UYYQQCUndlXiMbnxopwvvTiw==", "dev": true, - "license": "MIT", "dependencies": { - "@eslint/eslintrc": "^1.3.2", - "@humanwhocodes/config-array": "^0.10.5", - "@humanwhocodes/gitignore-to-minimatch": "^1.0.2", + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.4.0", + "@eslint/eslintrc": "^2.1.0", + "@eslint/js": "8.44.0", + "@humanwhocodes/config-array": "^0.11.10", "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", "ajv": "^6.10.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", "debug": "^4.3.2", "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.1.1", - "eslint-utils": "^3.0.0", - "eslint-visitor-keys": "^3.3.0", - "espree": "^9.4.0", - "esquery": "^1.4.0", + "eslint-scope": "^7.2.0", + "eslint-visitor-keys": "^3.4.1", + "espree": "^9.6.0", + "esquery": "^1.4.2", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^6.0.1", "find-up": "^5.0.0", - "glob-parent": "^6.0.1", - "globals": "^13.15.0", - "globby": "^11.1.0", - "grapheme-splitter": "^1.0.4", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", "ignore": "^5.2.0", - "import-fresh": "^3.0.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", - "js-sdsl": "^4.1.4", + "is-path-inside": "^3.0.3", "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", - "optionator": "^0.9.1", - "regexpp": "^3.2.0", + "optionator": "^0.9.3", "strip-ansi": "^6.0.1", - "strip-json-comments": "^3.1.0", "text-table": "^0.2.0" }, "bin": { @@ -43368,23 +42853,78 @@ "url": "https://opencollective.com/eslint" } }, - "tests/node_modules/globby": { - "version": "11.1.0", + "tests/node_modules/eslint-scope": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.1.tgz", + "integrity": "sha512-CvefSOsDdaYYvxChovdrPo/ZGt8d5lrJWleAc1diXRKhHGiTYEI26cvo8Kle/wGnsizoCJjK73FMg1/IkIwiNA==", "dev": true, - "license": "MIT", "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" }, "engines": { - "node": ">=10" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://opencollective.com/eslint" + } + }, + "tests/node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "tests/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "tests/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "tests/node_modules/optionator": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", + "dev": true, + "dependencies": { + "@aashutoshrathi/word-wrap": "^1.2.3", + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0" + }, + "engines": { + "node": ">= 0.8.0" } }, "tests/node_modules/typescript": { diff --git a/packages/common/Providers.tsx b/packages/common/Providers.tsx index 0312f11c17a..4da5e7d6e2a 100644 --- a/packages/common/Providers.tsx +++ b/packages/common/Providers.tsx @@ -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) => { diff --git a/packages/common/package.json b/packages/common/package.json index c713b87226a..6722c954a59 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -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": "*", diff --git a/packages/ui/index.tsx b/packages/ui/index.tsx index 88f5d9b0ef5..e53f54d332f 100644 --- a/packages/ui/index.tsx +++ b/packages/ui/index.tsx @@ -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' diff --git a/packages/ui/package.json b/packages/ui/package.json index 207a450235d..59f0f58500e 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -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", diff --git a/packages/ui/src/components/Dropdown/Dropdown.tsx b/packages/ui/src/components/Dropdown/Dropdown.tsx index 9e9d688f687..a1c1a3b25fe 100644 --- a/packages/ui/src/components/Dropdown/Dropdown.tsx +++ b/packages/ui/src/components/Dropdown/Dropdown.tsx @@ -103,7 +103,7 @@ export function Item({ icon, className, ...props }: ItemProps) { {...props} > {icon && icon} - {props.children} + {props.children} ) } diff --git a/packages/ui/src/components/shadcn/ui/accordion.tsx b/packages/ui/src/components/shadcn/ui/accordion.tsx index c5950299253..c5d5ec3dc15 100644 --- a/packages/ui/src/components/shadcn/ui/accordion.tsx +++ b/packages/ui/src/components/shadcn/ui/accordion.tsx @@ -16,8 +16,8 @@ AccordionItem.displayName = 'AccordionItem' const AccordionTrigger = React.forwardRef< React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, children, ...props }, ref) => ( + React.ComponentPropsWithoutRef & { hideIcon?: boolean } +>(({ className, children, hideIcon, ...props }, ref) => ( {children} - + {!hideIcon && } )) diff --git a/packages/ui/src/components/shadcn/ui/command.tsx b/packages/ui/src/components/shadcn/ui/command.tsx index 24f15e9e715..2f57d3bd0d7 100644 --- a/packages/ui/src/components/shadcn/ui/command.tsx +++ b/packages/ui/src/components/shadcn/ui/command.tsx @@ -70,8 +70,12 @@ CommandList.displayName = CommandPrimitive.List.displayName const CommandEmpty = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef ->((props, ref) => ( - +>(({ className, ...props }, ref) => ( + )) CommandEmpty.displayName = CommandPrimitive.Empty.displayName diff --git a/packages/ui/src/components/shadcn/ui/dropdown-menu.tsx b/packages/ui/src/components/shadcn/ui/dropdown-menu.tsx index 93eb4f2b669..e4141e48221 100644 --- a/packages/ui/src/components/shadcn/ui/dropdown-menu.tsx +++ b/packages/ui/src/components/shadcn/ui/dropdown-menu.tsx @@ -153,7 +153,7 @@ const DropdownMenuSeparator = React.forwardRef< >(({ className, ...props }, ref) => ( )) diff --git a/packages/ui/src/components/shadcn/ui/popover.tsx b/packages/ui/src/components/shadcn/ui/popover.tsx index 93e67689c75..b4bb221aa94 100644 --- a/packages/ui/src/components/shadcn/ui/popover.tsx +++ b/packages/ui/src/components/shadcn/ui/popover.tsx @@ -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 + const PopoverContent = React.forwardRef< React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, align = 'center', sideOffset = 4, ...props }, ref) => ( - - - -)) -PopoverContent.displayName = PopoverPrimitive.Content.displayName + PopoverContentProps +>(({ className, align = 'center', sideOffset = 4, portal = false, ...props }, ref) => { + const Portal = portal ? PopoverPrimitive.Portal : React.Fragment + return ( + + + + ) +}) +PopoverContent.displayName = 'PopoverContent' export { Popover, PopoverTrigger, PopoverContent } diff --git a/packages/ui/src/layout/banners/LW8CountdownBanner/LW8CountdownBanner.tsx b/packages/ui/src/layout/banners/LW8CountdownBanner/LW8CountdownBanner.tsx index e1b9a590ff1..25593b32db9 100644 --- a/packages/ui/src/layout/banners/LW8CountdownBanner/LW8CountdownBanner.tsx +++ b/packages/ui/src/layout/banners/LW8CountdownBanner/LW8CountdownBanner.tsx @@ -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) { +function CountdownButton({ children }: PropsWithChildren<{}>) { return ( - + ) } @@ -37,7 +30,7 @@ export function LW8CountdownBanner() {

Supabase Launch Week 8

- Live now + Live now
) @@ -58,9 +51,7 @@ export function LW8CountdownBanner() {
{!isLaunchWeekPage && (
- - Get your ticket - + Get your ticket
)}
diff --git a/spec/cli_v1_commands.yaml b/spec/cli_v1_commands.yaml index f991af72ffc..50557aa26c1 100644 --- a/spec/cli_v1_commands.yaml +++ b/spec/cli_v1_commands.yaml @@ -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 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 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 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 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 + 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 [flags] + subcommands: [] + flags: + - id: git-branch + name: --git-branch + description: Change the associated git branch. + default_value: '' + - id: name + name: --name + 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 + 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 + 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 [flags] + subcommands: [] + flags: + - id: region + name: --region + 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' diff --git a/spec/common-cli-sections.json b/spec/common-cli-sections.json index ba3c23a319f..12b83619e6e 100644 --- a/spec/common-cli-sections.json +++ b/spec/common-cli-sections.json @@ -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", diff --git a/spec/supabase_dart_v1.yml b/spec/supabase_dart_v1.yml index 46bf3067102..70a69f18086 100644 --- a/spec/supabase_dart_v1.yml +++ b/spec/supabase_dart_v1.yml @@ -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 diff --git a/studio/components/interfaces/Account/AccessTokenList.tsx b/studio/components/interfaces/Account/AccessTokenList.tsx index 5746aab2995..9b76f1c93b0 100644 --- a/studio/components/interfaces/Account/AccessTokenList.tsx +++ b/studio/components/interfaces/Account/AccessTokenList.tsx @@ -25,7 +25,7 @@ const AccessTokenList = observer(() => { return ( <> -
+
Token, diff --git a/studio/components/interfaces/App/AppBannerWrapper.tsx b/studio/components/interfaces/App/AppBannerWrapper.tsx index 3f3faa61251..581ca0c6669 100644 --- a/studio/components/interfaces/App/AppBannerWrapper.tsx +++ b/studio/components/interfaces/App/AppBannerWrapper.tsx @@ -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 ( diff --git a/studio/components/interfaces/App/RouteValidationWrapper.tsx b/studio/components/interfaces/App/RouteValidationWrapper.tsx index aacae694dc3..9fd0b0c8cbf 100644 --- a/studio/components/interfaces/App/RouteValidationWrapper.tsx +++ b/studio/components/interfaces/App/RouteValidationWrapper.tsx @@ -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} } diff --git a/studio/components/interfaces/Auth/Auth.types.ts b/studio/components/interfaces/Auth/Auth.types.ts index 55b756c31bd..49973dde46b 100644 --- a/studio/components/interfaces/Auth/Auth.types.ts +++ b/studio/components/interfaces/Auth/Auth.types.ts @@ -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 diff --git a/studio/components/interfaces/Auth/SiteUrl/SiteUrl.tsx b/studio/components/interfaces/Auth/SiteUrl/SiteUrl.tsx index 17168594ddf..b19659bb96c 100644 --- a/studio/components/interfaces/Auth/SiteUrl/SiteUrl.tsx +++ b/studio/components/interfaces/Auth/SiteUrl/SiteUrl.tsx @@ -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 = { diff --git a/studio/components/interfaces/BillingV2/Subscription/BillingBreakdown.tsx b/studio/components/interfaces/BillingV2/Subscription/BillingBreakdown.tsx index 0e820f87202..c6096abdf49 100644 --- a/studio/components/interfaces/BillingV2/Subscription/BillingBreakdown.tsx +++ b/studio/components/interfaces/BillingV2/Subscription/BillingBreakdown.tsx @@ -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) => { ) : (
{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) => { )}
- {usageMeta.available_in_plan ? ( - 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 ? ( + 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 +
+

Unavailable in your plan

+ +
+ ) ) : ( - // [Joshen] Needs a better CTA here
-

Unavailable in your plan

- +

+ The usage for this metric is missing. +

)} diff --git a/studio/components/interfaces/BillingV2/Subscription/Subscription.constants.ts b/studio/components/interfaces/BillingV2/Subscription/Subscription.constants.ts index 6575d3b3ca6..5a1864369ce 100644 --- a/studio/components/interfaces/BillingV2/Subscription/Subscription.constants.ts +++ b/studio/components/interfaces/BillingV2/Subscription/Subscription.constants.ts @@ -102,4 +102,4 @@ export const BILLING_BREAKDOWN_METRICS = [ category: 'Edge Functions', unitName: 'function' }, -] +] as const diff --git a/studio/components/interfaces/Database/Schemas/SchemaGraph.tsx b/studio/components/interfaces/Database/Schemas/SchemaGraph.tsx index 8e1848854ed..4a9dfc3ac67 100644 --- a/studio/components/interfaces/Database/Schemas/SchemaGraph.tsx +++ b/studio/components/interfaces/Database/Schemas/SchemaGraph.tsx @@ -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 }} > diff --git a/studio/components/interfaces/Home/ProjectList/ProjectCard.tsx b/studio/components/interfaces/Home/ProjectList/ProjectCard.tsx index 20bf8488a3a..ad3689be4f9 100644 --- a/studio/components/interfaces/Home/ProjectList/ProjectCard.tsx +++ b/studio/components/interfaces/Home/ProjectList/ProjectCard.tsx @@ -24,7 +24,7 @@ const ProjectCard = ({ project, rewriteHref }: ProjectCardProps) => { const isRestoring = project.status === PROJECT_STATUS.RESTORING return ( -
  • +
  • - + ) @@ -79,7 +79,7 @@ const IntegrationInstallation = React.forwardRef
    @@ -146,7 +146,7 @@ const IntegrationConnection = React.forwardRef
    -
    +
    @@ -194,7 +194,9 @@ const IntegrationConnectionOption = React.forwardRef
    diff --git a/studio/components/interfaces/Integrations/ProjectLinker.tsx b/studio/components/interfaces/Integrations/ProjectLinker.tsx index 1df94aed64d..8aefbcc265e 100644 --- a/studio/components/interfaces/Integrations/ProjectLinker.tsx +++ b/studio/components/interfaces/Integrations/ProjectLinker.tsx @@ -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(null) + const foreignProjectsComboBoxRef = useRef(null) + const selectedOrganization = useSelectedOrganization() - const [supabaseProjectRef, setSupabaseProjectRef] = useState(UNDEFINED_SELECT_VALUE) - const [foreignProjectId, setForeignProjectId] = useState(UNDEFINED_SELECT_VALUE) + const [supabaseProjectRef, setSupabaseProjectRef] = useState(undefined) + const [foreignProjectId, setForeignProjectId] = useState(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 (
    x.foreign_project_id)) - - // check that foreign project is not already installed - const filteredForeignProjects = foreignProjects.filter((foreignProject) => { - return !flatInstalledConnectionsIds.has(foreignProject.id) - }) - return (
    @@ -104,66 +131,149 @@ const ProjectLinker = ({
    Supabase
    - setSupabaseProjectRef(e)} + + - - Choose project - - {supabaseProjects.map((project) => ( - { - return ( - <> -
    - Supabase -
    - - ) - }} + +
    + } + iconRight={ + + + + } > - {project.name} - - ))} - + {selectedSupabaseProject ? selectedSupabaseProject.name : 'Choose Project'} + + + + + + + No results found. + + {supabaseProjects.map((project) => { + return ( + { + if (ref) setSupabaseProjectRef(ref) + setSupabaseProjectsComboboxOpen(false) + }} + > +
    + Supabase +
    + {project.name} +
    + ) + })} +
    +
    +
    +
    +
    - {integrationIcon} + + +
    - setForeignProjectId(e)} + + - - {choosePrompt} - - {filteredForeignProjects.map((project) => { - return ( - { - return getForeignProjectIcon?.(project) ?? null - }} - > - {project.name} - - ) - })} - + + + + + + + + No results found. + + {filteredForeignProjects.map((project) => { + return ( + { + if (id) setForeignProjectId(id) + setForeignProjectsComboboxOpen(false) + }} + > + {getForeignProjectIcon?.(project) ?? integrationIcon} + {project.name} + + ) + })} + + + + +
    @@ -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 diff --git a/studio/components/interfaces/Organization/GeneralSettings/GeneralSettings.tsx b/studio/components/interfaces/Organization/GeneralSettings/GeneralSettings.tsx index 73b69322234..00489485bea 100644 --- a/studio/components/interfaces/Organization/GeneralSettings/GeneralSettings.tsx +++ b/studio/components/interfaces/Organization/GeneralSettings/GeneralSettings.tsx @@ -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() diff --git a/studio/components/interfaces/Settings/Infrastructure/InfrastructureInfo.tsx b/studio/components/interfaces/Settings/Infrastructure/InfrastructureInfo.tsx index 5e81d11cadc..ddd6c3498f2 100644 --- a/studio/components/interfaces/Settings/Infrastructure/InfrastructureInfo.tsx +++ b/studio/components/interfaces/Settings/Infrastructure/InfrastructureInfo.tsx @@ -82,7 +82,7 @@ const InfrastructureInfo = () => { label="Current version" actions={[ isOnLatestVersion && ( - + Latest diff --git a/studio/components/layouts/AccountLayout/AccountLayout.tsx b/studio/components/layouts/AccountLayout/AccountLayout.tsx index bf3734d64af..7efdbaa5e11 100644 --- a/studio/components/layouts/AccountLayout/AccountLayout.tsx +++ b/studio/components/layouts/AccountLayout/AccountLayout.tsx @@ -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 { await signOut() - await router.push('/sign-in') } @@ -126,6 +127,10 @@ const AccountLayout = ({ children, title, breadcrumbs }: PropsWithChildren{children} + } + return ( <> @@ -137,7 +142,12 @@ const AccountLayout = ({ children, title, breadcrumbs }: PropsWithChildren - + {children} diff --git a/studio/components/layouts/AccountLayout/WithSidebar.tsx b/studio/components/layouts/AccountLayout/WithSidebar.tsx index 4413ab6a3cb..cdf48e39a65 100644 --- a/studio/components/layouts/AccountLayout/WithSidebar.tsx +++ b/studio/components/layouts/AccountLayout/WithSidebar.tsx @@ -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 = ({ +const WithSidebar = ({ title, header, breadcrumbs = [], @@ -37,9 +37,10 @@ const WithSidebar: FC = ({ 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 = ({
    )}
    - + {!navLayoutV2 && }
    {children}
    diff --git a/studio/components/layouts/AppLayout/AppHeader.tsx b/studio/components/layouts/AppLayout/AppHeader.tsx new file mode 100644 index 00000000000..5ea83c6e56a --- /dev/null +++ b/studio/components/layouts/AppLayout/AppHeader.tsx @@ -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 ( +
    +
    + + + Supabase + + + + {ref !== undefined && } +
    + +
    +
    + + + + +
    +
    + +
    +
    +
    + ) +} + +export default AppHeader diff --git a/studio/components/layouts/AppLayout/AppLayout.tsx b/studio/components/layouts/AppLayout/AppLayout.tsx new file mode 100644 index 00000000000..5abf9b3a826 --- /dev/null +++ b/studio/components/layouts/AppLayout/AppLayout.tsx @@ -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 ( +
    + {navLayoutV2 && } +
    + {children} +
    +
    + ) +} + +export default AppLayout diff --git a/studio/components/layouts/AppLayout/IncidentBanner.tsx b/studio/components/layouts/AppLayout/IncidentBanner.tsx new file mode 100644 index 00000000000..4004c1922cb --- /dev/null +++ b/studio/components/layouts/AppLayout/IncidentBanner.tsx @@ -0,0 +1,19 @@ +import Link from 'next/link' +import { IconExternalLink } from 'ui' + +const IncidentBanner = () => { + return ( + + +
    +

    + We are currently investigating a technical issue, follow status.supabase.com for updates +

    + +
    +
    + + ) +} + +export default IncidentBanner diff --git a/studio/components/layouts/AppLayout/OrganizationDropdown.tsx b/studio/components/layouts/AppLayout/OrganizationDropdown.tsx new file mode 100644 index 00000000000..035e4073307 --- /dev/null +++ b/studio/components/layouts/AppLayout/OrganizationDropdown.tsx @@ -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(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 + } + + return ( +
    + + +

    {orgName}

    + {isSuccess && {subscription?.plan.name}} +
    + + + + +
    + ) +} + +export default OrganizationDropdown diff --git a/studio/components/layouts/AppLayout/ProjectDropdown.tsx b/studio/components/layouts/AppLayout/ProjectDropdown.tsx new file mode 100644 index 00000000000..9a882441d23 --- /dev/null +++ b/studio/components/layouts/AppLayout/ProjectDropdown.tsx @@ -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 ( + + { + router.push(href) + setOpen(false) + }} + onClick={() => setOpen(false)} + > + + {project.name} + {project.ref === ref && } + + + + ) +} + +const ProjectDropdown = () => { + const router = useRouter() + const { ref } = useParams() + const projectDetails = useSelectedProject() + const selectedOrganization = useSelectedOrganization() + const projectNameRef = useRef(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 + } + + return IS_PLATFORM ? ( +
    + + +

    {selectedProject?.name}

    + {isSuccess && !isOrgBilling && {subscription?.plan.name}} +
    + + + + +
    + ) : ( + + ) +} + +export default ProjectDropdown diff --git a/studio/components/layouts/AppLayout/SettingsButton.tsx b/studio/components/layouts/AppLayout/SettingsButton.tsx new file mode 100644 index 00000000000..7576d779903 --- /dev/null +++ b/studio/components/layouts/AppLayout/SettingsButton.tsx @@ -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 ( + + + + + + + + + + + +
    + Settings +
    +
    +
    +
    + ) +} + +export default SettingsButton diff --git a/studio/components/layouts/AppLayout/UserSettingsDropdown.tsx b/studio/components/layouts/AppLayout/UserSettingsDropdown.tsx new file mode 100644 index 00000000000..b1e37ff0ef9 --- /dev/null +++ b/studio/components/layouts/AppLayout/UserSettingsDropdown.tsx @@ -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 ( + setOpen(!open)} modal={false}> + + {profile?.first_name ? profile?.first_name?.[0] : } + + + +
    +

    + {profile?.first_name} {profile?.last_name} +

    +

    {profile?.primary_email}

    +
    + + + + { + router.push('/account/me') + }} + onClick={() => setOpen(false)} + asChild + > + Preferences + + + + { + router.push('/account/tokens') + }} + onClick={() => setOpen(false)} + asChild + > + Access tokens + + + + { + setOpen(false) + setCommandMenuOpen(true) + }} + > + Command menu + ⌘K + + + Theme + { + const dark = x === 'dark' + toggleTheme(Boolean(dark)) + }} + > + Dark + Light + + + + { + onClickLogout() + setOpen(false) + }} + > + Log out + +
    +
    +
    + ) +} + +export default UserSettingsDropdown diff --git a/studio/components/layouts/AuthLayout/AuthLayout.tsx b/studio/components/layouts/AuthLayout/AuthLayout.tsx index 6ffb6073271..72456fe6fc2 100644 --- a/studio/components/layouts/AuthLayout/AuthLayout.tsx +++ b/studio/components/layouts/AuthLayout/AuthLayout.tsx @@ -14,7 +14,7 @@ export interface AuthLayoutProps { } const AuthLayout = ({ title, children }: PropsWithChildren) => { - 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) => meta.tables.load() meta.roles.load() meta.schemas.load() + authConfig.load() } }, [ui.selectedProjectRef]) diff --git a/studio/components/layouts/DatabaseLayout/DatabaseLayout.tsx b/studio/components/layouts/DatabaseLayout/DatabaseLayout.tsx index aed4e7515c6..4368de9d49d 100644 --- a/studio/components/layouts/DatabaseLayout/DatabaseLayout.tsx +++ b/studio/components/layouts/DatabaseLayout/DatabaseLayout.tsx @@ -28,6 +28,7 @@ const DatabaseLayout = ({ title, children }: PropsWithChildren(isInitialized) @@ -78,7 +79,11 @@ const DatabaseLayout = ({ title, children }: PropsWithChildren } > diff --git a/studio/components/layouts/DatabaseLayout/DatabaseMenu.utils.ts b/studio/components/layouts/DatabaseLayout/DatabaseMenu.utils.ts index 728c4ab0841..44950c66181 100644 --- a/studio/components/layouts/DatabaseLayout/DatabaseMenu.utils.ts +++ b/studio/components/layouts/DatabaseLayout/DatabaseMenu.utils.ts @@ -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', diff --git a/studio/components/layouts/OrganizationLayout.tsx b/studio/components/layouts/OrganizationLayout.tsx index 91ec8e0880a..691f99f25b5 100644 --- a/studio/components/layouts/OrganizationLayout.tsx +++ b/studio/components/layouts/OrganizationLayout.tsx @@ -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 {children} + } return ( { +interface FeedbackDropdownProps { + alt?: boolean +} + +const FeedbackDropdown = ({ alt = false }: FeedbackDropdownProps) => { const [isOpen, setIsOpen] = useState(false) const [feedback, setFeedback] = useState('') const [screenshot, setScreenshot] = useState() @@ -18,16 +29,29 @@ const FeedbackDropdown = () => { } return ( - { setIsOpen(e) if (!e) setScreenshot(undefined) }} - size="content" - side="bottom" - align="end" - overlay={ + > + + + + { screenshot={screenshot} setScreenshot={setScreenshot} /> - } - > - - + + ) } diff --git a/studio/components/layouts/ProjectLayout/LayoutHeader/FeedbackDropdown/FeedbackWidget.tsx b/studio/components/layouts/ProjectLayout/LayoutHeader/FeedbackDropdown/FeedbackWidget.tsx index 0b9571b34ba..39851aa93b4 100644 --- a/studio/components/layouts/ProjectLayout/LayoutHeader/FeedbackDropdown/FeedbackWidget.tsx +++ b/studio/components/layouts/ProjectLayout/LayoutHeader/FeedbackDropdown/FeedbackWidget.tsx @@ -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 = ({ value={feedback} onChange={onFeedbackChange} /> - +
    - -
    {screenshot !== undefined ? (
    = ({ }} className="cursor-pointer rounded h-[26px] w-[30px] border border-scale-600 relative bg-cover bg-center bg-no-repeat" > -
    = ({ }} > -
    +
    ) : ( - + + + + + } - onClick={() => { + onSelect={() => { if (uploadButtonRef.current) (uploadButtonRef.current as any).click() }} > + Upload screenshot - , - + } - onClick={() => captureScreenshot()} + onSelect={() => captureScreenshot()} > + Capture screenshot - , - ]} - > - - + + + )} = () => { +const HelpPopover = ({ alt = false }: HelpPopoverProps) => { const router = useRouter() const projectRef = router.query.ref const supportUrl = `/support/new${projectRef ? `?ref=${projectRef}` : ''}` return ( - -
    -
    Need help with your project?
    -

    - For issues with your project hosted on supabase.com, or other inquiries about our - hosted services. -

    -
    - - - - - - - - - + + + + +
    +
    +
    +
    + {alt ? ( + + + +
    +

    Help

    +
    +
    +
    + ) : null} +
    + +
    +
    Need help with your project?
    +

    + For issues with your project hosted on supabase.com, or other inquiries about our hosted + services. +

    +
    + + + + + + + + + +
    +

    + Expected response time is based on your billing plan. Pro and Pay as You Go plans are + prioritized. +

    +
    + + + +
    +
    + +
    +
    +
    Reach out to the community
    +

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

    -
    - -
    + - -
    -
    -
    Reach out to the community
    - -

    - For other support, including questions on our client libraries, advice, or best - practices. -

    -
    -
    - } - > - - + + ) } diff --git a/studio/components/layouts/ProjectLayout/LayoutHeader/NotificationsPopover/index.tsx b/studio/components/layouts/ProjectLayout/LayoutHeader/NotificationsPopover/index.tsx index 1ff8530f549..d11b4f510a2 100644 --- a/studio/components/layouts/ProjectLayout/LayoutHeader/NotificationsPopover/index.tsx +++ b/studio/components/layouts/ProjectLayout/LayoutHeader/NotificationsPopover/index.tsx @@ -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() - 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 = () => { > -
    +
    + {hasNewNotifications && ( + <> + {alt ? null : ( +
    +
    +
    +
    + )} + + )} - {hasNewNotifications && ( -
    -
    -
    -
    - )}
    diff --git a/studio/components/layouts/ProjectLayout/LayoutHeader/ProjectDropdown.tsx b/studio/components/layouts/ProjectLayout/LayoutHeader/ProjectDropdown.tsx index 80152718fc4..232e27d29f5 100644 --- a/studio/components/layouts/ProjectLayout/LayoutHeader/ProjectDropdown.tsx +++ b/studio/components/layouts/ProjectLayout/LayoutHeader/ProjectDropdown.tsx @@ -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 + } + return IS_PLATFORM ? ( { } > - ) : ( - ) } diff --git a/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.tsx b/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.tsx index 50bdff1621b..4efb7bf31a4 100644 --- a/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.tsx +++ b/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.tsx @@ -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 = ({}) => { +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 = ({}) => { const { setIsOpen } = useCommandMenu() return (
      - - - Supabase - - + {!navLayoutV2 && ( + + + Supabase + + + )} = ({}) => { link: `/project/${projectRef}`, }} /> -
      +
      {toolRoutes.map((route) => ( = ({}) => { /> ))}
    -
      - {IS_PLATFORM && showCmdkHelper && ( - - - - - - - -
      + {IS_PLATFORM && showCmdkHelper && ( + + +
      -
      -
      -
      - )} - - {IS_PLATFORM && ( - <> - - }> - Account Preferences - - - - - )} - Theme - toggleTheme(e === 'dark')} - > - {/* [Joshen] Removing system default for now, needs to be supported in useTheme from common packages */} - {/* System default */} - Dark - Light - - - } - > - - -
    +
    + +
    + + + + + +
    + {os === 'macos' ? ( + + ) : ( +

    CTRL

    + )} +

    K

    +
    +
    +
    + + )} + + {IS_PLATFORM && ( + <> + + } + > + Account Preferences + + + + + )} + Theme + toggleTheme(e === 'dark')} + > + {/* [Joshen] Removing system default for now, needs to be supported in useTheme from common packages */} + {/* System default */} + Dark + Light + + + } + > + + + + )}
    ) } diff --git a/studio/components/layouts/ProjectLayout/ProductMenuBar.tsx b/studio/components/layouts/ProjectLayout/ProductMenuBar.tsx index aac652b82f0..35f9456eb82 100644 --- a/studio/components/layouts/ProjectLayout/ProductMenuBar.tsx +++ b/studio/components/layouts/ProjectLayout/ProductMenuBar.tsx @@ -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 = ({ title, children }) => { - const ongoingIncident = useFlag('ongoingIncident') - const maxHeight = ongoingIncident ? 'calc(100vh - 44px)' : '100vh' - +const ProductMenuBar = ({ title, children }: PropsWithChildren) => { return (
    = ({ title, children }) => { >

    {title}

    -
    {children}
    +
    + {children} +
    ) } diff --git a/studio/components/layouts/ProjectLayout/ProjectLayout.tsx b/studio/components/layouts/ProjectLayout/ProjectLayout.tsx index 8d20baea54e..4b23582c1ef 100644 --- a/studio/components/layouts/ProjectLayout/ProjectLayout.tsx +++ b/studio/components/layouts/ProjectLayout/ProjectLayout.tsx @@ -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) => { 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 ( - - - - {title - ? `${title} | Supabase` - : selectedTable - ? `${selectedTable} | ${projectName} | ${organizationName} | Supabase` - : projectName - ? `${projectName} | ${organizationName} | Supabase` - : organizationName - ? `${organizationName} | Supabase` - : 'Supabase'} - - - -
    - {/* Left-most navigation side bar to access products */} - {!hideIconBar && } + + + + + {title + ? `${title} | Supabase` + : selectedTable + ? `${selectedTable} | ${projectName} | ${organizationName} | Supabase` + : projectName + ? `${projectName} | ${organizationName} | Supabase` + : organizationName + ? `${organizationName} | Supabase` + : 'Supabase'} + + + +
    + {/* Left-most navigation side bar to access products */} + {!hideIconBar && } - {/* Product menu bar */} - {!showPausedState && ( - - {productMenu} - - )} - -
    - {!hideHeader && } - {showPausedState ? ( -
    -
    - -
    -
    - ) : ( - {children} + {/* Product menu bar */} + {!showPausedState && ( + + {productMenu} + )} -
    -
    -
    + +
    + {!navLayoutV2 && !hideHeader && } + {showPausedState ? ( +
    +
    + +
    +
    + ) : ( + {children} + )} +
    +
    +
    + ) } @@ -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 ( - - - {title ? `${title} | Supabase` : 'Supabase'} - - - -
    - {/* Left-most navigation side bar to access products */} - {!hideIconBar && } + + + + {title ? `${title} | Supabase` : 'Supabase'} + + + +
    + {/* Left-most navigation side bar to access products */} + {!hideIconBar && } - {/* Product menu bar */} - {productMenu && !showPausedState && ( - {productMenu} - )} - -
    - {!hideHeader && } - {showPausedState ? ( -
    -
    - -
    -
    - ) : ( - children + {/* Product menu bar */} + {productMenu && !showPausedState && ( + {productMenu} )} -
    -
    -
    + +
    + {!navLayoutV2 && !hideHeader && } + {showPausedState ? ( +
    +
    + +
    +
    + ) : ( + children + )} +
    +
    +
    + ) } diff --git a/studio/components/layouts/ProjectSettingsLayout/SettingsLayout.tsx b/studio/components/layouts/ProjectSettingsLayout/SettingsLayout.tsx new file mode 100644 index 00000000000..2793fa28c80 --- /dev/null +++ b/studio/components/layouts/ProjectSettingsLayout/SettingsLayout.tsx @@ -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) => { + 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 ( + } + > +
    + {children} +
    +
    + ) +} + +export default withAuth(observer(SettingsLayout)) diff --git a/studio/components/layouts/SettingsLayout/SettingsMenu.utils.ts b/studio/components/layouts/ProjectSettingsLayout/SettingsMenu.utils.ts similarity index 100% rename from studio/components/layouts/SettingsLayout/SettingsMenu.utils.ts rename to studio/components/layouts/ProjectSettingsLayout/SettingsMenu.utils.ts diff --git a/studio/components/layouts/Scaffold.tsx b/studio/components/layouts/Scaffold.tsx index aec55712385..bcf1cfd9bd3 100644 --- a/studio/components/layouts/Scaffold.tsx +++ b/studio/components/layouts/Scaffold.tsx @@ -53,7 +53,7 @@ const ScaffoldColumn = React.forwardRef ) } diff --git a/studio/components/layouts/SettingsLayout/AccountSettingsMenu.tsx b/studio/components/layouts/SettingsLayout/AccountSettingsMenu.tsx new file mode 100644 index 00000000000..80c3b593dd3 --- /dev/null +++ b/studio/components/layouts/SettingsLayout/AccountSettingsMenu.tsx @@ -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 ( +
    +
    +

    Account Settings

    + {accountSettings.map((link) => ( + + ))} +
    +
    + ) +} + +export default AccountSettingsMenu diff --git a/studio/components/layouts/SettingsLayout/OrganizationSettingsMenu.tsx b/studio/components/layouts/SettingsLayout/OrganizationSettingsMenu.tsx new file mode 100644 index 00000000000..ee1ff6fe3d7 --- /dev/null +++ b/studio/components/layouts/SettingsLayout/OrganizationSettingsMenu.tsx @@ -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 ( +
    +
    +

    Organization

    + {organizationSettings.map((link) => ( + + ))} +
    + + {/* [Joshen] Hiding below until we figure out a better UX for consolidating settings on projects */} + {/*
    +

    Projects

    + + {projects?.map((project) => ( + + ))} + +
    + */} +
    + ) +} + +export default OrganizationSettingsMenu diff --git a/studio/components/layouts/SettingsLayout/ProjectSettingsMenuItem.tsx b/studio/components/layouts/SettingsLayout/ProjectSettingsMenuItem.tsx new file mode 100644 index 00000000000..2132e439e40 --- /dev/null +++ b/studio/components/layouts/SettingsLayout/ProjectSettingsMenuItem.tsx @@ -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 ( + + +
    + +

    + {project.name} +

    +
    +
    + +
    + {projectSettings.map((link) => { + const href = link.pathname.replace('[ref]', project.ref) + return ( + + ) + })} +
    +
    +
    + ) +} + +export default ProjectSettingsMenuItem diff --git a/studio/components/layouts/SettingsLayout/SettingsLayout.tsx b/studio/components/layouts/SettingsLayout/SettingsLayout.tsx index 61c49d15af0..81ec13f6e89 100644 --- a/studio/components/layouts/SettingsLayout/SettingsLayout.tsx +++ b/studio/components/layouts/SettingsLayout/SettingsLayout.tsx @@ -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) => { - 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 ( - } - > -
    - {children} -
    -
    + +
    + {router.pathname !== '/projects' && ( +
    + {slug === undefined && ref === undefined ? ( + + ) : ( + + )} +
    + )} +
    {children}
    +
    +
    ) } -export default withAuth(observer(SettingsLayout)) +export default SettingsLayout diff --git a/studio/components/layouts/SettingsLayout/SettingsMenuItem.tsx b/studio/components/layouts/SettingsLayout/SettingsMenuItem.tsx new file mode 100644 index 00000000000..e8bcf9b13a0 --- /dev/null +++ b/studio/components/layouts/SettingsLayout/SettingsMenuItem.tsx @@ -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 ( + + ) +} + +export default SettingsMenuItem diff --git a/studio/components/layouts/TableEditorLayout/TableEditorMenu.tsx b/studio/components/layouts/TableEditorLayout/TableEditorMenu.tsx index 1e8f7e114e6..7e7190a73e9 100644 --- a/studio/components/layouts/TableEditorLayout/TableEditorMenu.tsx +++ b/studio/components/layouts/TableEditorLayout/TableEditorMenu.tsx @@ -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 = ({ Loading schemas...
    + ) : meta.schemas.hasError ? ( + +

    Error: {meta.schemas?.error?.message}

    + +
    ) : ( = ({ keyMap }) => { +const KeyMap = ({ keyMap }: any) => { if (!keyMap) return null const keys = keyMap.split('+') diff --git a/studio/components/to-be-cleaned/ProductEmptyState.tsx b/studio/components/to-be-cleaned/ProductEmptyState.tsx index be23e0edc04..33ee6b1e1a8 100644 --- a/studio/components/to-be-cleaned/ProductEmptyState.tsx +++ b/studio/components/to-be-cleaned/ProductEmptyState.tsx @@ -39,7 +39,7 @@ const ProductEmptyState: FC = ({
    {ctaButtonLabel && onClickCta && ( - + diff --git a/studio/data/organizations/organizations-query.ts b/studio/data/organizations/organizations-query.ts index 98a2795b55b..4bf70566dd8 100644 --- a/studio/data/organizations/organizations-query.ts +++ b/studio/data/organizations/organizations-query.ts @@ -9,8 +9,11 @@ export async function getOrganizations(signal?: AbortSignal): Promise 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 } diff --git a/studio/hooks/misc/useCheckPermissions.ts b/studio/hooks/misc/useCheckPermissions.ts index 257b4482a67..f1228eda934 100644 --- a/studio/hooks/misc/useCheckPermissions.ts +++ b/studio/hooks/misc/useCheckPermissions.ts @@ -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 && diff --git a/studio/hooks/misc/useSelectedOrganization.ts b/studio/hooks/misc/useSelectedOrganization.ts index c2446380fc5..bc6be1b3710 100644 --- a/studio/hooks/misc/useSelectedOrganization.ts +++ b/studio/hooks/misc/useSelectedOrganization.ts @@ -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]) } diff --git a/studio/hooks/misc/useSelectedProject.ts b/studio/hooks/misc/useSelectedProject.ts index 824bb14a073..d5c1d82dfc2 100644 --- a/studio/hooks/misc/useSelectedProject.ts +++ b/studio/hooks/misc/useSelectedProject.ts @@ -4,6 +4,5 @@ import { useProjectDetailQuery } from 'data/projects/project-detail-query' export function useSelectedProject() { const { ref } = useParams() const { data } = useProjectDetailQuery({ ref }) - return data } diff --git a/studio/lib/common/fetch/base.ts b/studio/lib/common/fetch/base.ts index e7cdfdeceb1..1e338f28ca1 100644 --- a/studio/lib/common/fetch/base.ts +++ b/studio/lib/common/fetch/base.ts @@ -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(e: any, requestId: string): SupaResponse { const message = e?.message ? `An error has occurred: ${e.message}` : 'An error has occurred' @@ -86,15 +87,32 @@ export async function handleResponseError( } } +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 }) { diff --git a/studio/lib/constants/index.ts b/studio/lib/constants/index.ts index 24743c4904f..3e3b66f24b2 100644 --- a/studio/lib/constants/index.ts +++ b/studio/lib/constants/index.ts @@ -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', +} diff --git a/studio/pages/_app.tsx b/studio/pages/_app.tsx index 6051ab68387..b0652d80767 100644 --- a/studio/pages/_app.tsx +++ b/studio/pages/_app.tsx @@ -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()) diff --git a/studio/pages/api/auth/[ref]/config.ts b/studio/pages/api/auth/[ref]/config.ts index 34878dd37f3..aa9cd2c48bc 100644 --- a/studio/pages/api/auth/[ref]/config.ts +++ b/studio/pages/api/auth/[ref]/config.ts @@ -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, diff --git a/studio/pages/integrations/vercel/[slug]/marketplace/choose-project.tsx b/studio/pages/integrations/vercel/[slug]/marketplace/choose-project.tsx index aeaa77c4995..02228325215 100644 --- a/studio/pages/integrations/vercel/[slug]/marketplace/choose-project.tsx +++ b/studio/pages/integrations/vercel/[slug]/marketplace/choose-project.tsx @@ -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 = ( @@ -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 ( <> +
    - <> -
    -

    - Link a Supabase project to a Vercel project -

    - +
    +

    + Link a Supabase project to a Vercel project +

    + +
    + + { + if (next) { + window.location.href = next + } + }} + loadingForeignProjects={isLoadingVercelProjectsData} + loadingSupabaseProjects={isLoadingSupabaseProjectsData} /> - - { - if (next) { - window.location.href = next - } - }} - /> - { @@ -173,7 +179,8 @@ ${ENV_VAR_RAW_KEYS.map((x) => { ` })} `} - /> + /> + diff --git a/studio/pages/integrations/vercel/install.tsx b/studio/pages/integrations/vercel/install.tsx index 89ca43f3906..a93bee324e4 100644 --- a/studio/pages/integrations/vercel/install.tsx +++ b/studio/pages/integrations/vercel/install.tsx @@ -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(null) + const [selectedOrg, setSelectedOrg] = useState( + null + ) const [organizationIntegrationId, setOrganizationIntegrationId] = useState(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 ( <>
    @@ -168,24 +211,40 @@ const VercelIntegration: NextPageWithLayout = () => { {organizationIntegrationId === null && ( <> - +

    Choose organization

    <> { - 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 && ( + + + + Vercel Integration is already installed. + + + You will need to choose another organization to install the integration. + + + )}
    - +
    Docs
    @@ -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(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 ( - - - Loading... - - - ) - } + const [open, setOpen] = useState(false) + const ref = useRef(null) return ( - - {organizationsWithInstalledData?.map((org) => { - const label = ( -
    - {org.name} - {org.installationInstalled && Integration Installed} -
    - ) - return ( - } + <> + + + + + + + + + No results found. + + {organizationsWithInstalledData?.map((org) => { + return ( + { + if (slug) onSelectedOrgChange(slug) + setOpen(false) + }} + > + + {org.name}{' '} + {org?.installationInstalled && ( + + Integration Installed + + )} + + ) + })} + + + + + + ) } diff --git a/studio/pages/org/[slug]/general.tsx b/studio/pages/org/[slug]/general.tsx index 245a3e23f05..d186365fad5 100644 --- a/studio/pages/org/[slug]/general.tsx +++ b/studio/pages/org/[slug]/general.tsx @@ -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 ? ( ) : ( - + )} ) diff --git a/studio/pages/org/[slug]/index.tsx b/studio/pages/org/[slug]/index.tsx new file mode 100644 index 00000000000..b1e1245555c --- /dev/null +++ b/studio/pages/org/[slug]/index.tsx @@ -0,0 +1,83 @@ +import Link from 'next/link' +import { Button, IconPlus } from 'ui' + +import ProjectCard from 'components/interfaces/Home/ProjectList/ProjectCard' +import ShimmeringCard from 'components/interfaces/Home/ProjectList/ShimmeringCard' +import AppLayout from 'components/layouts/AppLayout/AppLayout' +import { ScaffoldContainer, ScaffoldSection } from 'components/layouts/Scaffold' +import AlertError from 'components/ui/AlertError' +import { useProjectsQuery } from 'data/projects/projects-query' +import { useSelectedOrganization } from 'hooks' +import { NextPageWithLayout } from 'types' + +const ProjectsPage: NextPageWithLayout = () => { + const { + data: allProjects, + error: projectsError, + isLoading: isLoadingProjects, + isError: isErrorProjects, + isSuccess: isSuccessProjects, + } = useProjectsQuery() + + const organization = useSelectedOrganization() + const projects = allProjects?.filter((project) => project.organization_id === organization?.id) + + return ( + + +
    + + + + + +
    +

    Projects

    + {isLoadingProjects && ( +
      + + +
    + )} + {isErrorProjects && ( + + )} + {isSuccessProjects && ( + <> + {(projects?.length ?? 0) === 0 ? ( +
    +
    +

    No projects

    +

    + Get started by creating a new project. +

    +
    + +
    + ) : ( +
      + {projects?.map((project) => ( + + ))} +
    + )} + + )} +
    +
    +
    +
    + ) +} + +ProjectsPage.getLayout = (page) => {page} + +export default ProjectsPage diff --git a/studio/pages/project/[ref]/auth/providers.tsx b/studio/pages/project/[ref]/auth/providers.tsx index 982b2a7e4e8..1ddd4ccc705 100644 --- a/studio/pages/project/[ref]/auth/providers.tsx +++ b/studio/pages/project/[ref]/auth/providers.tsx @@ -1,10 +1,8 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' import { observer } from 'mobx-react-lite' -import { useEffect } from 'react' import { AuthProvidersForm } from 'components/interfaces' import { AuthLayout } from 'components/layouts' -import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' import { FormsContainer } from 'components/ui/Forms' import NoPermission from 'components/ui/NoPermission' import { useCheckPermissions, useStore } from 'hooks' @@ -12,12 +10,6 @@ import { NextPageWithLayout } from 'types' const PageLayout: NextPageWithLayout = () => { const { authConfig } = useStore() - const { project } = useProjectContext() - - useEffect(() => { - authConfig.load() - }, [project?.ref]) - const canReadAuthSettings = useCheckPermissions(PermissionAction.READ, 'custom_config_gotrue') if (!canReadAuthSettings) { diff --git a/studio/pages/project/[ref]/auth/templates.tsx b/studio/pages/project/[ref]/auth/templates.tsx index da945c6e87c..fe0f5d01b77 100644 --- a/studio/pages/project/[ref]/auth/templates.tsx +++ b/studio/pages/project/[ref]/auth/templates.tsx @@ -12,12 +12,6 @@ import { NextPageWithLayout } from 'types' const PageLayout: NextPageWithLayout = () => { const { authConfig } = useStore() - const { project } = useProjectContext() - - useEffect(() => { - authConfig.load() - }, [project?.ref]) - const canReadAuthSettings = useCheckPermissions(PermissionAction.READ, 'custom_config_gotrue') if (!canReadAuthSettings) { diff --git a/studio/pages/project/[ref]/auth/url-configuration.tsx b/studio/pages/project/[ref]/auth/url-configuration.tsx index a855a371b95..896c81a5225 100644 --- a/studio/pages/project/[ref]/auth/url-configuration.tsx +++ b/studio/pages/project/[ref]/auth/url-configuration.tsx @@ -1,11 +1,9 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' import { observer } from 'mobx-react-lite' -import { useEffect } from 'react' import RedirectUrls from 'components/interfaces/Auth/RedirectUrls/RedirectUrls' import SiteUrl from 'components/interfaces/Auth/SiteUrl/SiteUrl' import { AuthLayout } from 'components/layouts' -import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' import { FormsContainer } from 'components/ui/Forms' import NoPermission from 'components/ui/NoPermission' import { useCheckPermissions, useStore } from 'hooks' @@ -13,12 +11,6 @@ import { NextPageWithLayout } from 'types' const URLConfiguration: NextPageWithLayout = () => { const { authConfig } = useStore() - const { project } = useProjectContext() - - useEffect(() => { - authConfig.load() - }, [project?.ref]) - const canReadAuthSettings = useCheckPermissions(PermissionAction.READ, 'custom_config_gotrue') if (!canReadAuthSettings) { @@ -31,7 +23,7 @@ const URLConfiguration: NextPageWithLayout = () => { ) } else { - return
    + return null } } diff --git a/studio/pages/projects.tsx b/studio/pages/projects.tsx index 8fe0df5780d..5b26b3ccdb1 100644 --- a/studio/pages/projects.tsx +++ b/studio/pages/projects.tsx @@ -5,11 +5,15 @@ import AlertError from 'components/ui/AlertError' import Connecting from 'components/ui/Loading/Loading' import { useOrganizationsQuery } from 'data/organizations/organizations-query' import { useAutoProjectsPrefetch } from 'data/projects/projects-query' -import { IS_PLATFORM } from 'lib/constants' +import { useFlag } from 'hooks' +import { IS_PLATFORM, LOCAL_STORAGE_KEYS } from 'lib/constants' import { useProfile } from 'lib/profile' +import { useRouter } from 'next/router' +import { useEffect } from 'react' import { NextPageWithLayout } from 'types' const ProjectsPage: NextPageWithLayout = () => { + const router = useRouter() const { data: organizations, isLoading: isOrganizationLoading, @@ -19,24 +23,40 @@ const ProjectsPage: NextPageWithLayout = () => { useAutoProjectsPrefetch() const { isLoading: isProfileLoading } = useProfile() - const isLoading = isOrganizationLoading || isProfileLoading + const navLayoutV2 = useFlag('navigationLayoutV2') + const hasWindowLoaded = typeof window !== 'undefined' + + useEffect(() => { + if (navLayoutV2 && isSuccess && hasWindowLoaded) { + const localStorageSlug = localStorage.getItem( + LOCAL_STORAGE_KEYS.RECENTLY_VISITED_ORGANIZATION + ) + const verifiedSlug = organizations.some((org) => org.slug === localStorageSlug) + + if (organizations.length === 0) router.push('/new') + else if (localStorageSlug && verifiedSlug) router.push(`/org/${localStorageSlug}`) + else router.push(`/org/${organizations[0].slug}`) + } + }, [navLayoutV2, isSuccess, hasWindowLoaded]) return ( <> - {isLoading && ( -
    + {navLayoutV2 && isLoading && ( +
    )} {isError && ( -
    +
    )} - {isSuccess && ( + {!navLayoutV2 && isSuccess && (
    {IS_PLATFORM && (
    diff --git a/studio/pages/sign-in.tsx b/studio/pages/sign-in.tsx index a98b3b31dcc..39270ebdf65 100644 --- a/studio/pages/sign-in.tsx +++ b/studio/pages/sign-in.tsx @@ -15,7 +15,7 @@ const SignInPage: NextPageWithLayout = () => { useEffect(() => { // if the dashboard is running locally, redirect straight to the projects page if (!IS_PLATFORM) { - router.replace('/projects') + router.replace('/project/default') return } diff --git a/studio/public/img/icons/figma-icon.svg b/studio/public/img/icons/figma-icon.svg new file mode 100644 index 00000000000..38539ba64cd --- /dev/null +++ b/studio/public/img/icons/figma-icon.svg @@ -0,0 +1 @@ + diff --git a/studio/stores/authConfig/ProjectAuthConfigStore.ts b/studio/stores/authConfig/ProjectAuthConfigStore.ts index 090b3d645d5..92c43762a59 100644 --- a/studio/stores/authConfig/ProjectAuthConfigStore.ts +++ b/studio/stores/authConfig/ProjectAuthConfigStore.ts @@ -60,13 +60,10 @@ export default class ProjectAuthConfigStore implements IProjectAuthConfigStore { } async fetchData() { - const headers = { - 'Content-Type': 'application/json', - } + const headers = { 'Content-Type': 'application/json' } const response = await get(this.baseUrl, { headers }) - if (response.error) { - throw response.error - } + + if (response.error) throw response.error this.data = response return response } @@ -80,7 +77,6 @@ export default class ProjectAuthConfigStore implements IProjectAuthConfigStore { this.state = LOADED return this.data } catch (e: any) { - console.error('Failed to load auth config', e.message) this.error = e this.state = ERROR } diff --git a/studio/stores/authConfig/schema/AuthProviders/AuthProvidersFormValidation.tsx b/studio/stores/authConfig/schema/AuthProviders/AuthProvidersFormValidation.tsx index e4442f75f71..78d1113b998 100644 --- a/studio/stores/authConfig/schema/AuthProviders/AuthProvidersFormValidation.tsx +++ b/studio/stores/authConfig/schema/AuthProviders/AuthProvidersFormValidation.tsx @@ -666,6 +666,44 @@ const EXTERNAL_PROVIDER_FACEBOOK = { }, } +const EXTERNAL_PROVIDER_FIGMA = { + $schema: JSON_SCHEMA_VERSION, + type: 'object', + title: 'Figma', + properties: { + EXTERNAL_FIGMA_ENABLED: { + title: 'Figma enabled', + type: 'boolean', + }, + EXTERNAL_FIGMA_CLIENT_ID: { + title: 'Client ID', + type: 'string', + }, + EXTERNAL_FIGMA_SECRET: { + title: 'Client Secret', + type: 'string', + isSecret: true, + }, + }, + validationSchema: object().shape({ + EXTERNAL_FIGMA_ENABLED: boolean().required(), + EXTERNAL_FIGMA_CLIENT_ID: string().when('EXTERNAL_FIGMA_ENABLED', { + is: true, + then: (schema) => schema.required('Client ID is required'), + otherwise: (schema) => schema, + }), + EXTERNAL_FIGMA_SECRET: string().when('EXTERNAL_FIGMA_ENABLED', { + is: true, + then: (schema) => schema.required('Client Secret is required'), + otherwise: (schema) => schema, + }), + }), + misc: { + iconKey: 'figma-icon', + requiresRedirect: true, + }, +} + const EXTERNAL_PROVIDER_GITHUB = { $schema: JSON_SCHEMA_VERSION, type: 'object', @@ -1283,6 +1321,7 @@ export const PROVIDERS_SCHEMAS = [ EXTERNAL_PROVIDER_BITBUCKET, EXTERNAL_PROVIDER_DISCORD, EXTERNAL_PROVIDER_FACEBOOK, + EXTERNAL_PROVIDER_FIGMA, EXTERNAL_PROVIDER_GITHUB, EXTERNAL_PROVIDER_GITLAB, EXTERNAL_PROVIDER_GOOGLE, diff --git a/studio/stores/jsonSchema/auth_gotrue_config.json b/studio/stores/jsonSchema/auth_gotrue_config.json index 9d820c8d39b..7f87191cb6e 100644 --- a/studio/stores/jsonSchema/auth_gotrue_config.json +++ b/studio/stores/jsonSchema/auth_gotrue_config.json @@ -157,6 +157,18 @@ "title": "Facebook secret", "type": "string" }, + "EXTERNAL_FIGMA_ENABLED": { + "title": "Figma enabled", + "type": "boolean" + }, + "EXTERNAL_FIGMA_CLIENT_ID": { + "title": "Figma client ID", + "type": "string" + }, + "EXTERNAL_FIGMA_SECRET": { + "title": "Figma secret", + "type": "string" + }, "EXTERNAL_GITHUB_ENABLED": { "title": "GitHub enabled", "type": "boolean" diff --git a/supabase/config.toml b/supabase/config.toml index 7b1702d2d8e..f0c3d816b66 100644 --- a/supabase/config.toml +++ b/supabase/config.toml @@ -59,8 +59,8 @@ double_confirm_changes = true enable_confirmations = false # Use an external OAuth provider. The full list of providers are: `apple`, `azure`, `bitbucket`, -# `discord`, `facebook`, `github`, `gitlab`, `google`, `keycloak`, `linkedin`, `notion`, `twitch`, -# `twitter`, `slack`, `spotify`, `workos`, `zoom`. +# `discord`, `facebook`, `figma`, `github`, `gitlab`, `google`, `keycloak`, `linkedin`, `notion`, +# `twitch`, `twitter`, `slack`, `spotify`, `workos`, `zoom`. [auth.external.apple] enabled = false client_id = "" diff --git a/tests/package.json b/tests/package.json index 7a61abd619c..d3f91789c1b 100644 --- a/tests/package.json +++ b/tests/package.json @@ -27,7 +27,7 @@ "allure-commandline": "^2.17.2", "allure-js-commons": "^2.0.0-beta.14", "cross-fetch": "^3.1.5", - "eslint": "^8.41.0", + "eslint": "^8.45.0", "jest": "^27.4.7", "jest-allure2-adapter": "^0.3.11", "postgres": "^3.0.5",