diff --git a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts
index 7385afd2d3d..88c2d614951 100644
--- a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts
+++ b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts
@@ -553,8 +553,10 @@ export const functions = {
url: '/guides/functions/examples/connect-to-postgres',
items: [],
},
+ { name: 'Discord Bot', url: '/guides/functions/examples/discord-bot', items: [] },
{ name: 'GitHub Actions', url: '/guides/functions/examples/github-actions', items: [] },
{ name: 'OG Image', url: '/guides/functions/examples/og-image', items: [] },
+ { name: 'OpenAI', url: '/guides/functions/examples/openai', items: [] },
{ name: 'Storage Caching', url: '/guides/functions/examples/storage-caching', items: [] },
{ name: 'Stripe Webhooks', url: '/guides/functions/examples/stripe-webhooks', items: [] },
{ name: 'Telegram Bot', url: '/guides/functions/examples/telegram-bot', items: [] },
@@ -821,6 +823,7 @@ export const integrations = {
url: undefined,
items: [
{ name: 'Estuary', url: '/guides/integrations/estuary', items: [] },
+ { name: 'OpenAI', url: '/guides/functions/examples/openai', items: [] },
{ name: 'pgMustard', url: '/guides/integrations/pgmustard', items: [] },
{ name: 'Prisma', url: '/guides/integrations/prisma', items: [] },
{ name: 'Sequin', url: '/guides/integrations/sequin', items: [] },
diff --git a/apps/docs/components/Navigation/NavigationMenu/NavigationMenuGuideList.tsx b/apps/docs/components/Navigation/NavigationMenu/NavigationMenuGuideList.tsx
index 7e52d424832..c55ec3b6f0d 100644
--- a/apps/docs/components/Navigation/NavigationMenu/NavigationMenuGuideList.tsx
+++ b/apps/docs/components/Navigation/NavigationMenu/NavigationMenuGuideList.tsx
@@ -38,7 +38,7 @@ const NavigationMenuGuideList: React.FC = ({ id, active }) => {
// disabled
// level !== 'home' && level !== id ? '-ml-8' : '',
- !active ? 'opacity-0 invisible absolute' : '',
+ !active ? 'opacity-0 invisible absolute h-0 overflow-hidden' : '',
].join(' ')}
>
diff --git a/apps/docs/layouts/DefaultLayout.tsx b/apps/docs/layouts/DefaultLayout.tsx
index 3e610896b8b..0a6b48c5547 100644
--- a/apps/docs/layouts/DefaultLayout.tsx
+++ b/apps/docs/layouts/DefaultLayout.tsx
@@ -21,7 +21,7 @@ const Layout: FC = (props: Props) => {
{props.meta?.title} | Supabase Docs
-
+
diff --git a/apps/docs/layouts/tutorials/TutorialLayout.tsx b/apps/docs/layouts/tutorials/TutorialLayout.tsx
index fc1a2fa8ea9..2f58a612cca 100644
--- a/apps/docs/layouts/tutorials/TutorialLayout.tsx
+++ b/apps/docs/layouts/tutorials/TutorialLayout.tsx
@@ -50,7 +50,7 @@ const Layout: FC = (props: Props) => {
{props.meta?.title} | Supabase
-
+
diff --git a/apps/docs/pages/_document.tsx b/apps/docs/pages/_document.tsx
index 98acbb79912..ed9a8a70c10 100644
--- a/apps/docs/pages/_document.tsx
+++ b/apps/docs/pages/_document.tsx
@@ -3,7 +3,9 @@ import { Html, Head, Main, NextScript } from 'next/document'
export default function Document() {
return (
-
+
+
+
diff --git a/apps/docs/pages/guides/auth/auth-helpers/sveltekit.mdx b/apps/docs/pages/guides/auth/auth-helpers/sveltekit.mdx
index 7598037b2d8..8d73f79e224 100644
--- a/apps/docs/pages/guides/auth/auth-helpers/sveltekit.mdx
+++ b/apps/docs/pages/guides/auth/auth-helpers/sveltekit.mdx
@@ -137,7 +137,8 @@ In order to get the most out of TypeScript and it´s intellisense, you should im
// and what to do when importing types
declare namespace App {
interface Supabase {
- Database: import('./DatabaseDefinitions').Database
+ // Use the path to where you generated the types using the Supbase CLI.
+ Database: import('../types/supabase').Database;
SchemaName: 'public'
}
diff --git a/apps/docs/pages/guides/functions.mdx b/apps/docs/pages/guides/functions.mdx
index a382f7ccde1..b19ddb81872 100644
--- a/apps/docs/pages/guides/functions.mdx
+++ b/apps/docs/pages/guides/functions.mdx
@@ -43,12 +43,12 @@ export const examples = [
{
name: 'With supabase-js',
description: 'Use the Supabase client inside your Edge Function.',
- href: 'https://github.com/supabase/supabase/blob/master/examples/edge-functions/supabase/functions/select-from-table-with-auth-rls/index.ts',
+ href: '/guides/functions/auth',
},
{
name: 'With CORS headers',
description: 'Send CORS headers for invoking from the browser.',
- href: 'https://github.com/supabase/supabase/blob/master/examples/edge-functions/supabase/functions/browser-with-cors/index.ts',
+ href: '/guides/functions/cors',
},
{
name: 'React Native with Stripe',
@@ -86,6 +86,71 @@ export const examples = [
description: `Get user location data from user's IP address.`,
href: 'https://github.com/supabase/supabase/tree/master/examples/edge-functions/supabase/functions/location',
},
+ {
+ name: 'Cloudflare Turnstile',
+ description: `Protecting Forms with Cloudflare Turnstile.`,
+ href: '/guides/functions/examples/cloudflare-turnstile',
+ },
+ {
+ name: 'Connect to Postgres',
+ description: `Connecting to Postgres from Edge Functions.`,
+ href: '/guides/functions/examples/connect-to-postgres',
+ },
+ {
+ name: 'Github Actions',
+ description: `Deploying Edge Functions with GitHub Actions.`,
+ href: '/guides/functions/examples/github-actions',
+ },
+ {
+ name: 'Oak Server Middleware',
+ description: `Request Routing with Oak server middleware.`,
+ href: 'https://github.com/supabase/supabase/tree/master/examples/edge-functions/supabase/functions/oak-server',
+ },
+ {
+ name: 'OpenAI',
+ description: `Using OpenAI in Edge Functions.`,
+ href: '/guides/functions/examples/openai',
+ },
+ {
+ name: 'Stripe Webhooks',
+ description: `Handling signed Stripe Webhooks with Edge Functions.`,
+ href: '/guides/functions/examples/stripe-webhooks',
+ },
+ {
+ name: 'Send emails',
+ description: `Send emails in Edge Functions.`,
+ href: 'https://github.com/supabase/supabase/tree/master/examples/edge-functions/supabase/functions/send-email-smtp',
+ },
+ {
+ name: 'Web Stream',
+ description: `Server-Sent Events in Edge Functions.`,
+ href: 'https://github.com/supabase/supabase/tree/master/examples/edge-functions/supabase/functions/streams',
+ },
+ {
+ name: 'Puppeteer',
+ description: `Generate screenshots with Puppeteer.`,
+ href: 'https://github.com/supabase/supabase/tree/master/examples/edge-functions/supabase/functions/puppeteer',
+ },
+ {
+ name: 'Discord Bot',
+ description: `Building a Slash Command Discord Bot with Edge Functions.`,
+ href: '/guides/functions/examples/discord-bot',
+ },
+ {
+ name: 'Telegram Bot',
+ description: `Building a Telegram Bot with Edge Functions.`,
+ href: '/guides/functions/examples/telegram-bot',
+ },
+ {
+ name: 'Upload File',
+ description: `Process multipart/form-data.`,
+ href: 'https://github.com/supabase/supabase/tree/master/examples/edge-functions/supabase/functions/file-upload-storage',
+ },
+ {
+ name: 'Upstash Redis',
+ description: `Build an Edge Functions Counter with Upstash Redis.`,
+ href: '/guides/functions/examples/upstash-redis',
+ },
]
export const Page = ({ children }) =>
diff --git a/apps/docs/pages/guides/functions/auth.mdx b/apps/docs/pages/guides/functions/auth.mdx
index 871c16bd6f2..29ad1505034 100644
--- a/apps/docs/pages/guides/functions/auth.mdx
+++ b/apps/docs/pages/guides/functions/auth.mdx
@@ -20,7 +20,7 @@ By creating a supabase client with the auth context from the function, you can d
2. Run queries in the context of the user with [Row Level Security (RLS)](/docs/guides/auth/row-level-security) policies enforced.
```js lines=14,17-19,22-23 title=supabase/functions/select-from-table-with-auth-rls/index.ts
-import { serve } from 'https://deno.land/std@0.131.0/http/server.ts'
+import { serve } from 'https://deno.land/std@0.177.0/http/server.ts'
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
serve(async (req: Request) => {
diff --git a/apps/docs/pages/guides/functions/cors.mdx b/apps/docs/pages/guides/functions/cors.mdx
index 766da74ae86..7274d810b02 100644
--- a/apps/docs/pages/guides/functions/cors.mdx
+++ b/apps/docs/pages/guides/functions/cors.mdx
@@ -24,7 +24,7 @@ export const corsHeaders = {
You can then import and use the CORS headers within your functions:
```ts index.ts
-import { serve } from 'https://deno.land/std@0.131.0/http/server.ts'
+import { serve } from 'https://deno.land/std@0.177.0/http/server.ts'
import { corsHeaders } from '../_shared/cors.ts'
console.log(`Function "browser-with-cors" up and running!`)
diff --git a/apps/docs/pages/guides/functions/examples/cloudflare-turnstile.mdx b/apps/docs/pages/guides/functions/examples/cloudflare-turnstile.mdx
index ef38552855f..830e91723c8 100644
--- a/apps/docs/pages/guides/functions/examples/cloudflare-turnstile.mdx
+++ b/apps/docs/pages/guides/functions/examples/cloudflare-turnstile.mdx
@@ -34,7 +34,7 @@ supabase functions new cloudflare-turnstile
And add the code to the `index.ts` file:
```ts index.ts
-import { serve } from 'https://deno.land/std@0.131.0/http/server.ts'
+import { serve } from 'https://deno.land/std@0.177.0/http/server.ts'
import { corsHeaders } from '../_shared/cors.ts'
console.log('Hello from Cloudflare Trunstile!')
diff --git a/apps/docs/pages/guides/functions/examples/connect-to-postgres.mdx b/apps/docs/pages/guides/functions/examples/connect-to-postgres.mdx
index 3b66a4affd9..d99931b58e8 100644
--- a/apps/docs/pages/guides/functions/examples/connect-to-postgres.mdx
+++ b/apps/docs/pages/guides/functions/examples/connect-to-postgres.mdx
@@ -4,6 +4,7 @@ export const meta = {
id: 'examples-postgres-on-the-edge',
title: 'Connect to Postgres',
description: 'Connecting to Postgres from Edge Functions.',
+ video: 'https://www.youtube.com/v/cl7EuF1-RsY',
}
@@ -19,7 +20,7 @@ Supabase Edge Functions allow you to go beyond HTTP and can connect to your Post
```ts index.ts
import * as postgres from 'https://deno.land/x/postgres@v0.14.2/mod.ts'
-import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'
+import { serve } from 'https://deno.land/std@0.177.0/http/server.ts'
// Get the connection string from the environment variable "DATABASE_URL"
const databaseUrl = Deno.env.get('DATABASE_URL')!
diff --git a/apps/docs/pages/guides/functions/examples/discord-bot.mdx b/apps/docs/pages/guides/functions/examples/discord-bot.mdx
new file mode 100644
index 00000000000..281a2ac0e3e
--- /dev/null
+++ b/apps/docs/pages/guides/functions/examples/discord-bot.mdx
@@ -0,0 +1,178 @@
+import Layout from '~/layouts/DefaultGuideLayout'
+
+export const meta = {
+ id: 'examples-discord-bot',
+ title: 'Discord Bot',
+ description: 'Building a Slash Command Discord Bot with Edge Functions.',
+ video: 'https://www.youtube.com/v/J24Bvo_m7DM',
+}
+
+
+
+
+
+## Create an application on Discord Developer Portal
+
+1. Go to [https://discord.com/developers/applications](https://discord.com/developers/applications) (login using your discord account if required).
+2. Click on **New Application** button available at left side of your profile picture.
+3. Name your application and click on **Create**.
+4. Go to **Bot** section, click on **Add Bot**, and finally on **Yes, do it!** to confirm.
+
+That's it. A new application is created which will hold our Slash Command. Don't close the tab as we need information from this application page throughout our development.
+
+Before we can write some code, we need to curl a discord endpoint to register a Slash Command in our app.
+
+Fill `DISCORD_BOT_TOKEN` with the token available in the **Bot** section and `CLIENT_ID` with the ID available on the **General Information** section of the page and run the command on your terminal.
+
+```bash
+BOT_TOKEN='replace_me_with_bot_token'
+CLIENT_ID='replace_me_with_client_id'
+curl -X POST \
+-H 'Content-Type: application/json' \
+-H "Authorization: Bot $BOT_TOKEN" \
+-d '{"name":"hello","description":"Greet a person","options":[{"name":"name","description":"The name of the person","type":3,"required":true}]}' \
+"https://discord.com/api/v8/applications/$CLIENT_ID/commands"
+```
+
+This will register a Slash Command named `hello` that accepts a parameter named `name` of type string.
+
+## Code
+
+```ts index.ts
+// Sift is a small routing library that abstracts away details like starting a
+// listener on a port, and provides a simple function (serve) that has an API
+// to invoke a function for a specific path.
+import { json, serve, validateRequest } from 'sift'
+// TweetNaCl is a cryptography library that we use to verify requests
+// from Discord.
+import nacl from 'nacl'
+
+enum DiscordCommandType {
+ Ping = 1,
+ ApplicationCommand = 2,
+}
+
+// For all requests to "/" endpoint, we want to invoke home() handler.
+serve({
+ '/discord-bot': home,
+})
+
+// The main logic of the Discord Slash Command is defined in this function.
+async function home(request: Request) {
+ // validateRequest() ensures that a request is of POST method and
+ // has the following headers.
+ const { error } = await validateRequest(request, {
+ POST: {
+ headers: ['X-Signature-Ed25519', 'X-Signature-Timestamp'],
+ },
+ })
+ if (error) {
+ return json({ error: error.message }, { status: error.status })
+ }
+
+ // verifySignature() verifies if the request is coming from Discord.
+ // When the request's signature is not valid, we return a 401 and this is
+ // important as Discord sends invalid requests to test our verification.
+ const { valid, body } = await verifySignature(request)
+ if (!valid) {
+ return json(
+ { error: 'Invalid request' },
+ {
+ status: 401,
+ }
+ )
+ }
+
+ const { type = 0, data = { options: [] } } = JSON.parse(body)
+ // Discord performs Ping interactions to test our application.
+ // Type 1 in a request implies a Ping interaction.
+ if (type === DiscordCommandType.Ping) {
+ return json({
+ type: 1, // Type 1 in a response is a Pong interaction response type.
+ })
+ }
+
+ // Type 2 in a request is an ApplicationCommand interaction.
+ // It implies that a user has issued a command.
+ if (type === DiscordCommandType.ApplicationCommand) {
+ const { value } = data.options.find(
+ (option: { name: string; value: string }) => option.name === 'name'
+ )
+ return json({
+ // Type 4 responds with the below message retaining the user's
+ // input at the top.
+ type: 4,
+ data: {
+ content: `Hello, ${value}!`,
+ },
+ })
+ }
+
+ // We will return a bad request error as a valid Discord request
+ // shouldn't reach here.
+ return json({ error: 'bad request' }, { status: 400 })
+}
+
+/** Verify whether the request is coming from Discord. */
+async function verifySignature(request: Request): Promise<{ valid: boolean; body: string }> {
+ const PUBLIC_KEY = Deno.env.get('DISCORD_PUBLIC_KEY')!
+ // Discord sends these headers with every request.
+ const signature = request.headers.get('X-Signature-Ed25519')!
+ const timestamp = request.headers.get('X-Signature-Timestamp')!
+ const body = await request.text()
+ const valid = nacl.sign.detached.verify(
+ new TextEncoder().encode(timestamp + body),
+ hexToUint8Array(signature),
+ hexToUint8Array(PUBLIC_KEY)
+ )
+
+ return { valid, body }
+}
+
+/** Converts a hexadecimal string to Uint8Array. */
+function hexToUint8Array(hex: string) {
+ return new Uint8Array(hex.match(/.{1,2}/g)!.map((val) => parseInt(val, 16)))
+}
+```
+
+## Deploy the Slash Command Handler
+
+```bash
+supabase functions deploy discord-bot --no-verify-jwt
+supabase secrets set DISCORD_PUBLIC_KEY=your_public_key
+```
+
+Navigate to your Function details in the Supabase Dashboard to get your Endpoint URL.
+
+### Configure Discord application to use our URL as interactions endpoint URL
+
+1. Go back to your application (Greeter) page on Discord Developer Portal
+2. Fill **INTERACTIONS ENDPOINT URL** field with the URL and click on **Save Changes**.
+
+The application is now ready. Let's proceed to the next section to install it.
+
+## Install the Slash Command on your Discord server
+
+So to use the `hello` Slash Command, we need to install our Greeter application on our Discord server. Here are the steps:
+
+1. Go to **OAuth2** section of the Discord application page on Discord Developer Portal
+2. Select `applications.commands` scope and click on the **Copy** button below.
+3. Now paste and visit the URL on your browser. Select your server and click on **Authorize**.
+
+Open Discord, type `/Promise` and press **Enter**.
+
+## Run locally
+
+```bash
+supabase functions serve discord-bot --no-verify-jwt --env-file ./supabase/.env.local
+ngrok http 54321
+```
+
+export const Page = ({ children }) =>
+
+export default Page
diff --git a/apps/docs/pages/guides/functions/examples/og-image.mdx b/apps/docs/pages/guides/functions/examples/og-image.mdx
index efa8eb99e59..be4f4dc46d2 100644
--- a/apps/docs/pages/guides/functions/examples/og-image.mdx
+++ b/apps/docs/pages/guides/functions/examples/og-image.mdx
@@ -50,7 +50,7 @@ export default function handler(req: Request) {
Create an `index.ts` file to execute the handler on incoming requests:
```ts index.ts
-import { serve } from 'https://deno.land/std@0.131.0/http/server.ts'
+import { serve } from 'https://deno.land/std@0.177.0/http/server.ts'
import handler from './handler.tsx'
console.log('Hello from og-image Function!')
diff --git a/apps/docs/pages/guides/functions/examples/openai.mdx b/apps/docs/pages/guides/functions/examples/openai.mdx
new file mode 100644
index 00000000000..bc24d6a151a
--- /dev/null
+++ b/apps/docs/pages/guides/functions/examples/openai.mdx
@@ -0,0 +1,84 @@
+import Layout from '~/layouts/DefaultGuideLayout'
+
+export const meta = {
+ id: 'examples-openai',
+ title: 'OpenAI',
+ description: 'Using OpenAI in Edge Functions.',
+ video: 'https://www.youtube.com/v/29p8kIqyU_Y',
+}
+
+
+
+
+
+Use the [OpenAI completions API](https://platform.openai.com/docs/api-reference/completions) in Supabase Edge Functions.
+
+```ts index.ts
+import 'xhr_polyfill'
+import { serve } from 'std/server'
+import { CreateCompletionRequest } from 'openai'
+
+serve(async (req) => {
+ const { query } = await req.json()
+
+ const completionConfig: CreateCompletionRequest = {
+ model: 'text-davinci-003',
+ prompt: query,
+ max_tokens: 256,
+ temperature: 0,
+ stream: true,
+ }
+
+ return fetch('https://api.openai.com/v1/completions', {
+ method: 'POST',
+ headers: {
+ Authorization: `Bearer ${Deno.env.get('OPENAI_API_KEY')}`,
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(completionConfig),
+ })
+})
+```
+
+## Run locally
+
+```bash
+supabase functions serve --env-file ./supabase/.env.local --no-verify-jwt
+```
+
+Use cURL or Postman to make a POST request to http://localhost:54321/functions/v1/openai.
+
+```bash
+curl -i --location --request POST http://localhost:54321/functions/v1/openai \
+ --header 'Content-Type: application/json' \
+ --data '{"query":"What is Supabase?"}'
+```
+
+## Deploy
+
+```bash
+supabase functions deploy --no-verify-jwt openai
+supabase secrets set --env-file ./supabase/.env.local
+```
+
+## Go deeper
+
+If you're interesting in learning how to use this to build your own ChatGPT, read [the blog post](/blog/chatgpt-supabase-docs) and check out the video:
+
+
+
+
+
+export const Page = ({ children }) =>
+
+export default Page
diff --git a/apps/docs/pages/guides/getting-started/quickstarts/nuxtjs.mdx b/apps/docs/pages/guides/getting-started/quickstarts/nuxtjs.mdx
index 366120f35e9..f9259185926 100644
--- a/apps/docs/pages/guides/getting-started/quickstarts/nuxtjs.mdx
+++ b/apps/docs/pages/guides/getting-started/quickstarts/nuxtjs.mdx
@@ -126,7 +126,7 @@ The fastest way to get started with supabase and Nuxt.js is to use the supabase-
- Next, in your Next.js app, create a file called supabase-client.js and add the following code to initialize the Supabase client and set your project's credentials:
+ Next, in your Nuxt.js app, create a file called supabase-client.js and add the following code to initialize the Supabase client and set your project's credentials:
diff --git a/apps/docs/pages/guides/getting-started/tutorials/with-sveltekit.mdx b/apps/docs/pages/guides/getting-started/tutorials/with-sveltekit.mdx
index 3be4542731a..1daf9a881ec 100644
--- a/apps/docs/pages/guides/getting-started/tutorials/with-sveltekit.mdx
+++ b/apps/docs/pages/guides/getting-started/tutorials/with-sveltekit.mdx
@@ -50,9 +50,9 @@ These variables will be exposed on the browser, and that's completely fine since
```js title=src/lib/supabaseClient.ts
import { createClient } from '@supabase/auth-helpers-sveltekit'
-import { env } from '$env/dynamic/public'
+import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY } from '$env/static/public'
-export const supabase = createClient(env.PUBLIC_SUPABASE_URL, env.PUBLIC_SUPABASE_ANON_KEY)
+export const supabase = createClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY)
```
Optionally, update `src/routes/styles.css` with the [CSS from the example](https://raw.githubusercontent.com/supabase/supabase/master/examples/user-management/svelte-user-management/src/app.css).
diff --git a/apps/docs/pages/guides/getting-started/tutorials/with-vue-3.mdx b/apps/docs/pages/guides/getting-started/tutorials/with-vue-3.mdx
index 5480df2d4aa..0cbdd921ca2 100644
--- a/apps/docs/pages/guides/getting-started/tutorials/with-vue-3.mdx
+++ b/apps/docs/pages/guides/getting-started/tutorials/with-vue-3.mdx
@@ -99,7 +99,7 @@ const handleLogin = async () => {
Sign in via magic link with your email below
-
+
{
const [user, setUser] = useState
(null)
- const [oneSignalInitialized, setOneSignalInitialized] =
- useState(false)
+ const [oneSignalInitialized, setOneSignalInitialized] = useState(false)
/**
* Initializes OneSignal SDK for a given Supabase User ID
@@ -123,9 +121,7 @@ const Home: NextPage = () => {
const { price } = Object.fromEntries(new FormData(event.currentTarget))
if (typeof price !== 'string') return
- const { error } = await supabase
- .from('orders')
- .insert({ price: Number(price) })
+ const { error } = await supabase.from('orders').insert({ price: Number(price) })
if (error) {
alert(error.message)
}
@@ -142,15 +138,13 @@ const Home: NextPage = () => {
initialize()
- const authListener = supabase.auth.onAuthStateChange(
- async (event, session) => {
- const user = session?.user ?? null
- setUser(user)
- if (user) {
- initializeOneSignal(user.id)
- }
+ const authListener = supabase.auth.onAuthStateChange(async (event, session) => {
+ const user = session?.user ?? null
+ setUser(user)
+ if (user) {
+ initializeOneSignal(user.id)
}
- )
+ })
return () => {
authListener.data.subscription.unsubscribe()
@@ -175,10 +169,7 @@ const Home: NextPage = () => {
$200
$300
-
+
Place an Order
@@ -190,10 +181,7 @@ const Home: NextPage = () => {
name="email"
placeholder="Email"
/>
-
+
Send Magic Link
@@ -211,7 +199,7 @@ There is quite a bit of stuff going on here, but basically, it’s creating a si
Notice that inside the `initializeOneSignal()` function, we are setting the Supabase user ID as an [external user ID of OneSignal](https://documentation.onesignal.com/docs/external-user-ids). This allows us to later send push notifications to the user using their Supabase user ID from the backend, which is very handy.
```tsx
-await OneSignal.setExternalUserId(uid);
+await OneSignal.setExternalUserId(uid)
```
The front-end side of things is done here. Let’s get into the backend.
@@ -237,7 +225,7 @@ supabase functions new notify
Replace the contents of `supabase/functions/notify/index.ts` with the following
```tsx
-import { serve } from 'https://deno.land/std@0.131.0/http/server.ts'
+import { serve } from 'https://deno.land/std@0.177.0/http/server.ts'
import * as OneSignal from 'https://esm.sh/@onesignal/node-onesignal@1.0.0-beta7'
const _OnesignalAppId_ = Deno.env.get('ONESIGNAL_APP_ID')!
@@ -263,12 +251,9 @@ serve(async (req) => {
}
const onesignalApiRes = await onesignal.createNotification(notification)
- return new Response(
- JSON.stringify({ onesignalResponse: onesignalApiRes }),
- {
- headers: { 'Content-Type': 'application/json' },
- }
- )
+ return new Response(JSON.stringify({ onesignalResponse: onesignalApiRes }), {
+ headers: { 'Content-Type': 'application/json' },
+ })
} catch (err) {
console.error('Failed to create OneSignal notification', err)
return new Response('Server error.', {
@@ -295,7 +280,6 @@ ONESIGNAL_REST_API_KEY=YOUR_ONESIGNAL_REST_API_KEY

-
Once your environment variables are filled in, you can run the following command to set the environment variable.
```bash
@@ -313,35 +297,35 @@ supabase functions deploy notify --no-verify-jwt
Finally, we get to set up the database! Run the following SQL to set up the `orders` table.
```sql
-create table if not exists public.orders (
- id uuid not null primary key default uuid_generate_v4(),
- created_at timestamptz not null default now(),
- user_id uuid not null default auth.uid(),
+create table
+ if not exists public.orders (
+ id uuid not null primary key default uuid_generate_v4 (),
+ created_at timestamptz not null default now (),
+ user_id uuid not null default auth.uid (),
price int8 not null
-);
+ );
```
As you can see, the `orders` table has 4 columns and 3 of them have default values. That means all we need to send from the front-end app is the price. That is why our insert statement looked very simple.
```tsx
const { error } = await supabase.from('orders').insert({
- price: 100,
- })
+ price: 100,
+})
```
Let’s also set up the webhook so that whenever a new row is inserted in the `orders` table, it calls the edge function. Go to `Database > Webhooks` and create a new Database Webhook. The table should be set to `orders` and Events should be inserted. The type should be HTTP Request, the HTTP method should be POST, and the URL should be the URL of your edge function. Hit confirm to save the webhook configuration.

-At this point, the app should be complete! Run your app locally with `npm run dev`, or deploy your app to a hosting service and see how you receive a push notification when you place an order!
+At this point, the app should be complete! Run your app locally with `npm run dev`, or deploy your app to a hosting service and see how you receive a push notification when you place an order!
Remember that if you decide to deploy your app to a hosting service, you would need to create another OneSignal app configured for your local address.

-
## Resources
-This particular example was using Next.js, but you can apply the same principles to implement send push notification, SMS, Emails, and in-app-notifications on other platforms as well.
+This particular example was using Next.js, but you can apply the same principles to implement send push notification, SMS, Emails, and in-app-notifications on other platforms as well.
- [OneSignal + Flutter + Supabase example](https://github.com/OneSignalDevelopers/onesignal-supabase-sample-integration-supabase)
- [OneSignal Mobile Quickstart](https://documentation.onesignal.com/docs/mobile-sdk-setup)
diff --git a/apps/docs/pages/guides/platform/database-usage.mdx b/apps/docs/pages/guides/platform/database-usage.mdx
index 6ce6385770d..e99bf995aba 100644
--- a/apps/docs/pages/guides/platform/database-usage.mdx
+++ b/apps/docs/pages/guides/platform/database-usage.mdx
@@ -23,27 +23,56 @@ This value is also reported in the [database settings page](https://app.supabase
## Database storage management
Supabase uses network-attached storage to balance performance with scalability.
-For Pro and Enterprise projects, disk size expands ~1.5x automatically (e.g., 8GB -> 12GB) when you reach 90% of the disk size.
-Disk size expansion can only occur once every six hours.
-Pro projects can store up to 1024TB.
-All projects enter read-only mode when you reach 95% of the disk size. In read-only mode, clients will encounter errors such as `cannot execute INSERT in a read-only transaction`.
-Regular operation (read-write mode) is automatically re-enabled once usage is below 95% of the disk size.
+- For **Free tier projects**, the database can enter ['read only' mode when you exceed the 500mb limit](#free-tier-project-read-only-mode).
+- For **Pro and Enterprise tier projects**, your [disk size expands automatically](#paid-tier-disk-auto-scaling).
-If you need more than 1024TB of disk size or require multiple storage expansions in a short period of time, [contact us](https://app.supabase.com/support/new) to learn more about the Enterprise plan.
+### Free tier project 'read-only' mode
-### Increasing available disk size
+Free tier projects can enter read-only mode when exceeding the 500mb database size limit.
-1. [Upgrade to the Pro or Enterprise plan](https://app.supabase.com/project/_/settings/billing/subscription) to increase your quota and expand your disk size automatically.
+In read-only mode, clients will encounter errors such as `cannot execute INSERT in a read-only transaction`.
+Regular operation (read-write mode) is automatically re-enabled once database storage is below the 500mb database size limit.
-2. Delete data from your project's database to lower its disk usage. If your database is already in read-only mode, run the following command to change the transaction mode to read-write for your session:
+For more database storage and avoid your database entering read-only mode when exceeding the limit, you can [upgrade to the Pro or Enterprise plan](https://app.supabase.com/project/_/settings/billing/subscription).
- ```sql
- SET
- default_transaction_read_only = 'off';
- ```
+### Paid tier disk auto-scaling
- This allows you to delete data from within the session.
+Pro and Enterprise have disk auto-scaling. Database storage expands automatically when the database reaches 90% of the disk size. The disk is expanded to be 50% larger (e.g., 8GB -> 12GB).
+Auto-scaling can only take place once every 6 hours.
+
+
+
+If you intend to import a lot of data into your database that would require multiple storage expansions then [reach out to our team](https://app.supabase.com/support/new), otherwise your project might enter a ['read-only' mode](#paid-tier-project-read-only-mode).
+
+
+
+You may require multiple storage expansions in a short period of time. For example, you need to upload more than 1.5x the current size of your database storage. This will lead to your database entering a [read-only mode](#paid-tier-project-read-only-mode). You can avoid this by [contacting the support team](https://app.supabase.com/support/new) so that your database can be resized to a sufficient amount before you start uploading to it.
+
+If you need more than 1024TB of disk size, you can [contact us](https://app.supabase.com/support/new) to learn more about the Enterprise plan.
+
+### Paid tier project 'read-only' mode
+
+Database storage expands, at most, once every 6 hours.
+If within those 6 hours you reach 95% of the disk space, your project will enter read-only mode.
+
+In read-only mode, clients will encounter errors such as `cannot execute INSERT in a read-only transaction`.
+Regular operation (read-write mode) is automatically re-enabled once usage is below 95% of the disk size, or when the database automatically resizes after the 6 hour period.
+
+### Increasing available database space
+
+If your project is on the free tier, you can [Upgrade to the Pro or Enterprise tier](https://app.supabase.com/project/_/settings/billing/subscription) so that you can start to use more than 500mb.
+
+For Pro and Enterprise tier projects that are [nearing their disk size limit](#paid-tier-project-read-only-mode), you can increase available disk size by deleting data from your project's database to lower its disk usage.
+
+If your project database is already in read-only mode, run the following command to change the transaction mode to read-write for your session:
+
+```sql
+SET
+ default_transaction_read_only = 'off';
+```
+
+This allows you to delete data from within the session.
### Preoccupied Space
diff --git a/apps/docs/pages/guides/platform/migrating-and-upgrading-projects.mdx b/apps/docs/pages/guides/platform/migrating-and-upgrading-projects.mdx
index 1b657450546..9b035e1264b 100644
--- a/apps/docs/pages/guides/platform/migrating-and-upgrading-projects.mdx
+++ b/apps/docs/pages/guides/platform/migrating-and-upgrading-projects.mdx
@@ -66,7 +66,7 @@ Replication for Realtime is disabled for all tables in your new project. On the
### Migrate Storage objects
-The new project has the old project's Storage buckets, but the Storage objects need to be migrated manually. Use this script to move storage objects from one project to another. If you have more than 10k objects, we can move the objects for you. Just contact us at [support@supabase.com](mailto:support@supabase.com).
+The new project has the old project's Storage buckets, but the Storage objects need to be migrated manually. Use this script to move storage objects from one project to another.
```js
// npm install @supabase/supabase-js@1
diff --git a/apps/docs/public/img/integrations/logos/openai_logo.png b/apps/docs/public/img/integrations/logos/openai_logo.png
new file mode 100644
index 00000000000..c5596878b0b
Binary files /dev/null and b/apps/docs/public/img/integrations/logos/openai_logo.png differ
diff --git a/apps/www/pages/_app.tsx b/apps/www/pages/_app.tsx
index b4fb7a031ce..9698060202f 100644
--- a/apps/www/pages/_app.tsx
+++ b/apps/www/pages/_app.tsx
@@ -38,7 +38,7 @@ export default function MyApp({ Component, pageProps }: AppProps) {
return (
<>
-
+
{
{site_title}
+
diff --git a/apps/www/pages/pricing/index.tsx b/apps/www/pages/pricing/index.tsx
index 9b209dc901e..bb359df65a1 100644
--- a/apps/www/pages/pricing/index.tsx
+++ b/apps/www/pages/pricing/index.tsx
@@ -83,34 +83,6 @@ export default function IndexPage() {
additional: '',
cta: 'Get Started',
},
- // {
- // name: 'Team',
- // nameBadge: 'New',
- // costUnit: 'per month per org',
- // href: 'https://app.supabase.com/new/new-project',
- // from: true,
- // priceLabel: 'Starting from',
- // priceMonthly: 599,
- // description: 'For scaling teams with permissions & access controls',
- // warning: '+ any additional usage',
- // features: [
- // 'Usage-based pricing',
- // 'Organization member roles (ABAC)',
- // 'SOC2',
- // 'Priority email support & SLAs',
- // '14 day backups',
- // '100,000 monthly active users included',
- // 'Standardized Security Questionnaire',
- // 'SSO for Supabase Dashboard',
- // '1 XS compute instance',
- // '28 day log retention',
- // ],
- // scale: '',
- // shutdown: '',
- // preface: 'The following features will apply to all projects within the organization:',
- // additional: '',
- // cta: 'Get Started',
- // },
{
name: 'Enterprise',
href: 'https://forms.supabase.com/enterprise',
@@ -127,8 +99,8 @@ export default function IndexPage() {
`Private Slack channel`,
`Uptime SLA`,
],
- priceLabel: 'Custom quotas',
- priceMonthly: 'Exclusive pricing',
+ priceLabel: '',
+ priceMonthly: 'Contact us',
preface: 'These apply to all projects within the organization:',
scale: '',
shutdown: '',
@@ -164,7 +136,7 @@ export default function IndexPage() {
name: 'Point in Time Recovery',
heroImg: 'addons-pitr-hero',
icon: 'pitr-upgrade',
- price: 'Starts from $5',
+ price: 'Starts from $100',
description: 'Roll back to any specific point in time and ensure that data is not lost.',
leftCtaText: 'Documentation',
leftCtaLink: 'https://supabase.com/docs/guides/platform/backups',
@@ -319,7 +291,7 @@ export default function IndexPage() {
{tier.name !== 'Enterprise' && '$'}
@@ -750,13 +722,15 @@ export default function IndexPage() {
<>
{tier.name !== 'Enterprise' && '$'}
{tier.priceMonthly}
-
per project per month
+ {tier.name !== 'Enterprise' && (
+ per project per month
+ )}
>
{tier.warning && (
@@ -861,66 +835,12 @@ export default function IndexPage() {
Frequently asked questions
-
- Can't find the answer to your question, ask someone in the community either on
- our Discord or GitHub.
-
-
-
- {/* @ts-ignore */}
+
@@ -940,6 +860,28 @@ export default function IndexPage() {
})}
+
+ Can't find the answer to your question, you can{' '}
+
+ open a support ticket
+ {' '}
+ and our team of experts will be able to help.
+
+
+ For enterprise enquries,{' '}
+
+ you can contact the team here
+
+ .
+
diff --git a/examples/user-management/sveltekit-user-management/src/app.html b/examples/user-management/sveltekit-user-management/src/app.html
index b555f2469a3..8ce87909fb7 100644
--- a/examples/user-management/sveltekit-user-management/src/app.html
+++ b/examples/user-management/sveltekit-user-management/src/app.html
@@ -3,7 +3,7 @@
-
+
%sveltekit.head%
diff --git a/package-lock.json b/package-lock.json
index ad7da916061..dccce67d772 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -3171,6 +3171,37 @@
"react": "^16.8.3 || ^17 || ^18"
}
},
+ "node_modules/@codemirror/language": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.0.0.tgz",
+ "integrity": "sha512-rtjk5ifyMzOna1c7PBu7J1VCt0PvA5wy3o8eMVnxMKb7z8KA7JFecvD04dSn14vj/bBaAbqRsGed5OjtofEnLA==",
+ "peer": true,
+ "dependencies": {
+ "@codemirror/state": "^6.0.0",
+ "@codemirror/view": "^6.0.0",
+ "@lezer/common": "^1.0.0",
+ "@lezer/highlight": "^1.0.0",
+ "@lezer/lr": "^1.0.0",
+ "style-mod": "^4.0.0"
+ }
+ },
+ "node_modules/@codemirror/state": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.2.0.tgz",
+ "integrity": "sha512-69QXtcrsc3RYtOtd+GsvczJ319udtBf1PTrr2KbLWM/e2CXUPnh0Nz9AUo8WfhSQ7GeL8dPVNUmhQVgpmuaNGA==",
+ "peer": true
+ },
+ "node_modules/@codemirror/view": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.9.0.tgz",
+ "integrity": "sha512-uFaqE6fBOp0Dj/tmWoe/TFlSSIPdpAzhvATgbq1eAKRkgq3hY79FioZ7nfdiT+24kz68AIWuSZ9hi3psKe36WQ==",
+ "peer": true,
+ "dependencies": {
+ "@codemirror/state": "^6.1.4",
+ "style-mod": "^4.0.0",
+ "w3c-keyname": "^2.2.4"
+ }
+ },
"node_modules/@colors/colors": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz",
@@ -3501,6 +3532,66 @@
"integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==",
"dev": true
},
+ "node_modules/@graphiql/react": {
+ "version": "0.15.0",
+ "resolved": "https://registry.npmjs.org/@graphiql/react/-/react-0.15.0.tgz",
+ "integrity": "sha512-kJqkdf6d4Cck05Wt5yCDZXWfs7HZgcpuoWq/v8nOa698qVaNMM3qdG4CpRsZEexku0DSSJzWWuanxd5x+sRcFg==",
+ "dependencies": {
+ "@graphiql/toolkit": "^0.8.0",
+ "@reach/combobox": "^0.17.0",
+ "@reach/dialog": "^0.17.0",
+ "@reach/listbox": "^0.17.0",
+ "@reach/menu-button": "^0.17.0",
+ "@reach/tooltip": "^0.17.0",
+ "@reach/visually-hidden": "^0.17.0",
+ "codemirror": "^5.65.3",
+ "codemirror-graphql": "^2.0.2",
+ "copy-to-clipboard": "^3.2.0",
+ "graphql-language-service": "^5.1.0",
+ "markdown-it": "^12.2.0",
+ "set-value": "^4.1.0"
+ },
+ "peerDependencies": {
+ "graphql": "^15.5.0 || ^16.0.0",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0",
+ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
+ "node_modules/@graphiql/react/node_modules/set-value": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/set-value/-/set-value-4.1.0.tgz",
+ "integrity": "sha512-zTEg4HL0RwVrqcWs3ztF+x1vkxfm0lP+MQQFPiMJTKVceBwEV0A569Ou8l9IYQG8jOZdMVI1hGsc0tmeD2o/Lw==",
+ "funding": [
+ "https://github.com/sponsors/jonschlinkert",
+ "https://paypal.me/jonathanschlinkert",
+ "https://jonschlinkert.dev/sponsor"
+ ],
+ "dependencies": {
+ "is-plain-object": "^2.0.4",
+ "is-primitive": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=11.0"
+ }
+ },
+ "node_modules/@graphiql/toolkit": {
+ "version": "0.8.0",
+ "resolved": "https://registry.npmjs.org/@graphiql/toolkit/-/toolkit-0.8.0.tgz",
+ "integrity": "sha512-DbMFhEKejpPzB6k8W3Mj+Rl8geXiw49USDF9Wdi06EEk1XLVh1iebDqveYY+4lViITsV4+BeGikxlqi8umfP4g==",
+ "dependencies": {
+ "@n1ru4l/push-pull-async-iterable-iterator": "^3.1.0",
+ "meros": "^1.1.4"
+ },
+ "peerDependencies": {
+ "graphql": "^15.5.0 || ^16.0.0",
+ "graphql-ws": ">= 4.5.0"
+ },
+ "peerDependenciesMeta": {
+ "graphql-ws": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@hcaptcha/react-hcaptcha": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/@hcaptcha/react-hcaptcha/-/react-hcaptcha-1.4.4.tgz",
@@ -4500,6 +4591,30 @@
"@jridgewell/sourcemap-codec": "1.4.14"
}
},
+ "node_modules/@lezer/common": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.0.2.tgz",
+ "integrity": "sha512-SVgiGtMnMnW3ActR8SXgsDhw7a0w0ChHSYAyAUxxrOiJ1OqYWEKk/xJd84tTSPo1mo6DXLObAJALNnd0Hrv7Ng==",
+ "peer": true
+ },
+ "node_modules/@lezer/highlight": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.1.3.tgz",
+ "integrity": "sha512-3vLKLPThO4td43lYRBygmMY18JN3CPh9w+XS2j8WC30vR4yZeFG4z1iFe4jXE43NtGqe//zHW5q8ENLlHvz9gw==",
+ "peer": true,
+ "dependencies": {
+ "@lezer/common": "^1.0.0"
+ }
+ },
+ "node_modules/@lezer/lr": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.3.3.tgz",
+ "integrity": "sha512-JPQe3mwJlzEVqy67iQiiGozhcngbO8QBgpqZM6oL1Wj/dXckrEexpBLeFkq0edtW5IqnPRFxA24BHJni8Js69w==",
+ "peer": true,
+ "dependencies": {
+ "@lezer/common": "^1.0.0"
+ }
+ },
"node_modules/@mapbox/node-pre-gyp": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.10.tgz",
@@ -4980,6 +5095,14 @@
"integrity": "sha512-Iozmtbqv0noj0uDDqoL0zNq0VBEfK2YFoMAZoxJe4cwphvLR+JskfF30QhXHOR4m3KrE6NLRYw+U9MRXvifyig==",
"dev": true
},
+ "node_modules/@n1ru4l/push-pull-async-iterable-iterator": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/@n1ru4l/push-pull-async-iterable-iterator/-/push-pull-async-iterable-iterator-3.2.0.tgz",
+ "integrity": "sha512-3fkKj25kEjsfObL6IlKPAlHYPq/oYwUkkQ03zsTTiDjD7vg/RxjdiLeCydqtxHZP0JgsXL3D/X5oAkMGzuUp/Q==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/@next/bundle-analyzer": {
"version": "13.0.5",
"resolved": "https://registry.npmjs.org/@next/bundle-analyzer/-/bundle-analyzer-13.0.5.tgz",
@@ -7196,6 +7319,229 @@
"@babel/runtime": "^7.13.10"
}
},
+ "node_modules/@reach/auto-id": {
+ "version": "0.17.0",
+ "resolved": "https://registry.npmjs.org/@reach/auto-id/-/auto-id-0.17.0.tgz",
+ "integrity": "sha512-ud8iPwF52RVzEmkHq1twuqGuPA+moreumUHdtgvU3sr3/15BNhwp3KyDLrKKSz0LP1r3V4pSdyF9MbYM8BoSjA==",
+ "dependencies": {
+ "@reach/utils": "0.17.0",
+ "tslib": "^2.3.0"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || 17.x",
+ "react-dom": "^16.8.0 || 17.x"
+ }
+ },
+ "node_modules/@reach/combobox": {
+ "version": "0.17.0",
+ "resolved": "https://registry.npmjs.org/@reach/combobox/-/combobox-0.17.0.tgz",
+ "integrity": "sha512-2mYvU5agOBCQBMdlM4cri+P1BbNwp05P1OuDyc33xJSNiBG7BMy4+ZSHJ0X4fyle6rHwSgCAOCLOeWV1XUYjoQ==",
+ "dependencies": {
+ "@reach/auto-id": "0.17.0",
+ "@reach/descendants": "0.17.0",
+ "@reach/popover": "0.17.0",
+ "@reach/portal": "0.17.0",
+ "@reach/utils": "0.17.0",
+ "prop-types": "^15.7.2",
+ "tiny-warning": "^1.0.3",
+ "tslib": "^2.3.0"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || 17.x",
+ "react-dom": "^16.8.0 || 17.x"
+ }
+ },
+ "node_modules/@reach/descendants": {
+ "version": "0.17.0",
+ "resolved": "https://registry.npmjs.org/@reach/descendants/-/descendants-0.17.0.tgz",
+ "integrity": "sha512-c7lUaBfjgcmKFZiAWqhG+VnXDMEhPkI4kAav/82XKZD6NVvFjsQOTH+v3tUkskrAPV44Yuch0mFW/u5Ntifr7Q==",
+ "dependencies": {
+ "@reach/utils": "0.17.0",
+ "tslib": "^2.3.0"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || 17.x",
+ "react-dom": "^16.8.0 || 17.x"
+ }
+ },
+ "node_modules/@reach/dialog": {
+ "version": "0.17.0",
+ "resolved": "https://registry.npmjs.org/@reach/dialog/-/dialog-0.17.0.tgz",
+ "integrity": "sha512-AnfKXugqDTGbeG3c8xDcrQDE4h9b/vnc27Sa118oQSquz52fneUeX9MeFb5ZEiBJK8T5NJpv7QUTBIKnFCAH5A==",
+ "dependencies": {
+ "@reach/portal": "0.17.0",
+ "@reach/utils": "0.17.0",
+ "prop-types": "^15.7.2",
+ "react-focus-lock": "^2.5.2",
+ "react-remove-scroll": "^2.4.3",
+ "tslib": "^2.3.0"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || 17.x",
+ "react-dom": "^16.8.0 || 17.x"
+ }
+ },
+ "node_modules/@reach/dropdown": {
+ "version": "0.17.0",
+ "resolved": "https://registry.npmjs.org/@reach/dropdown/-/dropdown-0.17.0.tgz",
+ "integrity": "sha512-qBTIGInhxtPHtdj4Pl2XZgZMz3e37liydh0xR3qc48syu7g71sL4nqyKjOzThykyfhA3Pb3/wFgsFJKGTSdaig==",
+ "dependencies": {
+ "@reach/auto-id": "0.17.0",
+ "@reach/descendants": "0.17.0",
+ "@reach/popover": "0.17.0",
+ "@reach/utils": "0.17.0",
+ "tslib": "^2.3.0"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || 17.x",
+ "react-dom": "^16.8.0 || 17.x"
+ }
+ },
+ "node_modules/@reach/listbox": {
+ "version": "0.17.0",
+ "resolved": "https://registry.npmjs.org/@reach/listbox/-/listbox-0.17.0.tgz",
+ "integrity": "sha512-AMnH1P6/3VKy2V/nPb4Es441arYR+t4YRdh9jdcFVrCOD6y7CQrlmxsYjeg9Ocdz08XpdoEBHM3PKLJqNAUr7A==",
+ "dependencies": {
+ "@reach/auto-id": "0.17.0",
+ "@reach/descendants": "0.17.0",
+ "@reach/machine": "0.17.0",
+ "@reach/popover": "0.17.0",
+ "@reach/utils": "0.17.0",
+ "prop-types": "^15.7.2"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || 17.x",
+ "react-dom": "^16.8.0 || 17.x"
+ }
+ },
+ "node_modules/@reach/machine": {
+ "version": "0.17.0",
+ "resolved": "https://registry.npmjs.org/@reach/machine/-/machine-0.17.0.tgz",
+ "integrity": "sha512-9EHnuPgXzkbRENvRUzJvVvYt+C2jp7PGN0xon7ffmKoK8rTO6eA/bb7P0xgloyDDQtu88TBUXKzW0uASqhTXGA==",
+ "dependencies": {
+ "@reach/utils": "0.17.0",
+ "@xstate/fsm": "1.4.0",
+ "tslib": "^2.3.0"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || 17.x",
+ "react-dom": "^16.8.0 || 17.x"
+ }
+ },
+ "node_modules/@reach/menu-button": {
+ "version": "0.17.0",
+ "resolved": "https://registry.npmjs.org/@reach/menu-button/-/menu-button-0.17.0.tgz",
+ "integrity": "sha512-YyuYVyMZKamPtivoEI6D0UEILYH3qZtg4kJzEAuzPmoR/aHN66NZO75Fx0gtjG1S6fZfbiARaCOZJC0VEiDOtQ==",
+ "dependencies": {
+ "@reach/dropdown": "0.17.0",
+ "@reach/popover": "0.17.0",
+ "@reach/utils": "0.17.0",
+ "prop-types": "^15.7.2",
+ "tiny-warning": "^1.0.3",
+ "tslib": "^2.3.0"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || 17.x",
+ "react-dom": "^16.8.0 || 17.x",
+ "react-is": "^16.8.0 || 17.x"
+ }
+ },
+ "node_modules/@reach/observe-rect": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@reach/observe-rect/-/observe-rect-1.2.0.tgz",
+ "integrity": "sha512-Ba7HmkFgfQxZqqaeIWWkNK0rEhpxVQHIoVyW1YDSkGsGIXzcaW4deC8B0pZrNSSyLTdIk7y+5olKt5+g0GmFIQ=="
+ },
+ "node_modules/@reach/popover": {
+ "version": "0.17.0",
+ "resolved": "https://registry.npmjs.org/@reach/popover/-/popover-0.17.0.tgz",
+ "integrity": "sha512-yYbBF4fMz4Ml4LB3agobZjcZ/oPtPsNv70ZAd7lEC2h7cvhF453pA+zOBGYTPGupKaeBvgAnrMjj7RnxDU5hoQ==",
+ "dependencies": {
+ "@reach/portal": "0.17.0",
+ "@reach/rect": "0.17.0",
+ "@reach/utils": "0.17.0",
+ "tabbable": "^4.0.0",
+ "tslib": "^2.3.0"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || 17.x",
+ "react-dom": "^16.8.0 || 17.x"
+ }
+ },
+ "node_modules/@reach/portal": {
+ "version": "0.17.0",
+ "resolved": "https://registry.npmjs.org/@reach/portal/-/portal-0.17.0.tgz",
+ "integrity": "sha512-+IxsgVycOj+WOeNPL2NdgooUdHPSY285wCtj/iWID6akyr4FgGUK7sMhRM9aGFyrGpx2vzr+eggbUmAVZwOz+A==",
+ "dependencies": {
+ "@reach/utils": "0.17.0",
+ "tiny-warning": "^1.0.3",
+ "tslib": "^2.3.0"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || 17.x",
+ "react-dom": "^16.8.0 || 17.x"
+ }
+ },
+ "node_modules/@reach/rect": {
+ "version": "0.17.0",
+ "resolved": "https://registry.npmjs.org/@reach/rect/-/rect-0.17.0.tgz",
+ "integrity": "sha512-3YB7KA5cLjbLc20bmPkJ06DIfXSK06Cb5BbD2dHgKXjUkT9WjZaLYIbYCO8dVjwcyO3GCNfOmPxy62VsPmZwYA==",
+ "dependencies": {
+ "@reach/observe-rect": "1.2.0",
+ "@reach/utils": "0.17.0",
+ "prop-types": "^15.7.2",
+ "tiny-warning": "^1.0.3",
+ "tslib": "^2.3.0"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || 17.x",
+ "react-dom": "^16.8.0 || 17.x"
+ }
+ },
+ "node_modules/@reach/tooltip": {
+ "version": "0.17.0",
+ "resolved": "https://registry.npmjs.org/@reach/tooltip/-/tooltip-0.17.0.tgz",
+ "integrity": "sha512-HP8Blordzqb/Cxg+jnhGmWQfKgypamcYLBPlcx6jconyV5iLJ5m93qipr1giK7MqKT2wlsKWy44ZcOrJ+Wrf8w==",
+ "dependencies": {
+ "@reach/auto-id": "0.17.0",
+ "@reach/portal": "0.17.0",
+ "@reach/rect": "0.17.0",
+ "@reach/utils": "0.17.0",
+ "@reach/visually-hidden": "0.17.0",
+ "prop-types": "^15.7.2",
+ "tiny-warning": "^1.0.3",
+ "tslib": "^2.3.0"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || 17.x",
+ "react-dom": "^16.8.0 || 17.x"
+ }
+ },
+ "node_modules/@reach/utils": {
+ "version": "0.17.0",
+ "resolved": "https://registry.npmjs.org/@reach/utils/-/utils-0.17.0.tgz",
+ "integrity": "sha512-M5y8fCBbrWeIsxedgcSw6oDlAMQDkl5uv3VnMVJ7guwpf4E48Xlh1v66z/1BgN/WYe2y8mB/ilFD2nysEfdGeA==",
+ "dependencies": {
+ "tiny-warning": "^1.0.3",
+ "tslib": "^2.3.0"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || 17.x",
+ "react-dom": "^16.8.0 || 17.x"
+ }
+ },
+ "node_modules/@reach/visually-hidden": {
+ "version": "0.17.0",
+ "resolved": "https://registry.npmjs.org/@reach/visually-hidden/-/visually-hidden-0.17.0.tgz",
+ "integrity": "sha512-T6xF3Nv8vVnjVkGU6cm0+kWtvliLqPAo8PcZ+WxkKacZsaHTjaZb4v1PaCcyQHmuTNT/vtTVNOJLG0SjQOIb7g==",
+ "dependencies": {
+ "prop-types": "^15.7.2",
+ "tslib": "^2.3.0"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || 17.x",
+ "react-dom": "^16.8.0 || 17.x"
+ }
+ },
"node_modules/@react-dnd/asap": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@react-dnd/asap/-/asap-4.0.1.tgz",
@@ -11362,6 +11708,11 @@
"@xtuc/long": "4.2.2"
}
},
+ "node_modules/@xstate/fsm": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/@xstate/fsm/-/fsm-1.4.0.tgz",
+ "integrity": "sha512-uTHDeu2xI5E1IFwf37JFQM31RrH7mY7877RqPBS4ZqSNUwoLDuct8AhBWaXGnVizBAYyimVwgCyGa9z/NiRhXA=="
+ },
"node_modules/@xtuc/ieee754": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz",
@@ -13905,6 +14256,24 @@
"node": ">=0.10.0"
}
},
+ "node_modules/codemirror": {
+ "version": "5.65.11",
+ "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.65.11.tgz",
+ "integrity": "sha512-Gp62g2eKSCHYt10axmGhKq3WoJSvVpvhXmowNq7pZdRVowwtvBR/hi2LSP5srtctKkRT33T6/n8Kv1UGp7JW4A=="
+ },
+ "node_modules/codemirror-graphql": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/codemirror-graphql/-/codemirror-graphql-2.0.3.tgz",
+ "integrity": "sha512-T58f+XQVKE6/6goAADJfu56wPRcdF2RUZMHITUol6h6Uo1KKUmknV4muKNFOl6GM15BjBraysF6HATp4oY6kyA==",
+ "dependencies": {
+ "graphql-language-service": "5.1.1"
+ },
+ "peerDependencies": {
+ "@codemirror/language": "6.0.0",
+ "codemirror": "^5.65.3",
+ "graphql": "^15.5.0 || ^16.0.0"
+ }
+ },
"node_modules/coffee-script": {
"version": "1.12.7",
"resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.12.7.tgz",
@@ -16586,7 +16955,6 @@
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz",
"integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==",
- "dev": true,
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
}
@@ -19489,6 +19857,23 @@
"integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==",
"dev": true
},
+ "node_modules/graphiql": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/graphiql/-/graphiql-2.2.0.tgz",
+ "integrity": "sha512-w1ujpCKMlkwkoUjeg0HpRiBBTm1WHAjHNkFv1TbMu6trjzz63mQ48GLZlmyQY1yhwmc+diCcvmmAt+AyvKLWWA==",
+ "dependencies": {
+ "@graphiql/react": "^0.15.0",
+ "@graphiql/toolkit": "^0.8.0",
+ "entities": "^2.0.0",
+ "graphql-language-service": "^5.1.0",
+ "markdown-it": "^12.2.0"
+ },
+ "peerDependencies": {
+ "graphql": "^15.5.0 || ^16.0.0",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0",
+ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
"node_modules/graphlib": {
"version": "2.1.8",
"resolved": "https://registry.npmjs.org/graphlib/-/graphlib-2.1.8.tgz",
@@ -19497,6 +19882,40 @@
"lodash": "^4.17.15"
}
},
+ "node_modules/graphql": {
+ "version": "16.6.0",
+ "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.6.0.tgz",
+ "integrity": "sha512-KPIBPDlW7NxrbT/eh4qPXz5FiFdL5UbaA0XUNz2Rp3Z3hqBSkbj0GVjwFDztsWVauZUWsbKHgMg++sk8UX0bkw==",
+ "engines": {
+ "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0"
+ }
+ },
+ "node_modules/graphql-language-service": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/graphql-language-service/-/graphql-language-service-5.1.1.tgz",
+ "integrity": "sha512-gpaDT9E3+3eWhoqO4C81CGhkzr7Vp2jH/eq+ykoUbgfvMEpqhGTfCeNmrf+S4K/+4WTkAAJBsYT0/ZPZkqe/Hg==",
+ "dependencies": {
+ "nullthrows": "^1.0.0",
+ "vscode-languageserver-types": "^3.17.1"
+ },
+ "bin": {
+ "graphql": "dist/temp-bin.js"
+ },
+ "peerDependencies": {
+ "graphql": "^15.5.0 || ^16.0.0"
+ }
+ },
+ "node_modules/graphql-ws": {
+ "version": "5.11.3",
+ "resolved": "https://registry.npmjs.org/graphql-ws/-/graphql-ws-5.11.3.tgz",
+ "integrity": "sha512-fU8zwSgAX2noXAsuFiCZ8BtXeXZOzXyK5u1LloCdacsVth4skdBMPO74EG51lBoWSIZ8beUocdpV8+cQHBODnQ==",
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "graphql": ">=0.11 <=16"
+ }
+ },
"node_modules/gray-matter": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz",
@@ -21183,6 +21602,14 @@
"integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
"dev": true
},
+ "node_modules/is-primitive": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-3.0.1.tgz",
+ "integrity": "sha512-GljRxhWvlCNRfZyORiH77FwdFwGcMO620o37EOYC0ORWdq+WYNVqW0w2Juzew4M+L81l6/QS3t5gkkihyRqv9w==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/is-reference": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.0.tgz",
@@ -23771,6 +24198,14 @@
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
"devOptional": true
},
+ "node_modules/linkify-it": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.3.tgz",
+ "integrity": "sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==",
+ "dependencies": {
+ "uc.micro": "^1.0.1"
+ }
+ },
"node_modules/list-item": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/list-item/-/list-item-1.1.1.tgz",
@@ -24166,6 +24601,29 @@
"node": ">=0.10.0"
}
},
+ "node_modules/markdown-it": {
+ "version": "12.3.2",
+ "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.2.tgz",
+ "integrity": "sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==",
+ "dependencies": {
+ "argparse": "^2.0.1",
+ "entities": "~2.1.0",
+ "linkify-it": "^3.0.1",
+ "mdurl": "^1.0.1",
+ "uc.micro": "^1.0.5"
+ },
+ "bin": {
+ "markdown-it": "bin/markdown-it.js"
+ }
+ },
+ "node_modules/markdown-it/node_modules/entities": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz",
+ "integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==",
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
"node_modules/markdown-link": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/markdown-link/-/markdown-link-0.1.1.tgz",
@@ -25125,6 +25583,22 @@
"uuid": "dist/bin/uuid"
}
},
+ "node_modules/meros": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/meros/-/meros-1.2.1.tgz",
+ "integrity": "sha512-R2f/jxYqCAGI19KhAvaxSOxALBMkaXWH2a7rOyqQw+ZmizX5bKkEYWLzdhC+U82ZVVPVp6MCXe3EkVligh+12g==",
+ "engines": {
+ "node": ">=13"
+ },
+ "peerDependencies": {
+ "@types/node": ">=13"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ }
+ }
+ },
"node_modules/methods": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
@@ -27361,6 +27835,11 @@
"url": "https://github.com/fb55/nth-check?sponsor=1"
}
},
+ "node_modules/nullthrows": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz",
+ "integrity": "sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw=="
+ },
"node_modules/num2fraction": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/num2fraction/-/num2fraction-1.2.2.tgz",
@@ -30848,6 +31327,17 @@
"react-dom": "^16.8.5 || ^17.0.0 || ^18.0.0"
}
},
+ "node_modules/react-clientside-effect": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/react-clientside-effect/-/react-clientside-effect-1.2.6.tgz",
+ "integrity": "sha512-XGGGRQAKY+q25Lz9a/4EPqom7WRjz3z9R2k4jhVKA/puQFH/5Nt27vFZYql4m4NVNdUvX8PS3O7r/Zzm7cjUlg==",
+ "dependencies": {
+ "@babel/runtime": "^7.12.13"
+ },
+ "peerDependencies": {
+ "react": "^15.3.0 || ^16.0.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
"node_modules/react-contexify": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/react-contexify/-/react-contexify-5.0.0.tgz",
@@ -31089,6 +31579,39 @@
"react": ">=16.8.6"
}
},
+ "node_modules/react-focus-lock": {
+ "version": "2.9.3",
+ "resolved": "https://registry.npmjs.org/react-focus-lock/-/react-focus-lock-2.9.3.tgz",
+ "integrity": "sha512-cGNkz9p5Fpqio6hBHlkKxzRYrBYtcPosFOL6Q3N/LSbHjwP/PTBqHpvbgaOYoE7rWfzw8qXPKTB3Tk/VPgw4NQ==",
+ "dependencies": {
+ "@babel/runtime": "^7.0.0",
+ "focus-lock": "^0.11.5",
+ "prop-types": "^15.6.2",
+ "react-clientside-effect": "^1.2.6",
+ "use-callback-ref": "^1.3.0",
+ "use-sidecar": "^1.1.2"
+ },
+ "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/react-focus-lock/node_modules/focus-lock": {
+ "version": "0.11.5",
+ "resolved": "https://registry.npmjs.org/focus-lock/-/focus-lock-0.11.5.tgz",
+ "integrity": "sha512-1mTr6pl9HBpJ8CqY7hRc38MCrcuTZIeYAkBD1gBTzbx5/to+bRBaBYtJ68iDq7ryTzAAbKrG3dVKjkrWTaaEaw==",
+ "dependencies": {
+ "tslib": "^2.0.3"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/react-from-dom": {
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/react-from-dom/-/react-from-dom-0.6.2.tgz",
@@ -34964,6 +35487,12 @@
"url": "https://opencollective.com/webpack"
}
},
+ "node_modules/style-mod": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.0.0.tgz",
+ "integrity": "sha512-OPhtyEjyyN9x3nhPsu76f52yUGXiZcgvsrFVtvTkyGRQJ0XK+GPc6ov1z+lRpbeabka+MYEQxOYRnt5nF30aMw==",
+ "peer": true
+ },
"node_modules/style-to-object": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-0.3.0.tgz",
@@ -35110,6 +35639,11 @@
"integrity": "sha512-qImOD23aDfnIDNqlG1NOehdB9IYsn1V9oByPjKY1nakv2MQYCEMyX033/q+aEtYCpmYK1cv2+NTmlH+ra6GA5A==",
"dev": true
},
+ "node_modules/tabbable": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-4.0.0.tgz",
+ "integrity": "sha512-H1XoH1URcBOa/rZZWxLxHCtOdVUEev+9vo5YdYhC9tCY4wnybX+VQrCYuy9ubkg69fCBxCONJOSLGfw0DWMffQ=="
+ },
"node_modules/table": {
"version": "6.8.1",
"resolved": "https://registry.npmjs.org/table/-/table-6.8.1.tgz",
@@ -36169,6 +36703,11 @@
"node": ">=4.2.0"
}
},
+ "node_modules/uc.micro": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz",
+ "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA=="
+ },
"node_modules/uglify-js": {
"version": "3.17.4",
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz",
@@ -37180,6 +37719,11 @@
"resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz",
"integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ=="
},
+ "node_modules/vscode-languageserver-types": {
+ "version": "3.17.3",
+ "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.3.tgz",
+ "integrity": "sha512-SYU4z1dL0PyIMd4Vj8YOqFvHu7Hz/enbWtpfnVbJHU4Nd1YNYx8u0ennumc6h48GQNeOLxmwySmnADouT/AuZA=="
+ },
"node_modules/vscode-oniguruma": {
"version": "1.6.2",
"resolved": "https://registry.npmjs.org/vscode-oniguruma/-/vscode-oniguruma-1.6.2.tgz",
@@ -37200,6 +37744,12 @@
"browser-process-hrtime": "^1.0.0"
}
},
+ "node_modules/w3c-keyname": {
+ "version": "2.2.6",
+ "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.6.tgz",
+ "integrity": "sha512-f+fciywl1SJEniZHD6H+kUO8gOnwIr7f4ijKA6+ZvJFjeGi1r4PDLl53Ayud9O/rk64RqgoQine0feoeOU0kXg==",
+ "peer": true
+ },
"node_modules/w3c-xmlserializer": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-2.0.0.tgz",
@@ -38875,6 +39425,8 @@
"studio": {
"version": "0.0.9",
"dependencies": {
+ "@graphiql/react": "^0.15.0",
+ "@graphiql/toolkit": "^0.8.0",
"@hcaptcha/react-hcaptcha": "^1.4.4",
"@headlessui/react": "^1.4.1",
"@monaco-editor/react": "^4.3.1",
@@ -38897,12 +39449,16 @@
"awesome-debounce-promise": "^2.1.0",
"blueimp-md5": "^2.19.0",
"clipboard": "^2.0.8",
+ "clsx": "^1.2.1",
"common": "*",
"config": "*",
"configcat-js": "^7.0.0",
"dayjs": "^1.11.0",
"file-saver": "^2.0.5",
"generate-password": "^1.7.0",
+ "graphiql": "^2.2.0",
+ "graphql": "^16.6.0",
+ "graphql-ws": "^5.11.3",
"html-to-image": "^1.10.8",
"immutability-helper": "^3.1.1",
"ip-address": "^8.1.0",
@@ -39164,6 +39720,14 @@
"postcss": "^8.1.0"
}
},
+ "studio/node_modules/clsx": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
+ "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"studio/node_modules/configcat-common": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/configcat-common/-/configcat-common-7.0.1.tgz",
@@ -41002,6 +41566,37 @@
"unist-util-visit-parents": "^3.0.0"
}
},
+ "@codemirror/language": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.0.0.tgz",
+ "integrity": "sha512-rtjk5ifyMzOna1c7PBu7J1VCt0PvA5wy3o8eMVnxMKb7z8KA7JFecvD04dSn14vj/bBaAbqRsGed5OjtofEnLA==",
+ "peer": true,
+ "requires": {
+ "@codemirror/state": "^6.0.0",
+ "@codemirror/view": "^6.0.0",
+ "@lezer/common": "^1.0.0",
+ "@lezer/highlight": "^1.0.0",
+ "@lezer/lr": "^1.0.0",
+ "style-mod": "^4.0.0"
+ }
+ },
+ "@codemirror/state": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.2.0.tgz",
+ "integrity": "sha512-69QXtcrsc3RYtOtd+GsvczJ319udtBf1PTrr2KbLWM/e2CXUPnh0Nz9AUo8WfhSQ7GeL8dPVNUmhQVgpmuaNGA==",
+ "peer": true
+ },
+ "@codemirror/view": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.9.0.tgz",
+ "integrity": "sha512-uFaqE6fBOp0Dj/tmWoe/TFlSSIPdpAzhvATgbq1eAKRkgq3hY79FioZ7nfdiT+24kz68AIWuSZ9hi3psKe36WQ==",
+ "peer": true,
+ "requires": {
+ "@codemirror/state": "^6.1.4",
+ "style-mod": "^4.0.0",
+ "w3c-keyname": "^2.2.4"
+ }
+ },
"@colors/colors": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz",
@@ -41268,6 +41863,46 @@
"integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==",
"dev": true
},
+ "@graphiql/react": {
+ "version": "0.15.0",
+ "resolved": "https://registry.npmjs.org/@graphiql/react/-/react-0.15.0.tgz",
+ "integrity": "sha512-kJqkdf6d4Cck05Wt5yCDZXWfs7HZgcpuoWq/v8nOa698qVaNMM3qdG4CpRsZEexku0DSSJzWWuanxd5x+sRcFg==",
+ "requires": {
+ "@graphiql/toolkit": "^0.8.0",
+ "@reach/combobox": "^0.17.0",
+ "@reach/dialog": "^0.17.0",
+ "@reach/listbox": "^0.17.0",
+ "@reach/menu-button": "^0.17.0",
+ "@reach/tooltip": "^0.17.0",
+ "@reach/visually-hidden": "^0.17.0",
+ "codemirror": "^5.65.3",
+ "codemirror-graphql": "^2.0.2",
+ "copy-to-clipboard": "^3.2.0",
+ "graphql-language-service": "^5.1.0",
+ "markdown-it": "^12.2.0",
+ "set-value": "^4.1.0"
+ },
+ "dependencies": {
+ "set-value": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/set-value/-/set-value-4.1.0.tgz",
+ "integrity": "sha512-zTEg4HL0RwVrqcWs3ztF+x1vkxfm0lP+MQQFPiMJTKVceBwEV0A569Ou8l9IYQG8jOZdMVI1hGsc0tmeD2o/Lw==",
+ "requires": {
+ "is-plain-object": "^2.0.4",
+ "is-primitive": "^3.0.1"
+ }
+ }
+ }
+ },
+ "@graphiql/toolkit": {
+ "version": "0.8.0",
+ "resolved": "https://registry.npmjs.org/@graphiql/toolkit/-/toolkit-0.8.0.tgz",
+ "integrity": "sha512-DbMFhEKejpPzB6k8W3Mj+Rl8geXiw49USDF9Wdi06EEk1XLVh1iebDqveYY+4lViITsV4+BeGikxlqi8umfP4g==",
+ "requires": {
+ "@n1ru4l/push-pull-async-iterable-iterator": "^3.1.0",
+ "meros": "^1.1.4"
+ }
+ },
"@hcaptcha/react-hcaptcha": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/@hcaptcha/react-hcaptcha/-/react-hcaptcha-1.4.4.tgz",
@@ -42089,6 +42724,30 @@
"@jridgewell/sourcemap-codec": "1.4.14"
}
},
+ "@lezer/common": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.0.2.tgz",
+ "integrity": "sha512-SVgiGtMnMnW3ActR8SXgsDhw7a0w0ChHSYAyAUxxrOiJ1OqYWEKk/xJd84tTSPo1mo6DXLObAJALNnd0Hrv7Ng==",
+ "peer": true
+ },
+ "@lezer/highlight": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.1.3.tgz",
+ "integrity": "sha512-3vLKLPThO4td43lYRBygmMY18JN3CPh9w+XS2j8WC30vR4yZeFG4z1iFe4jXE43NtGqe//zHW5q8ENLlHvz9gw==",
+ "peer": true,
+ "requires": {
+ "@lezer/common": "^1.0.0"
+ }
+ },
+ "@lezer/lr": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.3.3.tgz",
+ "integrity": "sha512-JPQe3mwJlzEVqy67iQiiGozhcngbO8QBgpqZM6oL1Wj/dXckrEexpBLeFkq0edtW5IqnPRFxA24BHJni8Js69w==",
+ "peer": true,
+ "requires": {
+ "@lezer/common": "^1.0.0"
+ }
+ },
"@mapbox/node-pre-gyp": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.10.tgz",
@@ -42423,6 +43082,11 @@
}
}
},
+ "@n1ru4l/push-pull-async-iterable-iterator": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/@n1ru4l/push-pull-async-iterable-iterator/-/push-pull-async-iterable-iterator-3.2.0.tgz",
+ "integrity": "sha512-3fkKj25kEjsfObL6IlKPAlHYPq/oYwUkkQ03zsTTiDjD7vg/RxjdiLeCydqtxHZP0JgsXL3D/X5oAkMGzuUp/Q=="
+ },
"@next/bundle-analyzer": {
"version": "13.0.5",
"resolved": "https://registry.npmjs.org/@next/bundle-analyzer/-/bundle-analyzer-13.0.5.tgz",
@@ -44135,6 +44799,172 @@
"@babel/runtime": "^7.13.10"
}
},
+ "@reach/auto-id": {
+ "version": "0.17.0",
+ "resolved": "https://registry.npmjs.org/@reach/auto-id/-/auto-id-0.17.0.tgz",
+ "integrity": "sha512-ud8iPwF52RVzEmkHq1twuqGuPA+moreumUHdtgvU3sr3/15BNhwp3KyDLrKKSz0LP1r3V4pSdyF9MbYM8BoSjA==",
+ "requires": {
+ "@reach/utils": "0.17.0",
+ "tslib": "^2.3.0"
+ }
+ },
+ "@reach/combobox": {
+ "version": "0.17.0",
+ "resolved": "https://registry.npmjs.org/@reach/combobox/-/combobox-0.17.0.tgz",
+ "integrity": "sha512-2mYvU5agOBCQBMdlM4cri+P1BbNwp05P1OuDyc33xJSNiBG7BMy4+ZSHJ0X4fyle6rHwSgCAOCLOeWV1XUYjoQ==",
+ "requires": {
+ "@reach/auto-id": "0.17.0",
+ "@reach/descendants": "0.17.0",
+ "@reach/popover": "0.17.0",
+ "@reach/portal": "0.17.0",
+ "@reach/utils": "0.17.0",
+ "prop-types": "^15.7.2",
+ "tiny-warning": "^1.0.3",
+ "tslib": "^2.3.0"
+ }
+ },
+ "@reach/descendants": {
+ "version": "0.17.0",
+ "resolved": "https://registry.npmjs.org/@reach/descendants/-/descendants-0.17.0.tgz",
+ "integrity": "sha512-c7lUaBfjgcmKFZiAWqhG+VnXDMEhPkI4kAav/82XKZD6NVvFjsQOTH+v3tUkskrAPV44Yuch0mFW/u5Ntifr7Q==",
+ "requires": {
+ "@reach/utils": "0.17.0",
+ "tslib": "^2.3.0"
+ }
+ },
+ "@reach/dialog": {
+ "version": "0.17.0",
+ "resolved": "https://registry.npmjs.org/@reach/dialog/-/dialog-0.17.0.tgz",
+ "integrity": "sha512-AnfKXugqDTGbeG3c8xDcrQDE4h9b/vnc27Sa118oQSquz52fneUeX9MeFb5ZEiBJK8T5NJpv7QUTBIKnFCAH5A==",
+ "requires": {
+ "@reach/portal": "0.17.0",
+ "@reach/utils": "0.17.0",
+ "prop-types": "^15.7.2",
+ "react-focus-lock": "^2.5.2",
+ "react-remove-scroll": "^2.4.3",
+ "tslib": "^2.3.0"
+ }
+ },
+ "@reach/dropdown": {
+ "version": "0.17.0",
+ "resolved": "https://registry.npmjs.org/@reach/dropdown/-/dropdown-0.17.0.tgz",
+ "integrity": "sha512-qBTIGInhxtPHtdj4Pl2XZgZMz3e37liydh0xR3qc48syu7g71sL4nqyKjOzThykyfhA3Pb3/wFgsFJKGTSdaig==",
+ "requires": {
+ "@reach/auto-id": "0.17.0",
+ "@reach/descendants": "0.17.0",
+ "@reach/popover": "0.17.0",
+ "@reach/utils": "0.17.0",
+ "tslib": "^2.3.0"
+ }
+ },
+ "@reach/listbox": {
+ "version": "0.17.0",
+ "resolved": "https://registry.npmjs.org/@reach/listbox/-/listbox-0.17.0.tgz",
+ "integrity": "sha512-AMnH1P6/3VKy2V/nPb4Es441arYR+t4YRdh9jdcFVrCOD6y7CQrlmxsYjeg9Ocdz08XpdoEBHM3PKLJqNAUr7A==",
+ "requires": {
+ "@reach/auto-id": "0.17.0",
+ "@reach/descendants": "0.17.0",
+ "@reach/machine": "0.17.0",
+ "@reach/popover": "0.17.0",
+ "@reach/utils": "0.17.0",
+ "prop-types": "^15.7.2"
+ }
+ },
+ "@reach/machine": {
+ "version": "0.17.0",
+ "resolved": "https://registry.npmjs.org/@reach/machine/-/machine-0.17.0.tgz",
+ "integrity": "sha512-9EHnuPgXzkbRENvRUzJvVvYt+C2jp7PGN0xon7ffmKoK8rTO6eA/bb7P0xgloyDDQtu88TBUXKzW0uASqhTXGA==",
+ "requires": {
+ "@reach/utils": "0.17.0",
+ "@xstate/fsm": "1.4.0",
+ "tslib": "^2.3.0"
+ }
+ },
+ "@reach/menu-button": {
+ "version": "0.17.0",
+ "resolved": "https://registry.npmjs.org/@reach/menu-button/-/menu-button-0.17.0.tgz",
+ "integrity": "sha512-YyuYVyMZKamPtivoEI6D0UEILYH3qZtg4kJzEAuzPmoR/aHN66NZO75Fx0gtjG1S6fZfbiARaCOZJC0VEiDOtQ==",
+ "requires": {
+ "@reach/dropdown": "0.17.0",
+ "@reach/popover": "0.17.0",
+ "@reach/utils": "0.17.0",
+ "prop-types": "^15.7.2",
+ "tiny-warning": "^1.0.3",
+ "tslib": "^2.3.0"
+ }
+ },
+ "@reach/observe-rect": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@reach/observe-rect/-/observe-rect-1.2.0.tgz",
+ "integrity": "sha512-Ba7HmkFgfQxZqqaeIWWkNK0rEhpxVQHIoVyW1YDSkGsGIXzcaW4deC8B0pZrNSSyLTdIk7y+5olKt5+g0GmFIQ=="
+ },
+ "@reach/popover": {
+ "version": "0.17.0",
+ "resolved": "https://registry.npmjs.org/@reach/popover/-/popover-0.17.0.tgz",
+ "integrity": "sha512-yYbBF4fMz4Ml4LB3agobZjcZ/oPtPsNv70ZAd7lEC2h7cvhF453pA+zOBGYTPGupKaeBvgAnrMjj7RnxDU5hoQ==",
+ "requires": {
+ "@reach/portal": "0.17.0",
+ "@reach/rect": "0.17.0",
+ "@reach/utils": "0.17.0",
+ "tabbable": "^4.0.0",
+ "tslib": "^2.3.0"
+ }
+ },
+ "@reach/portal": {
+ "version": "0.17.0",
+ "resolved": "https://registry.npmjs.org/@reach/portal/-/portal-0.17.0.tgz",
+ "integrity": "sha512-+IxsgVycOj+WOeNPL2NdgooUdHPSY285wCtj/iWID6akyr4FgGUK7sMhRM9aGFyrGpx2vzr+eggbUmAVZwOz+A==",
+ "requires": {
+ "@reach/utils": "0.17.0",
+ "tiny-warning": "^1.0.3",
+ "tslib": "^2.3.0"
+ }
+ },
+ "@reach/rect": {
+ "version": "0.17.0",
+ "resolved": "https://registry.npmjs.org/@reach/rect/-/rect-0.17.0.tgz",
+ "integrity": "sha512-3YB7KA5cLjbLc20bmPkJ06DIfXSK06Cb5BbD2dHgKXjUkT9WjZaLYIbYCO8dVjwcyO3GCNfOmPxy62VsPmZwYA==",
+ "requires": {
+ "@reach/observe-rect": "1.2.0",
+ "@reach/utils": "0.17.0",
+ "prop-types": "^15.7.2",
+ "tiny-warning": "^1.0.3",
+ "tslib": "^2.3.0"
+ }
+ },
+ "@reach/tooltip": {
+ "version": "0.17.0",
+ "resolved": "https://registry.npmjs.org/@reach/tooltip/-/tooltip-0.17.0.tgz",
+ "integrity": "sha512-HP8Blordzqb/Cxg+jnhGmWQfKgypamcYLBPlcx6jconyV5iLJ5m93qipr1giK7MqKT2wlsKWy44ZcOrJ+Wrf8w==",
+ "requires": {
+ "@reach/auto-id": "0.17.0",
+ "@reach/portal": "0.17.0",
+ "@reach/rect": "0.17.0",
+ "@reach/utils": "0.17.0",
+ "@reach/visually-hidden": "0.17.0",
+ "prop-types": "^15.7.2",
+ "tiny-warning": "^1.0.3",
+ "tslib": "^2.3.0"
+ }
+ },
+ "@reach/utils": {
+ "version": "0.17.0",
+ "resolved": "https://registry.npmjs.org/@reach/utils/-/utils-0.17.0.tgz",
+ "integrity": "sha512-M5y8fCBbrWeIsxedgcSw6oDlAMQDkl5uv3VnMVJ7guwpf4E48Xlh1v66z/1BgN/WYe2y8mB/ilFD2nysEfdGeA==",
+ "requires": {
+ "tiny-warning": "^1.0.3",
+ "tslib": "^2.3.0"
+ }
+ },
+ "@reach/visually-hidden": {
+ "version": "0.17.0",
+ "resolved": "https://registry.npmjs.org/@reach/visually-hidden/-/visually-hidden-0.17.0.tgz",
+ "integrity": "sha512-T6xF3Nv8vVnjVkGU6cm0+kWtvliLqPAo8PcZ+WxkKacZsaHTjaZb4v1PaCcyQHmuTNT/vtTVNOJLG0SjQOIb7g==",
+ "requires": {
+ "prop-types": "^15.7.2",
+ "tslib": "^2.3.0"
+ }
+ },
"@react-dnd/asap": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@react-dnd/asap/-/asap-4.0.1.tgz",
@@ -47351,6 +48181,11 @@
"@xtuc/long": "4.2.2"
}
},
+ "@xstate/fsm": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/@xstate/fsm/-/fsm-1.4.0.tgz",
+ "integrity": "sha512-uTHDeu2xI5E1IFwf37JFQM31RrH7mY7877RqPBS4ZqSNUwoLDuct8AhBWaXGnVizBAYyimVwgCyGa9z/NiRhXA=="
+ },
"@xtuc/ieee754": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz",
@@ -49297,6 +50132,19 @@
"resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz",
"integrity": "sha512-RpAVKQA5T63xEj6/giIbUEtZwJ4UFIc3ZtvEkiaUERylqe8xb5IvqcgOurZLahv93CLKfxcw5YI+DZcUBRyLXA=="
},
+ "codemirror": {
+ "version": "5.65.11",
+ "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.65.11.tgz",
+ "integrity": "sha512-Gp62g2eKSCHYt10axmGhKq3WoJSvVpvhXmowNq7pZdRVowwtvBR/hi2LSP5srtctKkRT33T6/n8Kv1UGp7JW4A=="
+ },
+ "codemirror-graphql": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/codemirror-graphql/-/codemirror-graphql-2.0.3.tgz",
+ "integrity": "sha512-T58f+XQVKE6/6goAADJfu56wPRcdF2RUZMHITUol6h6Uo1KKUmknV4muKNFOl6GM15BjBraysF6HATp4oY6kyA==",
+ "requires": {
+ "graphql-language-service": "5.1.1"
+ }
+ },
"coffee-script": {
"version": "1.12.7",
"resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.12.7.tgz",
@@ -52024,8 +52872,7 @@
"entities": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz",
- "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==",
- "dev": true
+ "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A=="
},
"env-paths": {
"version": "2.2.1",
@@ -54150,6 +54997,18 @@
"integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==",
"dev": true
},
+ "graphiql": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/graphiql/-/graphiql-2.2.0.tgz",
+ "integrity": "sha512-w1ujpCKMlkwkoUjeg0HpRiBBTm1WHAjHNkFv1TbMu6trjzz63mQ48GLZlmyQY1yhwmc+diCcvmmAt+AyvKLWWA==",
+ "requires": {
+ "@graphiql/react": "^0.15.0",
+ "@graphiql/toolkit": "^0.8.0",
+ "entities": "^2.0.0",
+ "graphql-language-service": "^5.1.0",
+ "markdown-it": "^12.2.0"
+ }
+ },
"graphlib": {
"version": "2.1.8",
"resolved": "https://registry.npmjs.org/graphlib/-/graphlib-2.1.8.tgz",
@@ -54158,6 +55017,26 @@
"lodash": "^4.17.15"
}
},
+ "graphql": {
+ "version": "16.6.0",
+ "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.6.0.tgz",
+ "integrity": "sha512-KPIBPDlW7NxrbT/eh4qPXz5FiFdL5UbaA0XUNz2Rp3Z3hqBSkbj0GVjwFDztsWVauZUWsbKHgMg++sk8UX0bkw=="
+ },
+ "graphql-language-service": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/graphql-language-service/-/graphql-language-service-5.1.1.tgz",
+ "integrity": "sha512-gpaDT9E3+3eWhoqO4C81CGhkzr7Vp2jH/eq+ykoUbgfvMEpqhGTfCeNmrf+S4K/+4WTkAAJBsYT0/ZPZkqe/Hg==",
+ "requires": {
+ "nullthrows": "^1.0.0",
+ "vscode-languageserver-types": "^3.17.1"
+ }
+ },
+ "graphql-ws": {
+ "version": "5.11.3",
+ "resolved": "https://registry.npmjs.org/graphql-ws/-/graphql-ws-5.11.3.tgz",
+ "integrity": "sha512-fU8zwSgAX2noXAsuFiCZ8BtXeXZOzXyK5u1LloCdacsVth4skdBMPO74EG51lBoWSIZ8beUocdpV8+cQHBODnQ==",
+ "requires": {}
+ },
"gray-matter": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz",
@@ -55373,6 +56252,11 @@
"integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
"dev": true
},
+ "is-primitive": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-3.0.1.tgz",
+ "integrity": "sha512-GljRxhWvlCNRfZyORiH77FwdFwGcMO620o37EOYC0ORWdq+WYNVqW0w2Juzew4M+L81l6/QS3t5gkkihyRqv9w=="
+ },
"is-reference": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.0.tgz",
@@ -57462,6 +58346,14 @@
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
"devOptional": true
},
+ "linkify-it": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.3.tgz",
+ "integrity": "sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==",
+ "requires": {
+ "uc.micro": "^1.0.1"
+ }
+ },
"list-item": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/list-item/-/list-item-1.1.1.tgz",
@@ -57785,6 +58677,25 @@
"resolved": "https://registry.npmjs.org/markdown-extensions/-/markdown-extensions-1.1.1.tgz",
"integrity": "sha512-WWC0ZuMzCyDHYCasEGs4IPvLyTGftYwh6wIEOULOF0HXcqZlhwRzrK0w2VUlxWA98xnvb/jszw4ZSkJ6ADpM6Q=="
},
+ "markdown-it": {
+ "version": "12.3.2",
+ "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.2.tgz",
+ "integrity": "sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==",
+ "requires": {
+ "argparse": "^2.0.1",
+ "entities": "~2.1.0",
+ "linkify-it": "^3.0.1",
+ "mdurl": "^1.0.1",
+ "uc.micro": "^1.0.5"
+ },
+ "dependencies": {
+ "entities": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz",
+ "integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w=="
+ }
+ }
+ },
"markdown-link": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/markdown-link/-/markdown-link-0.1.1.tgz",
@@ -58509,6 +59420,12 @@
}
}
},
+ "meros": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/meros/-/meros-1.2.1.tgz",
+ "integrity": "sha512-R2f/jxYqCAGI19KhAvaxSOxALBMkaXWH2a7rOyqQw+ZmizX5bKkEYWLzdhC+U82ZVVPVp6MCXe3EkVligh+12g==",
+ "requires": {}
+ },
"methods": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
@@ -60087,6 +61004,11 @@
"boolbase": "^1.0.0"
}
},
+ "nullthrows": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz",
+ "integrity": "sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw=="
+ },
"num2fraction": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/num2fraction/-/num2fraction-1.2.2.tgz",
@@ -62703,6 +63625,14 @@
"use-memo-one": "^1.1.1"
}
},
+ "react-clientside-effect": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/react-clientside-effect/-/react-clientside-effect-1.2.6.tgz",
+ "integrity": "sha512-XGGGRQAKY+q25Lz9a/4EPqom7WRjz3z9R2k4jhVKA/puQFH/5Nt27vFZYql4m4NVNdUvX8PS3O7r/Zzm7cjUlg==",
+ "requires": {
+ "@babel/runtime": "^7.12.13"
+ }
+ },
"react-contexify": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/react-contexify/-/react-contexify-5.0.0.tgz",
@@ -62878,6 +63808,29 @@
"prop-types": "^15.7.2"
}
},
+ "react-focus-lock": {
+ "version": "2.9.3",
+ "resolved": "https://registry.npmjs.org/react-focus-lock/-/react-focus-lock-2.9.3.tgz",
+ "integrity": "sha512-cGNkz9p5Fpqio6hBHlkKxzRYrBYtcPosFOL6Q3N/LSbHjwP/PTBqHpvbgaOYoE7rWfzw8qXPKTB3Tk/VPgw4NQ==",
+ "requires": {
+ "@babel/runtime": "^7.0.0",
+ "focus-lock": "^0.11.5",
+ "prop-types": "^15.6.2",
+ "react-clientside-effect": "^1.2.6",
+ "use-callback-ref": "^1.3.0",
+ "use-sidecar": "^1.1.2"
+ },
+ "dependencies": {
+ "focus-lock": {
+ "version": "0.11.5",
+ "resolved": "https://registry.npmjs.org/focus-lock/-/focus-lock-0.11.5.tgz",
+ "integrity": "sha512-1mTr6pl9HBpJ8CqY7hRc38MCrcuTZIeYAkBD1gBTzbx5/to+bRBaBYtJ68iDq7ryTzAAbKrG3dVKjkrWTaaEaw==",
+ "requires": {
+ "tslib": "^2.0.3"
+ }
+ }
+ }
+ },
"react-from-dom": {
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/react-from-dom/-/react-from-dom-0.6.2.tgz",
@@ -65695,6 +66648,8 @@
"version": "file:studio",
"requires": {
"@babel/core": "^7.17.5",
+ "@graphiql/react": "^0.15.0",
+ "@graphiql/toolkit": "^0.8.0",
"@hcaptcha/react-hcaptcha": "^1.4.4",
"@headlessui/react": "^1.4.1",
"@monaco-editor/react": "^4.3.1",
@@ -65752,12 +66707,16 @@
"babel-loader": "^8.2.3",
"blueimp-md5": "^2.19.0",
"clipboard": "^2.0.8",
+ "clsx": "^1.2.1",
"common": "*",
"config": "*",
"configcat-js": "^7.0.0",
"dayjs": "^1.11.0",
"file-saver": "^2.0.5",
"generate-password": "^1.7.0",
+ "graphiql": "^2.2.0",
+ "graphql": "^16.6.0",
+ "graphql-ws": "^5.11.3",
"html-to-image": "^1.10.8",
"immutability-helper": "^3.1.1",
"ip-address": "^8.1.0",
@@ -65917,6 +66876,11 @@
"postcss-value-parser": "^4.2.0"
}
},
+ "clsx": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
+ "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg=="
+ },
"configcat-common": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/configcat-common/-/configcat-common-7.0.1.tgz",
@@ -66121,6 +67085,12 @@
}
}
},
+ "style-mod": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.0.0.tgz",
+ "integrity": "sha512-OPhtyEjyyN9x3nhPsu76f52yUGXiZcgvsrFVtvTkyGRQJ0XK+GPc6ov1z+lRpbeabka+MYEQxOYRnt5nF30aMw==",
+ "peer": true
+ },
"style-to-object": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-0.3.0.tgz",
@@ -66318,6 +67288,11 @@
"integrity": "sha512-qImOD23aDfnIDNqlG1NOehdB9IYsn1V9oByPjKY1nakv2MQYCEMyX033/q+aEtYCpmYK1cv2+NTmlH+ra6GA5A==",
"dev": true
},
+ "tabbable": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-4.0.0.tgz",
+ "integrity": "sha512-H1XoH1URcBOa/rZZWxLxHCtOdVUEev+9vo5YdYhC9tCY4wnybX+VQrCYuy9ubkg69fCBxCONJOSLGfw0DWMffQ=="
+ },
"table": {
"version": "6.8.1",
"resolved": "https://registry.npmjs.org/table/-/table-6.8.1.tgz",
@@ -67081,6 +68056,11 @@
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.3.tgz",
"integrity": "sha512-CIfGzTelbKNEnLpLdGFgdyKhG23CKdKgQPOBc+OUNrkJ2vr+KSzsSV5kq5iWhEQbok+quxgGzrAtGWCyU7tHnA=="
},
+ "uc.micro": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz",
+ "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA=="
+ },
"uglify-js": {
"version": "3.17.4",
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz",
@@ -68032,6 +69012,11 @@
"resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz",
"integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ=="
},
+ "vscode-languageserver-types": {
+ "version": "3.17.3",
+ "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.3.tgz",
+ "integrity": "sha512-SYU4z1dL0PyIMd4Vj8YOqFvHu7Hz/enbWtpfnVbJHU4Nd1YNYx8u0ennumc6h48GQNeOLxmwySmnADouT/AuZA=="
+ },
"vscode-oniguruma": {
"version": "1.6.2",
"resolved": "https://registry.npmjs.org/vscode-oniguruma/-/vscode-oniguruma-1.6.2.tgz",
@@ -68051,6 +69036,12 @@
"browser-process-hrtime": "^1.0.0"
}
},
+ "w3c-keyname": {
+ "version": "2.2.6",
+ "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.6.tgz",
+ "integrity": "sha512-f+fciywl1SJEniZHD6H+kUO8gOnwIr7f4ijKA6+ZvJFjeGi1r4PDLl53Ayud9O/rk64RqgoQine0feoeOU0kXg==",
+ "peer": true
+ },
"w3c-xmlserializer": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-2.0.0.tgz",
diff --git a/packages/ui/src/components/Accordion/Accordion.tsx b/packages/ui/src/components/Accordion/Accordion.tsx
index 4a802c44ed6..11ca83de563 100644
--- a/packages/ui/src/components/Accordion/Accordion.tsx
+++ b/packages/ui/src/components/Accordion/Accordion.tsx
@@ -37,14 +37,13 @@ interface AccordionProps {
defaultActiveId?: (string | number)[]
icon?: React.ReactNode
iconPosition?: Align
- bordered: boolean
onChange?: (item: string | string[]) => void
openBehaviour: 'single' | 'multiple'
type: Type
size: Size
defaultValue?: string | string[] | undefined
- justified: Boolean
- chevronAlign: Align
+ justified?: Boolean
+ chevronAlign?: Align
}
function Accordion({
@@ -58,8 +57,8 @@ function Accordion({
type = 'default',
// size, // TO DO
defaultValue = undefined,
- justified = true,
- chevronAlign,
+ justified = false,
+ chevronAlign = 'left',
}: AccordionProps) {
// const [currentItems, setCurrentItems] = useState(defaultValue || [])
diff --git a/spec/supabase_py_v2.yml b/spec/supabase_py_v2.yml
index a3af18a5acd..486e429f3e1 100644
--- a/spec/supabase_py_v2.yml
+++ b/spec/supabase_py_v2.yml
@@ -281,18 +281,360 @@ functions:
name: Getting your data
code: |
```
- r = supabase
- .from('countries')
- .select("*").execute()
+ response = supabase.table('countries').select("*").execute()
+ ```
+ data:
+ sql: |
+ ```sql
+ create table
+ countries (id int8 primary key, name text);
+
+ insert into
+ countries (id, name)
+ values
+ (1, 'Afghanistan'),
+ (2, 'Albania'),
+ (3, 'Algeria');
+ ```
+ response: |
+ ```
+ APIResponse(data=[{'id': 1, 'name': 'Afghanistan'},
+ {'id': 2, 'name': 'Albania'},
+ {'id': 3, 'name': 'Algeria'}],
+ count=None)
```
- id: selecting-specific-columns
name: Selecting specific columns
code: |
```
- r = await supabase
- .from('countries')
- .select('name').execute()
+ response = supabase.table('countries').select('name').execute()
```
+ data:
+ sql: |
+ ```sql
+ create table
+ countries (id int8 primary key, name text);
+
+ insert into
+ countries (id, name)
+ values
+ (1, 'Afghanistan'),
+ (2, 'Albania'),
+ (3, 'Algeria');
+ ```
+ response: |
+ ```
+ APIResponse(data=[{'name': 'Afghanistan'},
+ {'name': 'Albania'},
+ {'name': 'Algeria'}],
+ count=None)
+ ```
+ - id: query-foreign-tables
+ name: Query foreign tables
+ description: |
+ If your database has foreign key relationships, you can query related tables too.
+ code: |
+ ```
+ response = supabase.table('countries').select('name, cities(name)').execute()
+ ```
+ data:
+ sql: |
+ ```sql
+ create table
+ countries (id int8 primary key, name text);
+ create table
+ cities (
+ id int8 primary key,
+ country_id int8 not null references countries,
+ name text
+ );
+
+ insert into
+ countries (id, name)
+ values
+ (1, 'Germany'),
+ (2, 'Indonesia');
+ insert into
+ cities (id, country_id, name)
+ values
+ (1, 2, 'Bali'),
+ (2, 1, 'Munich');
+ ```
+ response: |
+ ```
+ APIResponse(data=[{'name': 'Germany', 'cities': [{'name': 'Munich'}]},
+ {'name': 'Indonesia', 'cities': [{'name': 'Bali'}]}],
+ count=None)
+ ```
+ - id: query-foreign-tables-through-a-join-table
+ name: Query foreign tables through a join table
+ code: |
+ ```
+ response = supabase.table('users').select('name, teams(name)').execute()
+ ```
+ data:
+ sql: |
+ ```sql
+ create table
+ users (
+ id int8 primary key,
+ name text
+ );
+ create table
+ teams (
+ id int8 primary key,
+ name text
+ );
+ -- join table
+ create table
+ users_teams (
+ user_id int8 not null references users,
+ team_id int8 not null references teams,
+ -- both foreign keys must be part of a composite primary key
+ primary key (user_id, team_id)
+ );
+
+ insert into
+ users (id, name)
+ values
+ (1, 'Kiran'),
+ (2, 'Evan');
+ insert into
+ teams (id, name)
+ values
+ (1, 'Green'),
+ (2, 'Blue');
+ insert into
+ users_teams (user_id, team_id)
+ values
+ (1, 1),
+ (1, 2),
+ (2, 2);
+ ```
+ response: |
+ ```
+ APIResponse(data=[
+ {'name': 'Kiran', 'teams': [{'name': 'Green'}, {'name': 'Blue'}]},
+ {'name': 'Evan', 'teams': [{'name': 'Blue'}]}
+ ],
+ count=None)
+ ```
+ description: |
+ If you're in a situation where your tables are **NOT** directly
+ related, but instead are joined by a _join table_, you can still use
+ the `select()` method to query the related data. The join table needs
+ to have the foreign keys as part of its composite primary key.
+ hideCodeBlock: true
+ - id: query-the-same-foreign-table-multiple-times
+ name: Query the same foreign table multiple times
+ code: |
+ ```
+ response = supabase.table('messages').select('content,from:sender_id(name),to:receiver_id(name)').execute()
+ ```
+ data:
+ sql: |
+ ```sql
+ create table
+ users (id int8 primary key, name text);
+
+ create table
+ messages (
+ sender_id int8 not null references users,
+ receiver_id int8 not null references users,
+ content text
+ );
+
+ insert into
+ users (id, name)
+ values
+ (1, 'Kiran'),
+ (2, 'Evan');
+
+ insert into
+ messages (sender_id, receiver_id, content)
+ values
+ (1, 2, '👋');
+ ```
+ response: |
+ ```
+ APIResponse(data=[{'content': '👋',
+ 'from': {'name': 'Kiran'},
+ 'to': {'name': 'Evan'}}
+ ],
+ count=None)
+ ```
+ description: |
+ If you need to query the same foreign table twice, use the name of the
+ joined column to identify which join to use. You can also give each
+ column an alias.
+ hideCodeBlock: true
+ - id: filtering-through-foreign-tables
+ name: Filtering through foreign tables
+ code: |
+ ```
+ response = supabase.table('cities').select('name, countries(*)').eq('countries.name', 'Estonia').execute()
+ ```
+ data:
+ sql: |
+ ```sql
+ create table
+ countries (id int8 primary key, name text);
+ create table
+ cities (
+ id int8 primary key,
+ country_id int8 not null references countries,
+ name text
+ );
+
+ insert into
+ countries (id, name)
+ values
+ (1, 'Germany'),
+ (2, 'Indonesia');
+ insert into
+ cities (id, country_id, name)
+ values
+ (1, 2, 'Bali'),
+ (2, 1, 'Munich');
+ ```
+ response: |
+ ```
+ APIResponse(data=[{'name': 'Bali', 'countries': None},
+ {'name': 'Munich', 'countries': None}],
+ count=None)
+ ```
+ description: |
+ NOT WORKING CURRENTLY
+ If the filter on a foreign table's column is not satisfied, the foreign
+ table returns `[]` or `null` but the parent table is not filtered out.
+ If you want to filter out the parent table rows, use the `!inner` hint
+ hideCodeBlock: true
+ - id: querying-foreign-table-with-count
+ name: Querying foreign table with count
+ code: |
+ ```
+ response = supabase.table('countries').select('*, cities(count)').execute()
+ ```
+ data:
+ sql: |
+ ```sql
+ create table countries (
+ "id" "uuid" primary key default "extensions"."uuid_generate_v4"() not null,
+ "name" text
+ );
+
+ create table cities (
+ "id" "uuid" primary key default "extensions"."uuid_generate_v4"() not null,
+ "name" text,
+ "country_id" "uuid" references public.countries on delete cascade
+ );
+
+ with country as (
+ insert into countries (name)
+ values ('united kingdom') returning id
+ )
+ insert into cities (name, country_id) values
+ ('London', (select id from country)),
+ ('Manchester', (select id from country)),
+ ('Liverpool', (select id from country)),
+ ('Bristol', (select id from country));
+ ```
+ response: |
+ ```
+ APIResponse(data=[{'id': 1, 'name': 'Germany', 'cities': [{'count': 1}]},
+ {'id': 2, 'name': 'Indonesia', 'cities': [{'count': 1}]}],
+ count=None)
+ ```
+ description: |
+ You can get the number of rows in a related table by using the
+ **count** property.
+ hideCodeBlock: true
+ - id: querying-with-count-option
+ name: Querying with count option
+ code: |
+ ```
+ response = supabase.table('countries').select('*', count='exact').execute()
+ ```
+ data:
+ sql: |
+ ```sql
+ create table
+ countries (id int8 primary key, name text);
+
+ insert into
+ countries (id, name)
+ values
+ (1, 'Afghanistan'),
+ (2, 'Albania'),
+ (3, 'Algeria');
+ ```
+ response: |
+ ```
+ APIResponse(data=[{'id': 1, 'name': 'Germany'},
+ {'id': 2, 'name': 'Indonesia'}],
+ count=2)
+ ```
+ description: |
+ You can get the number of rows by using the
+ *count* parameter in the select query.
+ hideCodeBlock: true
+ - id: querying-json-data
+ name: Querying JSON data
+ code: |
+ ```
+ response = supabase.table('users').select('id, name, address->city').execute()
+ ```
+ data:
+ sql: |
+ ```sql
+ create table
+ users (
+ id int8 primary key,
+ name text,
+ address jsonb
+ );
+
+ insert into
+ users (id, name, address)
+ values
+ (1, 'Avdotya', '{"city":"Saint Petersburg"}');
+ ```
+ response: |
+ ```
+ APIResponse(data=[{'id': 1, 'name': 'Avdotya', 'city': 'Saint Petersburg'}], count=None)
+ ```
+ description: |
+ You can select and filter data inside of
+ [JSON](/docs/guides/database/json) columns. Postgres offers some
+ [operators](/docs/guides/database/json#query-the-jsonb-data) for
+ querying JSON data.
+ hideCodeBlock: true
+
+ - id: insert
+ title: 'Create data: insert()'
+ $ref: '@supabase/postgrest-js.PostgrestQueryBuilder.insert'
+ examples:
+ - id: create-a-record
+ name: Create a record
+ code: |
+ ```
+ data, count = supabase
+ .table('countries')
+ .insert({"id": 1, "name": "Denmark"})
+ .execute()
+ ```
+ data:
+ sql: |
+ ```sql
+ create table
+ countries (id int8 primary key, name text);
+ ```
+ response: |
+ ```
+ APIResponse(data=[{'id': 1, 'name': 'Denmark'}], count=None)
+ ```
+ hideCodeBlock: true
+ isSpotlight: true
- id: invoke
title: 'invoke()'
diff --git a/studio/components/grid/components/header/Header.tsx b/studio/components/grid/components/header/Header.tsx
index d839ced0cfd..1b7c0d8fbce 100644
--- a/studio/components/grid/components/header/Header.tsx
+++ b/studio/components/grid/components/header/Header.tsx
@@ -1,17 +1,9 @@
-import { FC, useState, ReactNode } from 'react'
-import {
- Button,
- IconDownload,
- IconPlus,
- IconX,
- IconTrash,
- Dropdown,
- IconColumns,
- IconChevronDown,
-} from 'ui'
import { saveAs } from 'file-saver'
+import { FC, useState, ReactNode } from 'react'
+import { Button, IconDownload, IconX, IconTrash, Dropdown, IconChevronDown } from 'ui'
+import { PermissionAction } from '@supabase/shared-types/out/constants'
-import { useStore } from 'hooks'
+import { checkPermissions, useStore } from 'hooks'
import FilterDropdown from './filter'
import SortPopover from './sort'
import RefreshButton from './RefreshButton'
@@ -80,6 +72,9 @@ const DefaultHeader: FC = ({
}) => {
const canAddNew = onAddRow !== undefined || onAddColumn !== undefined
+ // [Joshen] Using this logic to block both column and row creation/update/delete
+ const canCreateColumns = checkPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'columns')
+
return (
@@ -91,71 +86,73 @@ const DefaultHeader: FC
= ({
<>
-
-
-
-
+ {canCreateColumns && (
+
+
+
+
+
+ }
+ >
+
+
Insert row
+
Insert a new row into {table.name}
- }
- >
-
-
Insert row
-
Insert a new row into {table.name}
-
- ,
- ]
- : []),
- ...(onAddColumn !== undefined
- ? [
-
-
-
-
+ ,
+ ]
+ : []),
+ ...(onAddColumn !== undefined
+ ? [
+
+
+
+
+
+ }
+ >
+
+
Insert column
+
Insert a new column into {table.name}
- }
- >
-
-
Insert column
-
Insert a new column into {table.name}
-
- ,
- ]
- : []),
- ]}
- >
-
}>
- Insert
-
-
+ ,
+ ]
+ : []),
+ ]}
+ >
+
}>
+ Insert
+
+
+ )}
>
)}
diff --git a/studio/components/grid/components/menu/ColumnMenu.tsx b/studio/components/grid/components/menu/ColumnMenu.tsx
index d252aa58289..27d5f59b57a 100644
--- a/studio/components/grid/components/menu/ColumnMenu.tsx
+++ b/studio/components/grid/components/menu/ColumnMenu.tsx
@@ -10,7 +10,6 @@ import {
IconLock,
IconUnlock,
} from 'ui'
-import * as React from 'react'
import { useDispatch, useTrackedState } from '../../store'
type ColumnMenuProps = {
diff --git a/studio/components/interfaces/Auth/EmailTemplates/EmailTemplates.tsx b/studio/components/interfaces/Auth/EmailTemplates/EmailTemplates.tsx
index a982880a845..85462a7e4be 100644
--- a/studio/components/interfaces/Auth/EmailTemplates/EmailTemplates.tsx
+++ b/studio/components/interfaces/Auth/EmailTemplates/EmailTemplates.tsx
@@ -8,19 +8,18 @@ import TemplateEditor from './TemplateEditor'
const EmailTemplates = observer(() => {
return (
-
+
= ({}) => {
icon={ }
primaryText="Free Plan does not include project backups."
projectRef={projectRef}
- secondaryText="Please upgrade to the Pro plan for up to 7 days of scheduled backups."
+ secondaryText="Upgrade to the Pro plan for up to 7 days of scheduled backups."
/>
)
}
diff --git a/studio/components/interfaces/Database/Backups/PITR/PITRNotice.tsx b/studio/components/interfaces/Database/Backups/PITR/PITRNotice.tsx
index cd43b19d452..daab4106406 100644
--- a/studio/components/interfaces/Database/Backups/PITR/PITRNotice.tsx
+++ b/studio/components/interfaces/Database/Backups/PITR/PITRNotice.tsx
@@ -1,15 +1,22 @@
import Link from 'next/link'
import { Button, IconCalendar } from 'ui'
import { FormPanel } from 'components/ui/Forms'
-import { useParams } from 'hooks'
+import * as Tooltip from '@radix-ui/react-tooltip'
+import { checkPermissions, useParams } from 'hooks'
import { useProjectSubscriptionQuery } from 'data/subscriptions/project-subscription-query'
import { getPITRRetentionDuration } from './PITR.utils'
+import { PermissionAction } from '@supabase/shared-types/out/constants'
const PITRNotice = ({}) => {
const { ref: projectRef } = useParams()
const { data: subscription } = useProjectSubscriptionQuery({ projectRef })
const retentionPeriod = getPITRRetentionDuration(subscription?.addons ?? [])
+ const canUpdateSubscription = checkPermissions(
+ PermissionAction.BILLING_WRITE,
+ 'stripe.subscriptions'
+ )
+
return (
{
You can also increase your recovery retention period updating your PITR add-on
-
-
-
- Increase retention period
+
+
+
+
+ Increase retention period
+
-
-
+
+ {!canUpdateSubscription && (
+
+
+
+
+ You need additional permissions to amend subscriptions
+
+
+
+ )}
+
}
>
diff --git a/studio/components/interfaces/Database/Backups/PITR/PITRStatus.tsx b/studio/components/interfaces/Database/Backups/PITR/PITRStatus.tsx
index 4b47575c1d4..b634e1365c7 100644
--- a/studio/components/interfaces/Database/Backups/PITR/PITRStatus.tsx
+++ b/studio/components/interfaces/Database/Backups/PITR/PITRStatus.tsx
@@ -2,11 +2,13 @@ import dayjs from 'dayjs'
import { FC } from 'react'
import { observer } from 'mobx-react-lite'
import { Button, IconAlertCircle } from 'ui'
+import * as Tooltip from '@radix-ui/react-tooltip'
-import { useStore } from 'hooks'
+import { checkPermissions, useStore } from 'hooks'
import { Timezone } from './PITR.types'
import { FormPanel } from 'components/ui/Forms'
import TimezoneSelection from './TimezoneSelection'
+import { PermissionAction } from '@supabase/shared-types/out/constants'
interface Props {
selectedTimezone: Timezone
@@ -29,6 +31,11 @@ const PITRStatus: FC
= ({ selectedTimezone, onUpdateTimezone, onSetConfig
.tz(selectedTimezone?.utc[0])
.format('DD MMM YYYY, HH:mm:ss')
+ const canTriggerPhysicalBackup = checkPermissions(
+ PermissionAction.INFRA_EXECUTE,
+ 'queue_job.walg.prepare_restore'
+ )
+
return (
<>
= ({ selectedTimezone, onUpdateTimezone, onSetConfig
You'll be able to pick the right date and time when you begin
- onSetConfiguration()}>Start a restore
+
+
+ onSetConfiguration()}>
+ Start a restore
+
+
+ {!canTriggerPhysicalBackup && (
+
+
+
+
+ You need additional permissions to trigger a PITR recovery
+
+
+
+ )}
+
}
>
diff --git a/studio/components/interfaces/Database/Functions/FunctionsList/FunctionsList.tsx b/studio/components/interfaces/Database/Functions/FunctionsList/FunctionsList.tsx
index 46458d9a75d..df69111db50 100644
--- a/studio/components/interfaces/Database/Functions/FunctionsList/FunctionsList.tsx
+++ b/studio/components/interfaces/Database/Functions/FunctionsList/FunctionsList.tsx
@@ -60,6 +60,8 @@ const FunctionsList: FC = ({
title="Functions"
ctaButtonLabel="Create a new function"
onClickCta={() => createFunction()}
+ disabled={!canCreateFunctions}
+ disabledMessage="You need additional permissions to create functions"
>
diff --git a/studio/components/interfaces/Database/Roles/RolesList.tsx b/studio/components/interfaces/Database/Roles/RolesList.tsx
index 956fb1af750..c5059c26e05 100644
--- a/studio/components/interfaces/Database/Roles/RolesList.tsx
+++ b/studio/components/interfaces/Database/Roles/RolesList.tsx
@@ -1,17 +1,18 @@
+import { partition } from 'lodash'
import { FC, useState, useEffect } from 'react'
import { observer } from 'mobx-react-lite'
import * as Tooltip from '@radix-ui/react-tooltip'
import { PostgresRole } from '@supabase/postgres-meta'
import { Button, Input, IconPlus, IconSearch, IconX, Badge } from 'ui'
+import { PermissionAction } from '@supabase/shared-types/out/constants'
-import { useStore } from 'hooks'
+import { checkPermissions, useStore } from 'hooks'
import SparkBar from 'components/ui/SparkBar'
import { FormHeader } from 'components/ui/Forms'
import RoleRow from './RoleRow'
-import DeleteRoleModal from './DeleteRoleModal'
import { SUPABASE_ROLES } from './Roles.constants'
-import { partition } from 'lodash'
import CreateRolePanel from './CreateRolePanel'
+import DeleteRoleModal from './DeleteRoleModal'
interface Props {
onSelectRole: (role: any) => void
@@ -26,6 +27,8 @@ const RolesList: FC = ({}) => {
const [isCreatingRole, setIsCreatingRole] = useState(false)
const [selectedRoleToDelete, setSelectedRoleToDelete] = useState()
+ const canUpdateRoles = checkPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'roles')
+
useEffect(() => {
const getMaxConnectionLimit = async () => {
const res = await meta.query('show max_connections')
@@ -149,13 +152,31 @@ const RolesList: FC = ({}) => {
- }
- onClick={() => setIsCreatingRole(true)}
- >
- Add role
-
+
+
+ }
+ onClick={() => setIsCreatingRole(true)}
+ >
+ Add role
+
+
+ {!canUpdateRoles && (
+
+
+
+ You need additional permissions to add a new role
+
+
+ )}
+
@@ -184,7 +205,12 @@ const RolesList: FC = ({}) => {
)}
{otherRoles.map((role: PostgresRole, i: number) => (
-
+
))}
diff --git a/studio/components/interfaces/Database/Tables/ColumnList.tsx b/studio/components/interfaces/Database/Tables/ColumnList.tsx
index c5b668777fc..e53ed442863 100644
--- a/studio/components/interfaces/Database/Tables/ColumnList.tsx
+++ b/studio/components/interfaces/Database/Tables/ColumnList.tsx
@@ -1,11 +1,13 @@
import { FC, useState } from 'react'
import { observer } from 'mobx-react-lite'
+import * as Tooltip from '@radix-ui/react-tooltip'
import { Input, Button, IconSearch, IconPlus, IconChevronLeft, IconEdit3, IconTrash } from 'ui'
+import type { PostgresTable } from '@supabase/postgres-meta'
+import { PermissionAction } from '@supabase/shared-types/out/constants'
-import { useStore } from 'hooks'
+import { useStore, checkPermissions } from 'hooks'
import Table from 'components/to-be-cleaned/Table'
import NoSearchResults from 'components/to-be-cleaned/NoSearchResults'
-import type { PostgresTable } from '@supabase/postgres-meta'
interface Props {
selectedTable: PostgresTable
@@ -30,6 +32,7 @@ const ColumnList: FC = ({
: selectedTable.columns?.filter((column: any) => column.name.includes(filterString))) ?? []
const isLocked = meta.excludedSchemas.includes(selectedTable.schema ?? '')
+ const canUpdateColumns = checkPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'columns')
return (
<>
@@ -58,9 +61,32 @@ const ColumnList: FC = ({
{!isLocked && (
-
} onClick={() => onAddColumn()}>
- New column
-
+
+
+ }
+ onClick={() => onAddColumn()}
+ >
+ New column
+
+
+ {!canUpdateColumns && (
+
+
+
+
+ You need additional permissions to create columns
+
+
+
+ )}
+
)}
@@ -103,20 +129,59 @@ const ColumnList: FC = ({
-
onEditColumn(x)}
- icon={ }
- style={{ padding: 5 }}
- type="text"
- disabled={isLocked}
- />
- onDeleteColumn(x)}
- icon={ }
- style={{ padding: 5 }}
- type="text"
- disabled={isLocked}
- />
+
+
+ onEditColumn(x)}
+ icon={ }
+ style={{ padding: 5 }}
+ type="text"
+ disabled={!canUpdateColumns || isLocked}
+ />
+
+ {!canUpdateColumns && (
+
+
+
+
+ You need additional permissions to edit columns
+
+
+
+ )}
+
+
+
+
+ onDeleteColumn(x)}
+ icon={ }
+ style={{ padding: 5 }}
+ type="text"
+ disabled={!canUpdateColumns || isLocked}
+ />
+
+ {!canUpdateColumns && (
+
+
+
+
+ You need additional permissions to delete columns
+
+
+
+ )}
+
diff --git a/studio/components/interfaces/Database/Tables/TableList.tsx b/studio/components/interfaces/Database/Tables/TableList.tsx
index de856582937..b058f9ce6f4 100644
--- a/studio/components/interfaces/Database/Tables/TableList.tsx
+++ b/studio/components/interfaces/Database/Tables/TableList.tsx
@@ -12,10 +12,11 @@ import {
IconLock,
IconCheck,
} from 'ui'
-
import { partition } from 'lodash'
+import * as Tooltip from '@radix-ui/react-tooltip'
+import { PermissionAction } from '@supabase/shared-types/out/constants'
-import { useStore } from 'hooks'
+import { useStore, checkPermissions } from 'hooks'
import Table from 'components/to-be-cleaned/Table'
import NoSearchResults from 'components/to-be-cleaned/NoSearchResults'
import type { PostgresTable, PostgresSchema } from '@supabase/postgres-meta'
@@ -38,8 +39,8 @@ const TableList: FC = ({
onOpenTable = () => {},
}) => {
const { meta } = useStore()
-
const [filterString, setFilterString] = useState('')
+ const canUpdateTables = checkPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'tables')
const schemas: PostgresSchema[] = meta.schemas.list()
const [protectedSchemas, openSchemas] = partition(schemas, (schema) =>
@@ -120,9 +121,32 @@ const TableList: FC = ({
{!isLocked && (
-
} onClick={() => onAddTable()}>
- New table
-
+
+
+ }
+ onClick={() => onAddTable()}
+ >
+ New table
+
+
+ {!canUpdateTables && (
+
+
+
+
+ You need additional permissions to create tables
+
+
+
+ )}
+
)}
@@ -183,7 +207,62 @@ const TableList: FC = ({
>
{x.columns.length} columns
-
+
+ }
+ style={{ padding: 5 }}
+ disabled={!canUpdateTables || isLocked}
+ onClick={() => onEditTable(x)}
+ />
+
+ {!canUpdateTables && (
+
+
+
+
+ You need additional permissions to edit tables
+
+
+
+ )}
+
+
+
+
+ }
+ style={{ padding: 5 }}
+ disabled={!canUpdateTables || isLocked}
+ onClick={() => onDeleteTable(x)}
+ />
+
+ {!canUpdateTables && (
+
+
+
+
+ You need additional permissions to delete tables
+
+
+
+ )}
+
+
+ {/* }
style={{ padding: 5 }}
@@ -196,7 +275,7 @@ const TableList: FC = ({
style={{ padding: 5 }}
disabled={isLocked}
onClick={() => onDeleteTable(x)}
- />
+ /> */}
diff --git a/studio/components/interfaces/Database/Wrappers/CreateWrapper.tsx b/studio/components/interfaces/Database/Wrappers/CreateWrapper.tsx
index dbe8fe10c83..242345a01f7 100644
--- a/studio/components/interfaces/Database/Wrappers/CreateWrapper.tsx
+++ b/studio/components/interfaces/Database/Wrappers/CreateWrapper.tsx
@@ -36,7 +36,7 @@ const CreateWrapper = () => {
const [selectedTableToEdit, setSelectedTableToEdit] = useState()
const [formErrors, setFormErrors] = useState<{ [k: string]: string }>({})
- const canCreateWrapper = checkPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'extensions')
+ const canCreateWrapper = checkPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'wrappers')
const wrapperMeta = WRAPPERS.find((wrapper) => wrapper.name === type)
const initialValues =
@@ -164,6 +164,7 @@ const CreateWrapper = () => {
isSubmitting={isSubmitting}
hasChanges={hasChanges}
handleReset={handleReset}
+ disabled={!canCreateWrapper}
helper={
!canCreateWrapper
? 'You need additional permissions to create a foreign data wrapper'
diff --git a/studio/components/interfaces/Database/Wrappers/EditWrapper.tsx b/studio/components/interfaces/Database/Wrappers/EditWrapper.tsx
index 2286ec04c7f..fdcec1aa4b8 100644
--- a/studio/components/interfaces/Database/Wrappers/EditWrapper.tsx
+++ b/studio/components/interfaces/Database/Wrappers/EditWrapper.tsx
@@ -65,7 +65,7 @@ const EditWrapper = () => {
const [selectedTableToEdit, setSelectedTableToEdit] = useState()
const [formErrors, setFormErrors] = useState<{ [k: string]: string }>({})
- const canCreateWrapper = checkPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'extensions')
+ const canUpdateWrapper = checkPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'wrappers')
const initialValues =
wrapperMeta !== undefined
@@ -248,7 +248,7 @@ const EditWrapper = () => {
return (
{
handleReset()
setWrapperTables(initialTables)
}}
+ disabled={!canUpdateWrapper}
helper={
- !canCreateWrapper
- ? 'You need additional permissions to create a foreign data wrapper'
+ !canUpdateWrapper
+ ? 'You need additional permissions to edit a foreign data wrapper'
: undefined
}
/>
diff --git a/studio/components/interfaces/Database/Wrappers/WrapperRow.tsx b/studio/components/interfaces/Database/Wrappers/WrapperRow.tsx
index 35b7efbed34..896cd11d45e 100644
--- a/studio/components/interfaces/Database/Wrappers/WrapperRow.tsx
+++ b/studio/components/interfaces/Database/Wrappers/WrapperRow.tsx
@@ -1,10 +1,12 @@
import Link from 'next/link'
import Image from 'next/image'
import { FC } from 'react'
-import { Collapsible, IconChevronUp, Button, IconExternalLink, IconTrash, IconEdit } from 'ui'
import { partition } from 'lodash'
+import * as Tooltip from '@radix-ui/react-tooltip'
+import { PermissionAction } from '@supabase/shared-types/out/constants'
+import { Collapsible, IconChevronUp, Button, IconExternalLink, IconTrash, IconEdit } from 'ui'
-import { useParams, useStore } from 'hooks'
+import { useParams, useStore, checkPermissions } from 'hooks'
import { WrapperMeta } from './Wrappers.types'
import { FDW } from 'data/fdw/fdws-query'
import { useFDWDeleteMutation } from 'data/fdw/fdw-delete-mutation'
@@ -24,6 +26,8 @@ const WrapperRow: FC = ({ wrappers = [], wrapperMeta, isOpen, onOpen }) =
const { project } = useProjectContext()
const { mutateAsync: deleteFDW } = useFDWDeleteMutation()
+ const canManageWrappers = checkPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'wrappers')
+
const onDeleteWrapper = (wrapper: any) => {
confirmAlert({
title: `Confirm to disable ${wrapper.name}`,
@@ -156,21 +160,69 @@ const WrapperRow: FC = ({ wrappers = [], wrapperMeta, isOpen, onOpen }) =
-
-
+ {canManageWrappers ? (
+
+
+ }
+ className="py-2"
+ />
+
+
+ ) : (
+
+
+ }
+ className="py-2"
+ />
+
+ {!canManageWrappers && (
+
+
+
+
+ You need additional permissions to edit wrappers
+
+
+
+ )}
+
+ )}
+
+
}
+ disabled={!canManageWrappers}
+ icon={ }
className="py-2"
+ onClick={() => onDeleteWrapper(wrapper)}
/>
-
-
- }
- className="py-2"
- onClick={() => onDeleteWrapper(wrapper)}
- />
+
+ {!canManageWrappers && (
+
+
+
+
+ You need additional permissions to add wrappers
+
+
+
+ )}
+
)
diff --git a/studio/components/interfaces/Database/Wrappers/Wrappers.tsx b/studio/components/interfaces/Database/Wrappers/Wrappers.tsx
index 461d9776b17..529d9cd0c16 100644
--- a/studio/components/interfaces/Database/Wrappers/Wrappers.tsx
+++ b/studio/components/interfaces/Database/Wrappers/Wrappers.tsx
@@ -1,4 +1,5 @@
import Link from 'next/link'
+import { groupBy } from 'lodash'
import { useState } from 'react'
import { Button, IconExternalLink } from 'ui'
import { observer } from 'mobx-react-lite'
@@ -11,7 +12,6 @@ import ShimmeringLoader from 'components/ui/ShimmeringLoader'
import WrappersDropdown from './WrappersDropdown'
import WrappersDisabledState from './WrappersDisabledState'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
-import { groupBy } from 'lodash'
const Wrappers = () => {
const { meta } = useStore()
diff --git a/studio/components/interfaces/Database/Wrappers/WrappersDropdown.tsx b/studio/components/interfaces/Database/Wrappers/WrappersDropdown.tsx
index b74c9a9fffb..76a5ce5b6b4 100644
--- a/studio/components/interfaces/Database/Wrappers/WrappersDropdown.tsx
+++ b/studio/components/interfaces/Database/Wrappers/WrappersDropdown.tsx
@@ -3,8 +3,10 @@ import Image from 'next/image'
import { FC, Fragment } from 'react'
import { observer } from 'mobx-react-lite'
import { Button, Dropdown, IconPlus } from 'ui'
+import * as Tooltip from '@radix-ui/react-tooltip'
+import { PermissionAction } from '@supabase/shared-types/out/constants'
-import { useParams } from 'hooks'
+import { useParams, checkPermissions } from 'hooks'
import { WRAPPERS } from './Wrappers.constants'
interface Props {
@@ -14,6 +16,32 @@ interface Props {
const WrapperDropdown: FC = ({ buttonText = 'Add wrapper', align = 'end' }) => {
const { ref } = useParams()
+ const canManageWrappers = checkPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'wrappers')
+
+ if (!canManageWrappers) {
+ return (
+
+
+ }>
+ {buttonText}
+
+
+
+
+
+
+ You need additional permissions to add wrappers
+
+
+
+
+ )
+ }
return (
= () => {
+ const router = useRouter()
+ const { functions, ui } = useStore()
+ const { ref: projectRef, id } = useParams()
+ const [isCopied, setIsCopied] = useState(false)
+ const [showDeleteModal, setShowDeleteModal] = useState(false)
+ const [showInstructions, setShowInstructions] = useState(false)
+ const [selectedFunction, setSelectedFunction] = useState(null)
+
+ const canDeleteEdgeFunction = checkPermissions(PermissionAction.FUNCTIONS_WRITE, '*')
+ const { mutateAsync: deleteEdgeFunction, isLoading: isDeleting } = useEdgeFunctionDeleteMutation()
+
+ useEffect(() => {
+ setSelectedFunction(functions.byId(id))
+ }, [functions.isLoaded, ui.selectedProject])
+
+ const { data: settings } = useProjectApiQuery({ projectRef })
+
+ // Get the API service
+ const apiService = settings?.autoApiService
+ const anonKey = apiService?.service_api_keys.find((x) => x.name === 'anon key')
+ ? apiService.defaultApiKey
+ : undefined
+
+ const endpoint = apiService?.app_config.endpoint ?? ''
+ const endpointSections = endpoint.split('.')
+ const functionsEndpoint = [
+ ...endpointSections.slice(0, 1),
+ 'functions',
+ ...endpointSections.slice(1),
+ ].join('.')
+ const functionUrl = `${apiService?.protocol}://${functionsEndpoint}/${selectedFunction?.slug}`
+
+ const onConfirmDelete = async () => {
+ if (!projectRef) return console.error('Project ref is required')
+ if (selectedFunction === undefined) return console.error('No edge function selected')
+
+ try {
+ await deleteEdgeFunction({ projectRef, slug: selectedFunction.slug })
+ ui.setNotification({
+ category: 'success',
+ message: `Successfully deleted "${selectedFunction.name}"`,
+ })
+ router.push(`/project/${projectRef}/functions`)
+ } catch (error: any) {
+ ui.setNotification({
+ category: 'error',
+ message: `Failed to delete function: ${error.message}`,
+ })
+ }
+ }
+
+ const managementCommands: any = [
+ {
+ command: `supabase functions deploy ${selectedFunction?.slug}`,
+ description: 'This will overwrite the deployed function with your new function',
+ jsx: () => {
+ return (
+ <>
+ supabase functions deploy{' '}
+ {selectedFunction?.slug}
+ >
+ )
+ },
+ comment: 'Deploy a new version',
+ },
+ {
+ command: `supabase functions delete ${selectedFunction?.slug}`,
+ description: 'This will remove the function and all the logs associated with it',
+ jsx: () => {
+ return (
+ <>
+ supabase functions delete{' '}
+ {selectedFunction?.slug}
+ >
+ )
+ },
+ comment: 'Delete the function',
+ },
+ ]
+
+ const secretCommands: any = [
+ {
+ command: `supabase secrets list`,
+ description: 'This will list all the secrets for your project',
+ jsx: () => {
+ return (
+ <>
+ supabase secrets list
+ >
+ )
+ },
+ comment: 'View all secrets',
+ },
+ {
+ command: `supabase secrets set NAME1=VALUE1 NAME2=VALUE2`,
+ description: 'This will set secrets for your project',
+ jsx: () => {
+ return (
+ <>
+ supabase secrets set NAME1=VALUE1 NAME2=VALUE2
+ >
+ )
+ },
+ comment: 'Set secrets for your project',
+ },
+ {
+ command: `supabase secrets unset NAME1 NAME2 `,
+ description: 'This will delete secrets for your project',
+ jsx: () => {
+ return (
+ <>
+ supabase secrets unset NAME1 NAME2
+ >
+ )
+ },
+ comment: 'Unset secrets for your project',
+ },
+ ]
+
+ const invokeCommands: any = [
+ {
+ command: `curl -L -X POST '${functionUrl}' -H 'Authorization: Bearer ${
+ anonKey ?? '[YOUR ANON KEY]'
+ }' --data '{"name":"Functions"}'`,
+ description: 'Invokes the hello function',
+ jsx: () => {
+ return (
+ <>
+ curl -L -X POST 'https://{functionsEndpoint}/
+ {selectedFunction?.slug}' -H 'Authorization: Bearer [YOUR ANON KEY]'{' '}
+ {`--data '{"name":"Functions"}'`}
+ >
+ )
+ },
+ comment: 'Invoke your function',
+ },
+ ]
+
+ return (
+ <>
+
+
+
+
Function Name
+
{selectedFunction?.name}
+
+
+
+
Endpoint URL
+
{functionUrl}
+
{
+ function onCopy(value: any) {
+ setIsCopied(true)
+ navigator.clipboard.writeText(value).then()
+ setTimeout(function () {
+ setIsCopied(false)
+ }, 3000)
+ }
+ event.stopPropagation()
+ onCopy(functionUrl)
+ }}
+ >
+ {isCopied ? (
+
+
+
+ ) : (
+
+ )}
+
+
+
+
+
Created At
+
+ {selectedFunction?.created_at &&
+ dayjs(selectedFunction.created_at).format('dddd, MMMM D, YYYY h:mm A')}
+
+
+
+
+
Last Updated At
+
+ {selectedFunction?.updated_at &&
+ dayjs(selectedFunction.updated_at).format('dddd, MMMM D, YYYY h:mm A')}
+
+
+
+
+
Deployments
+
{selectedFunction?.version}
+
+
+
+
Regions
+
+
+
+ Earth
+
+
All functions are deployed globally
+
+
+
+
+
+
+
+
+
+
+
Command line access
+
+
setShowInstructions(!showInstructions)}>
+ {showInstructions ? (
+
+ ) : (
+
+ )}
+
+
+
+
Deployment management
+
+
Invoke
+
+
Secrets management
+
+
+
+
Delete Edge Function}>
+
+
+
+ Make sure you have made a backup if you want to restore your edge function
+
+
+
+ setShowDeleteModal(true)}
+ >
+ Delete edge function
+
+
+ {!canDeleteEdgeFunction && (
+
+
+
+
+ You need additional permissions to delete an edge function
+
+
+
+ )}
+
+
+
+
+
+
+ Confirm to delete {selectedFunction?.name}}
+ visible={showDeleteModal}
+ loading={isDeleting}
+ onCancel={() => setShowDeleteModal(false)}
+ onConfirm={onConfirmDelete}
+ >
+
+
+
+ Ensure that you have made a backup if you want to restore your edge function
+
+
+
+
+ >
+ )
+}
+
+export default EdgeFunctionDetails
diff --git a/studio/components/interfaces/Functions/EdgeFunctionsListItem.tsx b/studio/components/interfaces/Functions/EdgeFunctionsListItem.tsx
new file mode 100644
index 00000000000..0d9ce1f727b
--- /dev/null
+++ b/studio/components/interfaces/Functions/EdgeFunctionsListItem.tsx
@@ -0,0 +1,103 @@
+import dayjs from 'dayjs'
+import { FC, useState } from 'react'
+import { useRouter } from 'next/router'
+import { observer } from 'mobx-react-lite'
+import { IconCheck, IconClipboard } from 'ui'
+import * as Tooltip from '@radix-ui/react-tooltip'
+
+import { useParams, useStore } from 'hooks'
+import Table from 'components/to-be-cleaned/Table'
+import { EdgeFunctionsResponse } from 'data/edge-functions/edge-functions-query'
+
+interface Props {
+ function: EdgeFunctionsResponse
+}
+
+const EdgeFunctionsListItem: FC = ({ function: item }) => {
+ const router = useRouter()
+ const { ui } = useStore()
+ const { ref } = useParams()
+ const [isCopied, setIsCopied] = useState(false)
+
+ // get the .co or .net TLD from the restUrl
+ const restUrl = ui.selectedProject?.restUrl
+ const restUrlTld = new URL(restUrl as string).hostname.split('.').pop()
+ const functionUrl = `https://${ref}.functions.supabase.${restUrlTld}/${item.slug}`
+
+ return (
+ {
+ router.push(`/project/${ref}/functions/${item.id}/details`)
+ }}
+ >
+
+
+
+
+
+
{functionUrl}
+
{
+ function onCopy(value: any) {
+ setIsCopied(true)
+ navigator.clipboard.writeText(value).then()
+ setTimeout(function () {
+ setIsCopied(false)
+ }, 3000)
+ }
+ event.stopPropagation()
+ onCopy(functionUrl)
+ }}
+ >
+ {isCopied ? (
+
+
+
+ ) : (
+
+ )}
+
+
+
+
+ {dayjs(item.created_at).format('DD MMM, YYYY HH:mm')}
+
+
+
+
+
+
{dayjs(item.updated_at).fromNow()}
+
+
+
+
+
+
+ Last updated on {dayjs(item.updated_at).format('DD MMM, YYYY HH:mm')}
+
+
+
+
+
+
+ {item.version}
+
+
+ )
+}
+
+export default observer(EdgeFunctionsListItem)
diff --git a/studio/components/interfaces/Functions/FunctionsListItem.tsx b/studio/components/interfaces/Functions/FunctionsListItem.tsx
deleted file mode 100644
index f2b9045d417..00000000000
--- a/studio/components/interfaces/Functions/FunctionsListItem.tsx
+++ /dev/null
@@ -1,62 +0,0 @@
-import dayjs from 'dayjs'
-import { FC } from 'react'
-import { useRouter } from 'next/router'
-import { observer } from 'mobx-react-lite'
-
-import { useStore } from 'hooks'
-import Table from 'components/to-be-cleaned/Table'
-
-interface Props {
- function: any
-}
-
-const FunctionsListItem: FC = ({ function: item }) => {
- const { ui } = useStore()
- const is_functionConfirmed = item.email_confirmed_at || item.phone_confirmed_at
-
- const router = useRouter()
- const ref = ui?.selectedProject?.ref
-
- // get the .co or .net TLD from the restUrl
- const restUrl = ui.selectedProject?.restUrl
- const restUrlTld = new URL(restUrl as string).hostname.split('.').pop()
-
- return (
- {
- router.push(`/project/${ref}/functions/${item.id}`)
- }}
- >
-
-
- {item.name}
-
-
-
-
- {`https://${ref}.functions.supabase.${restUrlTld}/${item.slug}`}
- {`/${item.name}`}
-
-
-
-
- {dayjs(item.created_at).format('DD MMM, YYYY HH:mm')}
-
-
-
-
- {dayjs(item.updated_at).format('DD MMM, YYYY HH:mm')}
-
-
-
- v{item.version}
-
-
- {item.status}
-
-
- )
-}
-
-export default observer(FunctionsListItem)
diff --git a/studio/components/interfaces/Functions/FunctionsNav.tsx b/studio/components/interfaces/Functions/FunctionsNav.tsx
index dfe6bd5ab13..af24e28d29f 100644
--- a/studio/components/interfaces/Functions/FunctionsNav.tsx
+++ b/studio/components/interfaces/Functions/FunctionsNav.tsx
@@ -1,8 +1,7 @@
import { Tabs } from 'ui'
import { useRouter } from 'next/router'
-// @ts-ignore
-const FunctionsNav = ({ item }) => {
+const FunctionsNav = ({ item }: any) => {
const router = useRouter()
const activeRoute = router.pathname.split('/')[5]
const { ref } = router.query
@@ -17,9 +16,8 @@ const FunctionsNav = ({ item }) => {
router.push(`/project/${ref}/functions/${item.id}/${e === 'metrics' ? '' : e}`)
}
>
-
- {/* */}
+
diff --git a/studio/components/interfaces/Functions/TerminalInstructions.tsx b/studio/components/interfaces/Functions/TerminalInstructions.tsx
index f8f30b523ab..16a2e2eea21 100644
--- a/studio/components/interfaces/Functions/TerminalInstructions.tsx
+++ b/studio/components/interfaces/Functions/TerminalInstructions.tsx
@@ -10,19 +10,16 @@ import { Commands } from './Functions.types'
interface Props {
closable?: boolean
+ removeBorder?: boolean
}
-const TerminalInstructions: FC = ({ closable = false }) => {
+const TerminalInstructions: FC = ({ closable = false, removeBorder = false }) => {
const router = useRouter()
const { ref: projectRef } = useParams()
-
const [showInstructions, setShowInstructions] = useState(!closable)
-
const { data: tokens } = useAccessTokensQuery()
+ const { data: settings } = useProjectApiQuery({ projectRef })
- const { data: settings } = useProjectApiQuery({
- projectRef,
- })
const apiService = settings?.autoApiService
const anonKey = apiService?.service_api_keys.find((x) => x.name === 'anon key')
? apiService.defaultApiKey
@@ -86,7 +83,9 @@ const TerminalInstructions: FC = ({ closable = false }) => {
return (
@@ -107,7 +106,7 @@ const TerminalInstructions: FC
= ({ closable = false }) => {
)}
-
diff --git a/studio/components/interfaces/Functions/index.ts b/studio/components/interfaces/Functions/index.ts
index b096ff5e604..003d5feaca6 100644
--- a/studio/components/interfaces/Functions/index.ts
+++ b/studio/components/interfaces/Functions/index.ts
@@ -1,5 +1,6 @@
-import FunctionsListItem from './FunctionsListItem'
+import EdgeFunctionDetails from './EdgeFunctionDetails'
+import EdgeFunctionsListItem from './EdgeFunctionsListItem'
import FunctionsEmptyState from './FunctionsEmptyState'
import TerminalInstructions from './TerminalInstructions'
-export { FunctionsListItem, FunctionsEmptyState, TerminalInstructions }
+export { EdgeFunctionDetails, EdgeFunctionsListItem, FunctionsEmptyState, TerminalInstructions }
diff --git a/studio/components/interfaces/GraphQL/GraphiQL.tsx b/studio/components/interfaces/GraphQL/GraphiQL.tsx
new file mode 100644
index 00000000000..4780cf40d25
--- /dev/null
+++ b/studio/components/interfaces/GraphQL/GraphiQL.tsx
@@ -0,0 +1,583 @@
+/* Based on https://github.com/graphql/graphiql/blob/main/packages/graphiql/src/components/GraphiQL.tsx */
+
+import {
+ Button,
+ ChevronDownIcon,
+ ChevronUpIcon,
+ CopyIcon,
+ Dialog,
+ ExecuteButton,
+ GraphiQLProvider,
+ HeaderEditor,
+ KeyboardShortcutIcon,
+ MergeIcon,
+ PlusIcon,
+ PrettifyIcon,
+ QueryEditor,
+ ReloadIcon,
+ ResponseEditor,
+ SettingsIcon,
+ Spinner,
+ Tab,
+ Tabs,
+ ToolbarButton,
+ Tooltip,
+ UnStyledButton,
+ useCopyQuery,
+ useDragResize,
+ useEditorContext,
+ useExecutionContext,
+ useMergeQuery,
+ usePluginContext,
+ usePrettifyEditors,
+ useSchemaContext,
+ useStorageContext,
+ useTheme,
+ VariableEditor,
+} from '@graphiql/react'
+import { Fetcher } from '@graphiql/toolkit'
+import clsx from 'clsx'
+import 'graphiql/graphiql.css'
+import { PropsWithChildren, useEffect, useState } from 'react'
+import styles from './graphiql.module.css'
+
+export type GraphiQLProps = {
+ fetcher: Fetcher
+ theme?: 'dark' | 'light'
+ apiKey?: string
+}
+
+const GraphiQL = ({ fetcher, theme = 'dark', apiKey }: GraphiQLProps) => {
+ // Ensure props are correct
+ if (typeof fetcher !== 'function') {
+ throw new TypeError(
+ 'The `GraphiQL` component requires a `fetcher` function to be passed as prop.'
+ )
+ }
+
+ return (
+
+
+
+ )
+}
+
+type GraphiQLInterfaceProps = {
+ theme: 'dark' | 'light'
+}
+
+export const GraphiQLInterface = ({ theme }: GraphiQLInterfaceProps) => {
+ const editorContext = useEditorContext({ nonNull: true })
+ const executionContext = useExecutionContext({ nonNull: true })
+ const schemaContext = useSchemaContext({ nonNull: true })
+ const storageContext = useStorageContext()
+ const pluginContext = usePluginContext()
+
+ const copy = useCopyQuery()
+ const merge = useMergeQuery()
+ const prettify = usePrettifyEditors()
+
+ const { setTheme } = useTheme()
+ useEffect(() => {
+ setTheme(theme)
+ }, [theme])
+
+ const PluginContent = pluginContext?.visiblePlugin?.content
+
+ const pluginResize = useDragResize({
+ defaultSizeRelation: 1 / 3,
+ direction: 'horizontal',
+ initiallyHidden: pluginContext?.visiblePlugin ? undefined : 'second',
+ onHiddenElementChange: (resizableElement) => {
+ if (resizableElement === 'first') {
+ pluginContext?.setVisiblePlugin(null)
+ }
+ },
+ sizeThresholdSecond: 200,
+ storageKey: 'docExplorerFlex',
+ })
+ const editorResize = useDragResize({
+ direction: 'horizontal',
+ storageKey: 'editorFlex',
+ })
+ const editorToolsResize = useDragResize({
+ defaultSizeRelation: 3,
+ direction: 'vertical',
+ initiallyHidden: (() => {
+ return editorContext.initialVariables || editorContext.initialHeaders ? undefined : 'second'
+ })(),
+ sizeThresholdSecond: 60,
+ storageKey: 'secondaryEditorFlex',
+ })
+
+ const [activeSecondaryEditor, setActiveSecondaryEditor] = useState<'variables' | 'headers'>(
+ () => {
+ return !editorContext.initialVariables && editorContext.initialHeaders
+ ? 'headers'
+ : 'variables'
+ }
+ )
+ const [showDialog, setShowDialog] = useState<'settings' | 'short-keys' | null>(null)
+ const [clearStorageStatus, setClearStorageStatus] = useState<'success' | 'error' | null>(null)
+
+ const logo =
+
+ const toolbar = (
+ <>
+ prettify()} label="Prettify query (Shift-Ctrl-P)">
+
+
+ merge()} label="Merge fragments into query (Shift-Ctrl-M)">
+
+
+ copy()} label="Copy query (Shift-Ctrl-C)">
+
+
+ >
+ )
+
+ const onClickReference = () => {
+ if (pluginResize.hiddenElement === 'second') {
+ pluginResize.setHiddenElement(null)
+ }
+ }
+
+ const modifier =
+ window.navigator.platform.toLowerCase().indexOf('mac') === 0 ? (
+ Cmd
+ ) : (
+ Ctrl
+ )
+
+ return (
+
+
+
+
+
+
+ {editorContext.tabs.length > 1 ? (
+ <>
+ {editorContext.tabs.map((tab, index) => (
+
+ {
+ executionContext.stop()
+ editorContext.changeTab(index)
+ }}
+ >
+ {tab.title}
+
+ {
+ if (editorContext.activeTabIndex === index) {
+ executionContext.stop()
+ }
+ editorContext.closeTab(index)
+ }}
+ />
+
+ ))}
+
+
+ editorContext.addTab()}
+ aria-label="Add tab"
+ >
+
+
+
+
+ >
+ ) : null}
+
+
+ {editorContext.tabs.length === 1 ? (
+
+
+ editorContext.addTab()}
+ aria-label="Add tab"
+ >
+
+
+
+
+ ) : null}
+ {logo}
+
+
+
+
+
+
+
+
+
+
+
+
+ {toolbar}
+
+
+
+
+
+
+ {
+ if (editorToolsResize.hiddenElement === 'second') {
+ editorToolsResize.setHiddenElement(null)
+ }
+ setActiveSecondaryEditor('variables')
+ }}
+ >
+ Variables
+
+
+ {
+ if (editorToolsResize.hiddenElement === 'second') {
+ editorToolsResize.setHiddenElement(null)
+ }
+ setActiveSecondaryEditor('headers')
+ }}
+ >
+ Headers
+
+
+
+ {
+ editorToolsResize.setHiddenElement(
+ editorToolsResize.hiddenElement === 'second' ? null : 'second'
+ )
+ }}
+ aria-label={
+ editorToolsResize.hiddenElement === 'second'
+ ? 'Show editor tools'
+ : 'Hide editor tools'
+ }
+ >
+ {editorToolsResize.hiddenElement === 'second' ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+ {executionContext.isFetching ? : null}
+
+
+
+
+
+
+
+
+ {pluginContext?.visiblePlugin ?
: null}
+
+
+
+
+
+
+ {pluginContext?.plugins.map((plugin) => {
+ const isVisible = plugin === pluginContext.visiblePlugin
+ const label = `${isVisible ? 'Hide' : 'Show'} ${plugin.title}`
+ const Icon = plugin.icon
+ return (
+
+ {
+ if (isVisible) {
+ pluginContext.setVisiblePlugin(null)
+ pluginResize.setHiddenElement('second')
+ } else {
+ pluginContext.setVisiblePlugin(plugin)
+ pluginResize.setHiddenElement(null)
+ }
+ }}
+ aria-label={label}
+ >
+
+
+
+ )
+ })}
+
+
+
+ schemaContext.introspect()}
+ aria-label="Re-fetch GraphQL schema"
+ >
+
+
+
+
+ setShowDialog('short-keys')}
+ aria-label="Open short keys dialog"
+ >
+
+
+
+
+ setShowDialog('settings')}
+ aria-label="Open settings dialog"
+ >
+
+
+
+
+
+
setShowDialog(null)}>
+
+
Short Keys
+
setShowDialog(null)} />
+
+
+
+
+
+
+ Short key
+ Function
+
+
+
+
+
+ {modifier}
+ {' + '}
+ F
+
+ Search in editor
+
+
+
+ {modifier}
+ {' + '}
+ K
+
+ Search in documentation
+
+
+
+ {modifier}
+ {' + '}
+ Enter
+
+ Execute query
+
+
+
+ Ctrl
+ {' + '}
+ Shift
+ {' + '}
+ P
+
+ Prettify editors
+
+
+
+ Ctrl
+ {' + '}
+ Shift
+ {' + '}
+ M
+
+ Merge fragments definitions into operation definition
+
+
+
+ Ctrl
+ {' + '}
+ Shift
+ {' + '}
+ C
+
+ Copy query
+
+
+
+ Ctrl
+ {' + '}
+ Shift
+ {' + '}
+ R
+
+ Re-fetch schema using introspection
+
+
+
+
+ The editors use{' '}
+
+ CodeMirror Key Maps
+ {' '}
+ that add more short keys. This instance of Graphi QL uses sublime
+ .
+
+
+
+
+
{
+ setShowDialog(null)
+ setClearStorageStatus(null)
+ }}
+ >
+
+
Settings
+
{
+ setShowDialog(null)
+ setClearStorageStatus(null)
+ }}
+ />
+
+
+ {storageContext ? (
+
+
+
Clear storage
+
+ Remove all locally stored data and start fresh.
+
+
+
+ {
+ try {
+ storageContext?.clear()
+ setClearStorageStatus('success')
+ } catch {
+ setClearStorageStatus('error')
+ }
+ }}
+ >
+ {clearStorageStatus === 'success'
+ ? 'Cleared data'
+ : clearStorageStatus === 'error'
+ ? 'Failed'
+ : 'Clear data'}
+
+
+
+ ) : null}
+
+
+ )
+}
+
+// Configure the UI by providing this Component as a child of GraphiQL.
+function GraphiQLLogo(props: PropsWithChildren) {
+ return (
+
+ )
+}
+
+GraphiQLLogo.displayName = 'GraphiQLLogo'
+
+export default GraphiQL
diff --git a/studio/components/interfaces/GraphQL/graphiql.module.css b/studio/components/interfaces/GraphQL/graphiql.module.css
new file mode 100644
index 00000000000..dc574beed61
--- /dev/null
+++ b/studio/components/interfaces/GraphQL/graphiql.module.css
@@ -0,0 +1,27 @@
+.graphiqlContainer .graphiqlSessions {
+ margin: 0;
+ border-radius: 0;
+}
+
+.graphiqlContainer .graphiqlSession {
+ padding: 0;
+}
+
+.graphiqlContainer .graphiqlEditors {
+ border-radius: 0;
+}
+
+.graphiqlContainer .graphiqlEditors:global(.full-height) {
+ margin-top: calc(0px - var(--session-header-height));
+}
+
+/* Colors */
+:global(body.graphiql-dark) .graphiqlContainer {
+ --color-base: 0, 0%, 11%;
+ --color-primary: 153, 50%, 50%;
+}
+
+:global(body.graphiql-light) .graphiqlContainer {
+ --color-base: 210, 17%, 98%;
+ --color-primary: 153, 50%, 50%;
+}
diff --git a/studio/components/interfaces/Settings/Database/ConnectionPooling.tsx b/studio/components/interfaces/Settings/Database/ConnectionPooling.tsx
index 111a003258a..43ce15af121 100644
--- a/studio/components/interfaces/Settings/Database/ConnectionPooling.tsx
+++ b/studio/components/interfaces/Settings/Database/ConnectionPooling.tsx
@@ -1,6 +1,6 @@
import { FC, useState, useEffect, Fragment } from 'react'
import { observer } from 'mobx-react-lite'
-import { useStore } from 'hooks'
+import { checkPermissions, useStore } from 'hooks'
import { pluckObjectFields } from 'lib/helpers'
import Loading from 'components/ui/Loading'
import Panel from 'components/ui/Panel'
@@ -12,6 +12,7 @@ import ToggleField from 'components/to-be-cleaned/forms/ToggleField'
import SchemaFormPanel from 'components/to-be-cleaned/forms/SchemaFormPanel'
import { Input } from 'ui'
import ShimmeringLoader from 'components/ui/ShimmeringLoader'
+import { PermissionAction } from '@supabase/shared-types/out/constants'
interface Props {}
@@ -127,6 +128,11 @@ export const PgbouncerConfig: FC = observer(
({ projectRef, bouncerInfo, connectionInfo }) => {
const { ui } = useStore()
+ const canUpdateConnectionPoolingConfiguration = checkPermissions(
+ PermissionAction.UPDATE,
+ 'projects'
+ )
+
const [updates, setUpdates] = useState({
pgbouncer_enabled: bouncerInfo.pgbouncer_enabled,
pool_mode: bouncerInfo.pool_mode || 'transaction',
@@ -198,6 +204,8 @@ export const PgbouncerConfig: FC = observer(
onChangeModel={(model: any) => setUpdates(model)}
onSubmit={(model: any) => updateConfig(model)}
onReset={() => setUpdates(bouncerInfo)}
+ disabled={!canUpdateConnectionPoolingConfiguration}
+ disabledMessage="You need additional permissions to update connection pooling settings"
>
diff --git a/studio/components/interfaces/Settings/Database/NetworkRestrictions/NetworkRestrictions.tsx b/studio/components/interfaces/Settings/Database/NetworkRestrictions/NetworkRestrictions.tsx
index 7fa753067e7..b3c47c59119 100644
--- a/studio/components/interfaces/Settings/Database/NetworkRestrictions/NetworkRestrictions.tsx
+++ b/studio/components/interfaces/Settings/Database/NetworkRestrictions/NetworkRestrictions.tsx
@@ -1,8 +1,10 @@
import Link from 'next/link'
-import { useState } from 'react'
+import { FC, useState } from 'react'
+import * as Tooltip from '@radix-ui/react-tooltip'
import { Button, IconAlertCircle, IconExternalLink, IconGlobe, IconLock } from 'ui'
+import { PermissionAction } from '@supabase/shared-types/out/constants'
-import { useParams } from 'hooks'
+import { checkPermissions, useParams } from 'hooks'
import Panel from 'components/ui/Panel'
import { FormPanel, FormHeader } from 'components/ui/Forms'
import ShimmeringLoader from 'components/ui/ShimmeringLoader'
@@ -12,6 +14,62 @@ import DisallowAllModal from './DisallowAllModal'
import AllowAllModal from './AllowAllModal'
import { useNetworkRestrictionsQuery } from 'data/network-restrictions/network-restrictions-query'
+const AllowAllAccessButton: FC<{ disabled: boolean; onClick: (value: boolean) => void }> = ({
+ disabled,
+ onClick,
+}) => (
+
+
+ onClick(true)}>
+ Allow all access
+
+
+ {disabled && (
+
+
+
+
+ You need additional permissions to update network restrictions
+
+
+
+ )}
+
+)
+
+const DisallowAllAccessButton: FC<{ disabled: boolean; onClick: (value: boolean) => void }> = ({
+ disabled,
+ onClick,
+}) => (
+
+
+ onClick(true)}>
+ Restrict all access
+
+
+ {disabled && (
+
+
+
+
+ You need additional permissions to update network restrictions
+
+
+
+ )}
+
+)
+
const NetworkRestrictions = ({}) => {
const { ref } = useParams()
@@ -21,6 +79,8 @@ const NetworkRestrictions = ({}) => {
const [selectedRestrictionToRemove, setSelectedRestrictionToRemove] = useState
()
const { data, isLoading } = useNetworkRestrictionsQuery({ projectRef: ref })
+ const canUpdateNetworkRestrictions = checkPermissions(PermissionAction.UPDATE, 'projects')
+
const hasAccessToRestrictions = data?.entitlement === 'allowed'
const restrictedIps = data?.config.dbAllowedCidrs ?? []
const restrictionStatus = data?.status ?? ''
@@ -48,7 +108,31 @@ const NetworkRestrictions = ({}) => {
- setIsAddingAddress(true)}>Add restriction
+
+
+ setIsAddingAddress(true)}
+ >
+ Add restriction
+
+
+ {!canUpdateNetworkRestrictions && (
+
+
+
+
+ You need additional permissions to update network restrictions
+
+
+
+ )}
+
{isLoading ? (
@@ -75,12 +159,14 @@ const NetworkRestrictions = ({}) => {
-
setIsAllowingAll(true)}>
- Allow all access
-
-
setIsDisallowingAll(true)}>
- Restrict all access
-
+
+
@@ -101,9 +187,10 @@ const NetworkRestrictions = ({}) => {
- setIsDisallowingAll(true)}>
- Restrict all access
-
+
) : isDisallowedAll ? (
@@ -125,9 +212,10 @@ const NetworkRestrictions = ({}) => {
-
setIsAllowingAll(true)}>
- Allow all access
-
+
) : (
@@ -146,12 +234,14 @@ const NetworkRestrictions = ({}) => {
-
setIsAllowingAll(true)}>
- Allow all access
-
-
setIsDisallowingAll(true)}>
- Restrict all access
-
+
+
{restrictedIps.map((ip) => {
diff --git a/studio/components/interfaces/Settings/Database/SSLConfiguration.tsx b/studio/components/interfaces/Settings/Database/SSLConfiguration.tsx
index 192c6137897..20935a97d97 100644
--- a/studio/components/interfaces/Settings/Database/SSLConfiguration.tsx
+++ b/studio/components/interfaces/Settings/Database/SSLConfiguration.tsx
@@ -1,8 +1,10 @@
import Link from 'next/link'
import { useState, useEffect } from 'react'
-import { useParams, useStore, useFlag } from 'hooks'
import * as Tooltip from '@radix-ui/react-tooltip'
import { Button, IconDownload, Toggle, IconLoader, Alert } from 'ui'
+import { PermissionAction } from '@supabase/shared-types/out/constants'
+
+import { checkPermissions, useParams, useStore, useFlag } from 'hooks'
import {
FormHeader,
FormPanel,
@@ -27,6 +29,8 @@ const SSLConfiguration = () => {
})
const { mutateAsync: updateSSLEnforcement } = useSSLEnforcementUpdateMutation()
+ const canUpdateSSLEnforcement = checkPermissions(PermissionAction.UPDATE, 'projects')
+
const hasAccessToSSLEnforcement = !sslEnforcementConfiguration?.isNotAllowed
const env = process.env.NEXT_PUBLIC_ENVIRONMENT === 'prod' ? 'prod' : 'staging'
const hasSSLCertificate =
diff --git a/studio/components/interfaces/Settings/Logs/LogsErrorRenderers/ResourcesExceededErrorRenderer.tsx b/studio/components/interfaces/Settings/Logs/LogsErrorRenderers/ResourcesExceededErrorRenderer.tsx
index 2473f64aa19..c8b76284adc 100644
--- a/studio/components/interfaces/Settings/Logs/LogsErrorRenderers/ResourcesExceededErrorRenderer.tsx
+++ b/studio/components/interfaces/Settings/Logs/LogsErrorRenderers/ResourcesExceededErrorRenderer.tsx
@@ -19,7 +19,6 @@ const ResourcesExceededErrorRenderer: React.FC = ({ error, i
type="default"
chevronAlign="left"
size="small"
- bordered={false}
iconPosition="left"
>
diff --git a/studio/components/interfaces/Settings/ProjectUsageBars/ProjectUsageBars.tsx b/studio/components/interfaces/Settings/ProjectUsageBars/ProjectUsageBars.tsx
index a7d3134706e..4de70bac330 100644
--- a/studio/components/interfaces/Settings/ProjectUsageBars/ProjectUsageBars.tsx
+++ b/studio/components/interfaces/Settings/ProjectUsageBars/ProjectUsageBars.tsx
@@ -1,16 +1,29 @@
-import { FC, useEffect } from 'react'
-import { Badge, Button, IconAlertCircle, IconInfo, Loading, IconExternalLink } from 'ui'
+import Link from 'next/link'
+import { FC, useEffect, useState } from 'react'
+import { useRouter } from 'next/router'
+import * as Tooltip from '@radix-ui/react-tooltip'
+import { PermissionAction } from '@supabase/shared-types/out/constants'
+import {
+ Badge,
+ Button,
+ IconAlertCircle,
+ IconInfo,
+ Loading,
+ IconExternalLink,
+ Alert,
+ IconBookOpen,
+} from 'ui'
-import { useStore } from 'hooks'
+import { checkPermissions, useStore } from 'hooks'
import { formatBytes } from 'lib/helpers'
import { PRICING_TIER_PRODUCT_IDS, USAGE_APPROACHING_THRESHOLD } from 'lib/constants'
import SparkBar from 'components/ui/SparkBar'
import ShimmeringLoader from 'components/ui/ShimmeringLoader'
import InformationBox from 'components/ui/InformationBox'
import { USAGE_BASED_PRODUCTS } from 'components/interfaces/Billing/Billing.constants'
+import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
+import { executeSql } from 'data/sql/execute-sql-query'
import { ProjectUsageResponseUsageKeys, useProjectUsageQuery } from 'data/usage/project-usage-query'
-import { useRouter } from 'next/router'
-import * as Tooltip from '@radix-ui/react-tooltip'
interface Props {
projectRef?: string
@@ -21,8 +34,14 @@ const ProjectUsage: FC = ({ projectRef }) => {
const { data: usage, error, isLoading } = useProjectUsageQuery({ projectRef })
const router = useRouter()
- const subscriptionTier = ui.selectedProject?.subscription_tier
+ const { project } = useProjectContext()
+ const [isReadOnlyMode, setIsReadOnlyMode] = useState(false)
+ const canUpdateSubscription = checkPermissions(
+ PermissionAction.BILLING_WRITE,
+ 'stripe.subscriptions'
+ )
+ const subscriptionTier = ui.selectedProject?.subscription_tier
const projectHasNoLimits =
subscriptionTier === PRICING_TIER_PRODUCT_IDS.PAYG ||
subscriptionTier === PRICING_TIER_PRODUCT_IDS.TEAM ||
@@ -59,11 +78,25 @@ const ProjectUsage: FC = ({ projectRef }) => {
/>
)
}
+ // [Terry]
+ // temporary solution to check if project is in read only mode
+ // until we get an api endpoint for this
+
+ async function checkForReadOnlyMode() {
+ const sql = `show default_transaction_read_only;`
+ const connectionString = project?.connectionString
+ const { result: readOnlyStatus } = await executeSql({ projectRef, connectionString, sql })
+ if (readOnlyStatus[0]?.default_transaction_read_only === 'on') setIsReadOnlyMode(true)
+ }
+
+ useEffect(() => {
+ if (projectRef) checkForReadOnlyMode()
+ }, [projectRef])
const isPaidTier = subscriptionTier !== PRICING_TIER_PRODUCT_IDS.FREE
const featureFootnotes: Record = {
- db_size: (
+ db_size: isPaidTier ? (
{usage?.disk_volume_size_gb &&
Disk Size: {usage.disk_volume_size_gb} GB }
@@ -77,6 +110,8 @@ const ProjectUsage: FC
= ({ projectRef }) => {
+ ) : (
+ <>>
),
}
@@ -127,7 +162,6 @@ const ProjectUsage: FC
= ({ projectRef }) => {
-
{/* Line items */}
{usage === undefined ? (
@@ -148,14 +182,16 @@ const ProjectUsage: FC
= ({ projectRef }) => {
usageElement = (
Not included in {planName} tier
-
- router.push(`/project/${projectRef}/settings/billing/update`)
- }
- >
- Upgrade to Pro
-
+ {canUpdateSubscription && (
+
+ router.push(`/project/${projectRef}/settings/billing/update`)
+ }
+ >
+ Upgrade to Pro
+
+ )}
)
} else if (showUsageExceedMessage) {
@@ -253,6 +289,121 @@ const ProjectUsage: FC = ({ projectRef }) => {
)}
+
+ {!isReadOnlyMode && product.title === 'Database' && (
+
+ )}
)
})}
diff --git a/studio/components/interfaces/Settings/Vault/Keys/EncryptionKeysManagement.tsx b/studio/components/interfaces/Settings/Vault/Keys/EncryptionKeysManagement.tsx
index 876805da8a1..63cac0d9820 100644
--- a/studio/components/interfaces/Settings/Vault/Keys/EncryptionKeysManagement.tsx
+++ b/studio/components/interfaces/Settings/Vault/Keys/EncryptionKeysManagement.tsx
@@ -1,6 +1,7 @@
import dayjs from 'dayjs'
import Link from 'next/link'
import { observer } from 'mobx-react-lite'
+import * as Tooltip from '@radix-ui/react-tooltip'
import { FC, Fragment, useEffect, useState } from 'react'
import {
Alert,
@@ -11,13 +12,13 @@ import {
Modal,
Form,
IconTrash,
- Badge,
IconKey,
IconLoader,
IconX,
IconExternalLink,
} from 'ui'
-import { useParams, useStore } from 'hooks'
+import { PermissionAction } from '@supabase/shared-types/out/constants'
+import { useParams, useStore, checkPermissions } from 'hooks'
import Divider from 'components/ui/Divider'
const DEFAULT_KEY_NAME = 'No description provided'
@@ -34,6 +35,8 @@ const EncryptionKeysManagement: FC = ({}) => {
const [selectedKeyToRemove, setSelectedKeyToRemove] = useState()
const [isDeletingKey, setIsDeletingKey] = useState(false)
+ const canManageKeys = checkPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'tables')
+
useEffect(() => {
if (id !== undefined) setSearchValue(id)
}, [id])
@@ -143,9 +146,32 @@ const EncryptionKeysManagement: FC = ({}) => {
- setShowAddKeyModal(true)}>
- Add new key
-
+
+
+ setShowAddKeyModal(true)}
+ >
+ Add new key
+
+
+ {!canManageKeys && (
+
+
+
+
+ You need additional permissions to add keys
+
+
+
+ )}
+
@@ -178,12 +204,32 @@ const EncryptionKeysManagement: FC = ({}) => {
Added on {dayjs(key.created).format('MMM D, YYYY')}
- }
- onClick={() => setSelectedKeyToRemove(key)}
- />
+
+
+ }
+ disabled={!canManageKeys}
+ onClick={() => setSelectedKeyToRemove(key)}
+ />
+
+ {!canManageKeys && (
+
+
+
+
+ You need additional permissions to delete keys
+
+
+
+ )}
+
{idx !== keys.length - 1 && }
diff --git a/studio/components/interfaces/Settings/Vault/Secrets/SecretRow.tsx b/studio/components/interfaces/Settings/Vault/Secrets/SecretRow.tsx
index 412435028c3..c3c08a02e92 100644
--- a/studio/components/interfaces/Settings/Vault/Secrets/SecretRow.tsx
+++ b/studio/components/interfaces/Settings/Vault/Secrets/SecretRow.tsx
@@ -1,3 +1,4 @@
+import Link from 'next/link'
import dayjs from 'dayjs'
import { FC, useState } from 'react'
import {
@@ -12,9 +13,10 @@ import {
IconMoreVertical,
IconLoader,
} from 'ui'
+import * as Tooltip from '@radix-ui/react-tooltip'
+import { PermissionAction } from '@supabase/shared-types/out/constants'
import { VaultSecret } from 'types'
-import { useParams, useStore } from 'hooks'
-import Link from 'next/link'
+import { useParams, useStore, checkPermissions } from 'hooks'
interface Props {
secret: VaultSecret
@@ -30,6 +32,8 @@ const SecretRow: FC = ({ secret, onSelectEdit, onSelectRemove }) => {
const [revealedValue, setRevealedValue] = useState()
const name = secret?.name ?? 'No name provided'
+ const canManageSecrets = checkPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'tables')
+
const revealSecret = async () => {
setIsLoading(true)
if (revealedValue === undefined) {
@@ -106,15 +110,58 @@ const SecretRow: FC = ({ secret, onSelectEdit, onSelectRemove }) => {
className="w-[120px]"
overlay={
<>
- } onClick={() => onSelectEdit(secret)}>
- Edit
-
- }
- onClick={() => onSelectRemove(secret)}
- >
- Delete
-
+
+
+ }
+ disabled={!canManageSecrets}
+ onClick={() => onSelectEdit(secret)}
+ >
+ Edit
+
+
+ {!canManageSecrets && (
+
+
+
+
+ You need additional permissions to edit secrets
+
+
+
+ )}
+
+
+
+ }
+ onClick={() => onSelectRemove(secret)}
+ >
+ Delete
+
+
+ {!canManageSecrets && (
+
+
+
+
+ You need additional permissions to delete secrets
+
+
+
+ )}
+
>
}
>
diff --git a/studio/components/interfaces/Settings/Vault/Secrets/SecretsManagement.tsx b/studio/components/interfaces/Settings/Vault/Secrets/SecretsManagement.tsx
index 468773ef358..05ca9c5d11c 100644
--- a/studio/components/interfaces/Settings/Vault/Secrets/SecretsManagement.tsx
+++ b/studio/components/interfaces/Settings/Vault/Secrets/SecretsManagement.tsx
@@ -1,9 +1,11 @@
import Link from 'next/link'
import { observer } from 'mobx-react-lite'
+import * as Tooltip from '@radix-ui/react-tooltip'
import { FC, Fragment, useState, useEffect } from 'react'
import { IconSearch, Input, Button, Listbox, IconLoader, IconExternalLink, IconX } from 'ui'
+import { PermissionAction } from '@supabase/shared-types/out/constants'
-import { useStore, useParams } from 'hooks'
+import { useStore, useParams, checkPermissions } from 'hooks'
import SecretRow from './SecretRow'
import EditSecretModal from './EditSecretModal'
import DeleteSecretModal from './DeleteSecretModal'
@@ -22,6 +24,8 @@ const SecretsManagement: FC = ({}) => {
const [selectedSecretToEdit, setSelectedSecretToEdit] = useState()
const [selectedSecretToRemove, setSelectedSecretToRemove] = useState()
+ const canManageSecrets = checkPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'tables')
+
useEffect(() => {
if (search !== undefined) setSearchValue(search)
}, [search])
@@ -105,9 +109,32 @@ const SecretsManagement: FC = ({}) => {
- setShowAddSecretModal(true)}>
- Add new secret
-
+
+
+ setShowAddSecretModal(true)}
+ >
+ Add new secret
+
+
+ {!canManageSecrets && (
+
+
+
+
+ You need additional permissions to add secrets
+
+
+
+ )}
+
diff --git a/studio/components/interfaces/TableGridEditor/GridHeaderActions.tsx b/studio/components/interfaces/TableGridEditor/GridHeaderActions.tsx
index 198e9a2a964..2a8a757aaa5 100644
--- a/studio/components/interfaces/TableGridEditor/GridHeaderActions.tsx
+++ b/studio/components/interfaces/TableGridEditor/GridHeaderActions.tsx
@@ -1,9 +1,11 @@
import { FC } from 'react'
import Link from 'next/link'
+import * as Tooltip from '@radix-ui/react-tooltip'
import { Button, IconAlertCircle, IconLock } from 'ui'
import type { PostgresPolicy, PostgresTable } from '@supabase/postgres-meta'
-import { useStore } from 'hooks'
+import { useStore, checkPermissions } from 'hooks'
+import { PermissionAction } from '@supabase/shared-types/out/constants'
interface Props {
table: PostgresTable
@@ -14,8 +16,34 @@ const GridHeaderActions: FC = ({ table }) => {
const projectRef = ui.selectedProject?.ref
const policies = meta.policies.list((policy: PostgresPolicy) => policy.table_id === table.id)
+ const isReadOnly =
+ !checkPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'tables') &&
+ !checkPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'columns')
+
return (
+ {isReadOnly && (
+
+
+
+ Viewing as read-only
+
+
+
+
+
+
+ You need additional permissions to manage your project's data
+
+
+
+
+ )}
= ({
const isVaultEnabled = useFlag('vaultExtension')
const [encryptedColumns, setEncryptedColumns] = useState([])
+ const isReadOnly =
+ !checkPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'tables') &&
+ !checkPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'columns')
+
const getEncryptedColumns = async (table: any) => {
const columns = await vault.listEncryptedColumns(table.schema, table.name)
setEncryptedColumns(columns)
@@ -170,7 +174,7 @@ const TableGridEditor: FC = ({
theme={theme}
gridProps={{ height: '100%' }}
storageRef={projectRef}
- editable={canUpdateTables && canEditViaTableEditor}
+ editable={!isReadOnly && canUpdateTables && canEditViaTableEditor}
schema={selectedTable.schema}
table={gridTable}
headerActions={
diff --git a/studio/components/layouts/DocsLayout/DocsLayout.tsx b/studio/components/layouts/DocsLayout/DocsLayout.tsx
index c172f2c7a88..4a1a2addce8 100644
--- a/studio/components/layouts/DocsLayout/DocsLayout.tsx
+++ b/studio/components/layouts/DocsLayout/DocsLayout.tsx
@@ -2,7 +2,7 @@ import { ReactElement, useEffect } from 'react'
import { observer } from 'mobx-react-lite'
import { useRouter } from 'next/router'
-import { useStore, withAuth } from 'hooks'
+import { useFlag, useStore, withAuth } from 'hooks'
import Error from 'components/ui/Error'
import ProductMenu from 'components/ui/ProductMenu'
import ProjectLayout from 'components/layouts/ProjectLayout/ProjectLayout'
@@ -13,7 +13,11 @@ function DocsLayout({ title, children }: { title: string; children: ReactElement
const { meta, ui } = useStore()
const { data, isLoading, error } = meta.openApi
+ const graphiql = useFlag('graphiql')
+
const getPage = () => {
+ if (router.pathname.endsWith('graphiql')) return 'graphiql'
+
const { page, resource } = router.query
if (!page && !resource) return 'introduction'
return (page || resource || '') as string
@@ -45,7 +49,7 @@ function DocsLayout({ title, children }: { title: string; children: ReactElement
productMenu={
}
>
diff --git a/studio/components/layouts/DocsLayout/DocsLayout.utils.tsx b/studio/components/layouts/DocsLayout/DocsLayout.utils.tsx
index c97284cf3e1..155f14f6d57 100644
--- a/studio/components/layouts/DocsLayout/DocsLayout.utils.tsx
+++ b/studio/components/layouts/DocsLayout/DocsLayout.utils.tsx
@@ -1,10 +1,12 @@
import { ProductMenuGroup } from 'components/ui/ProductMenu/ProductMenu.types'
+import SVG from 'react-inlinesvg'
import { IconBook, IconBookOpen } from 'ui'
export const generateDocsMenu = (
ref: string,
tables: string[],
- functions: string[]
+ functions: string[],
+ showGraphiql: boolean
): ProductMenuGroup[] => {
return [
{
@@ -58,6 +60,31 @@ export const generateDocsMenu = (
}),
],
},
+ ...(showGraphiql
+ ? [
+ {
+ title: 'GraphQL',
+ items: [
+ {
+ name: 'GraphiQL',
+ key: 'graphiql',
+ url: `/project/${ref}/api/graphiql`,
+ icon: (
+
+ code.replace(/svg/, 'svg class="m-auto text-color-inherit"')
+ }
+ />
+ ),
+ items: [],
+ },
+ ],
+ },
+ ]
+ : []),
{
title: 'More Resources',
items: [
diff --git a/studio/components/layouts/FunctionsLayout.tsx b/studio/components/layouts/FunctionsLayout.tsx
index 511d401da95..0dc242cd836 100644
--- a/studio/components/layouts/FunctionsLayout.tsx
+++ b/studio/components/layouts/FunctionsLayout.tsx
@@ -1,14 +1,15 @@
import Link from 'next/link'
import { useRouter } from 'next/router'
-import { FC, useEffect, ReactNode } from 'react'
+import { FC, useEffect, ReactNode, useState } from 'react'
import { observer } from 'mobx-react-lite'
-import { IconCode } from 'ui'
+import { Button, IconExternalLink, IconCode, Modal, IconTerminal } from 'ui'
-import { checkPermissions, useStore, withAuth } from 'hooks'
+import { checkPermissions, useParams, useStore, withAuth } from 'hooks'
import FunctionsNav from '../interfaces/Functions/FunctionsNav'
import BaseLayout from 'components/layouts'
import NoPermission from 'components/ui/NoPermission'
import { PermissionAction } from '@supabase/shared-types/out/constants'
+import { TerminalInstructions } from 'components/interfaces/Functions'
interface Props {
title?: string
@@ -16,10 +17,10 @@ interface Props {
}
const FunctionsLayout: FC = ({ title, children }) => {
- const { functions, ui } = useStore()
const router = useRouter()
-
- const { id, ref } = router.query
+ const { id, ref } = useParams()
+ const { functions, ui } = useStore()
+ const [showTerminalInstructions, setShowTerminalInstructions] = useState(false)
useEffect(() => {
if (ui.selectedProjectRef) functions.load()
@@ -87,7 +88,7 @@ const FunctionsLayout: FC = ({ title, children }) => {
].join(' ')}
>
-
+
= ({ title, children }) => {
-
-
-
- Edge Functions
-
-
-
Beta
+
+
+
+
+ Edge Functions
+
+
+
Beta
+ {name && (
+
+ )}
+
+
-
- {name && (
- <>
-
-
-
-
-
-
{name}
- >
- )}
{item &&
}
@@ -140,6 +161,24 @@ const FunctionsLayout: FC
= ({ title, children }) => {
)}
+
+ setShowTerminalInstructions(false)}
+ header={Deploying an edge function to your project }
+ customFooter={
+
+ setShowTerminalInstructions(false)}>
+ Confirm
+
+
+ }
+ >
+
+
+
+
)
}
diff --git a/studio/components/layouts/ProjectLayout/LayoutHeader/LayoutHeader.tsx b/studio/components/layouts/ProjectLayout/LayoutHeader/LayoutHeader.tsx
index cf8789a80e2..b3b3c01e9ea 100644
--- a/studio/components/layouts/ProjectLayout/LayoutHeader/LayoutHeader.tsx
+++ b/studio/components/layouts/ProjectLayout/LayoutHeader/LayoutHeader.tsx
@@ -11,19 +11,43 @@ import HelpPopover from './HelpPopover'
import NotificationsPopover from './NotificationsPopover'
import { getResourcesExceededLimits } from 'components/ui/OveragesBanner/OveragesBanner.utils'
import { useProjectUsageQuery } from 'data/usage/project-usage-query'
+import { Badge } from 'ui'
+import { executeSql } from 'data/sql/execute-sql-query'
+import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
+
+import { useEffect, useState } from 'react'
const LayoutHeader = ({ customHeaderComponents, breadcrumbs = [], headerBorder = true }: any) => {
const { ui } = useStore()
const { selectedOrganization, selectedProject } = ui
const { ref: projectRef } = useParams()
+ const { project } = useProjectContext()
+
+ const [isReadOnlyMode, setIsReadOnlyMode] = useState(false)
+
+ // [Terry]
+ // temporary solution to check if project is in read only mode
+ // until we get an api endpoint for this
+ async function checkForReadOnlyMode() {
+ const sql = `show default_transaction_read_only;`
+ const connectionString = project?.connectionString
+ const { result: readOnlyStatus } = await executeSql({ projectRef, connectionString, sql })
+ if (readOnlyStatus[0]?.default_transaction_read_only === 'on') setIsReadOnlyMode(true)
+ }
+
+ useEffect(() => {
+ if (projectRef) checkForReadOnlyMode()
+ }, [projectRef])
const { data: usage } = useProjectUsageQuery({ projectRef })
const resourcesExceededLimits = getResourcesExceededLimits(usage)
+
const projectHasNoLimits =
ui.selectedProject?.subscription_tier === PRICING_TIER_PRODUCT_IDS.PAYG ||
ui.selectedProject?.subscription_tier === PRICING_TIER_PRODUCT_IDS.ENTERPRISE ||
ui.selectedProject?.subscription_tier === PRICING_TIER_PRODUCT_IDS.TEAM
+
const showOverUsageBadge =
selectedProject?.subscription_tier !== undefined &&
!projectHasNoLimits &&
@@ -63,6 +87,18 @@ const LayoutHeader = ({ customHeaderComponents, breadcrumbs = [], headerBorder =
{/* Project Dropdown */}
+ {/* [Terry] Temporary until we figure out how we want to display this permanently */}
+ {/* context: https://www.notion.so/supabase/DB-Disk-Size-Free-tier-Read-only-Critical-f2b8937c13a149e3ac769fe5888f6db0*/}
+ {!isReadOnlyMode && (
+
+ )}
+
{/* [Joshen TODO] Temporarily hidden until usage endpoint is sorted out */}
{/* {showOverUsageBadge && (
diff --git a/studio/components/layouts/StorageLayout/StorageLayout.tsx b/studio/components/layouts/StorageLayout/StorageLayout.tsx
index 93b1d1346ea..53432b6461c 100644
--- a/studio/components/layouts/StorageLayout/StorageLayout.tsx
+++ b/studio/components/layouts/StorageLayout/StorageLayout.tsx
@@ -40,6 +40,7 @@ const StorageLayout: FC
= ({ title, children }) => {
buckets,
} = storageExplorerStore || {}
+ // [Joshen] At the moment, without the service key, we won't be able to pull any data from storage
const { data: settings, isLoading } = useProjectApiQuery({ projectRef })
const apiService = settings?.autoApiService
const serviceKey = find(apiService?.service_api_keys ?? [], (key) => key.tags === 'service_role')
diff --git a/studio/components/layouts/StorageLayout/StorageMenu.tsx b/studio/components/layouts/StorageLayout/StorageMenu.tsx
index 441e07c40a9..ae80c18a4c9 100644
--- a/studio/components/layouts/StorageLayout/StorageMenu.tsx
+++ b/studio/components/layouts/StorageLayout/StorageMenu.tsx
@@ -1,12 +1,15 @@
import { FC } from 'react'
import Link from 'next/link'
+import { find } from 'lodash'
import { useRouter } from 'next/router'
import { observer } from 'mobx-react-lite'
-import { useParams } from 'hooks'
+import * as Tooltip from '@radix-ui/react-tooltip'
import { Button, Menu, IconLoader, Alert, IconEdit } from 'ui'
+import { useParams } from 'hooks'
import BucketRow from './BucketRow'
import { useStorageStore } from 'localStores/storageExplorer/StorageExplorerStore'
+import { useProjectApiQuery } from 'data/config/project-api-query'
interface Props {}
@@ -30,22 +33,47 @@ const StorageMenu: FC = () => {
openToggleBucketPublicModal,
} = storageExplorerStore || {}
+ const { data: settings, isLoading } = useProjectApiQuery({ projectRef: ref })
+ const apiService = settings?.autoApiService
+ const serviceKey = find(apiService?.service_api_keys ?? [], (key) => key.tags === 'service_role')
+ const canAccessStorage = !isLoading && apiService && serviceKey !== undefined
+
return (
-
-
-
- }
- style={{ justifyContent: 'start' }}
- onClick={openCreateBucketModal}
- >
- New bucket
-
+
+
+
+
+
+ }
+ disabled={!canAccessStorage}
+ style={{ justifyContent: 'start' }}
+ onClick={openCreateBucketModal}
+ >
+ New bucket
+
+
+ {!canAccessStorage && (
+
+
+
+
+ You need additional permissions to create buckets
+
+
+
+ )}
+
diff --git a/studio/components/to-be-cleaned/Storage/StorageSettings/StorageSettings.tsx b/studio/components/to-be-cleaned/Storage/StorageSettings/StorageSettings.tsx
index 4e05953129a..a604849edf0 100644
--- a/studio/components/to-be-cleaned/Storage/StorageSettings/StorageSettings.tsx
+++ b/studio/components/to-be-cleaned/Storage/StorageSettings/StorageSettings.tsx
@@ -1,12 +1,14 @@
import { useState } from 'react'
+import * as Tooltip from '@radix-ui/react-tooltip'
import { Button, Form, IconClock, Input, Listbox } from 'ui'
-import { useStore } from 'hooks'
+import { checkPermissions, useStore } from 'hooks'
import { useProjectStorageConfigQuery } from 'data/config/project-storage-config-query'
import { useProjectStorageConfigUpdateUpdateMutation } from 'data/config/project-storage-config-update-mutation'
import UpgradeToPro from 'components/ui/UpgradeToPro'
import { convertFromBytes, convertToBytes } from './StorageSettings.utils'
import { StorageSizeUnits, STORAGE_FILE_SIZE_LIMIT_MAX_BYTES } from './StorageSettings.constants'
+import { PermissionAction } from '@supabase/shared-types/out/constants'
export type StorageSettingsProps = {
projectRef: string | undefined
@@ -42,6 +44,8 @@ const StorageConfig = ({ config, projectRef }: any) => {
const [selectedUnit, setSelectedUnit] = useState(unit)
let initialValues = { fileSizeLimit: value, unformattedFileSizeLimit: fileSizeLimit }
+ const canUpdateStorageSettings = checkPermissions(PermissionAction.UPDATE, 'projects')
+
const formattedMaxSizeBytes = `${new Intl.NumberFormat('en-US').format(
STORAGE_FILE_SIZE_LIMIT_MAX_BYTES
)} bytes`
@@ -138,7 +142,7 @@ const StorageConfig = ({ config, projectRef }: any) => {
id="fileSizeLimit"
name="fileSizeLimit"
type="number"
- disabled={isFreeTier}
+ disabled={isFreeTier || !canUpdateStorageSettings}
step={1}
onKeyPress={(event) => {
if (event.charCode < 48 || event.charCode > 57) {
@@ -184,13 +188,20 @@ const StorageConfig = ({ config, projectRef }: any) => {
icon={ }
primaryText="Free Plan has a fixed upload file size limit of 50 MB."
projectRef={projectRef}
- secondaryText="Please upgrade to Pro plan for a configurable upload file size limit of up to 5 GB."
+ secondaryText="Upgrade to the Pro plan for a configurable upload file size limit of up to 5 GB."
/>
)}
-
+
+ {!canUpdateStorageSettings ? (
+
+ You need additional permissions to update storage settings
+
+ ) : (
+
+ )}
{
type="primary"
htmlType="submit"
loading={isSubmitting}
- disabled={!hasChanges && hasChanges !== undefined}
+ disabled={
+ !canUpdateStorageSettings || (!hasChanges && hasChanges !== undefined)
+ }
>
Save
diff --git a/studio/components/to-be-cleaned/forms/SchemaForm.js b/studio/components/to-be-cleaned/forms/SchemaForm.js
index bc5e6ac20a0..fa65c7b1560 100644
--- a/studio/components/to-be-cleaned/forms/SchemaForm.js
+++ b/studio/components/to-be-cleaned/forms/SchemaForm.js
@@ -2,10 +2,19 @@ import { AutoForm } from 'uniforms-bootstrap4'
import Ajv from 'ajv'
import { JSONSchemaBridge } from 'uniforms-bridge-json-schema'
-const SchemaForm = ({ schema, model, children, formRef, onSubmit, onChangeModel = () => {} }) => {
+const SchemaForm = ({
+ schema,
+ model,
+ disabled = false,
+ children,
+ formRef,
+ onSubmit,
+ onChangeModel = () => {},
+}) => {
const validatedSchema = new JSONSchemaBridge(schema, createValidator(schema))
return (
{title || ''}
-
-
- {cancelLabel || 'Cancel'}
-
-
{disabledMessage}
+ ) : (
+
- {submitLabel || 'Save'}
-
-
+
+ {cancelLabel || 'Cancel'}
+
+
+ {submitLabel || 'Save'}
+
+
+ )}
{message}
{
setHasChanged(true)
if (onChangeModel) onChangeModel(model)
diff --git a/studio/components/ui/Flag/FlagProvider.tsx b/studio/components/ui/Flag/FlagProvider.tsx
index e521fec1a65..40d1ac7dae8 100644
--- a/studio/components/ui/Flag/FlagProvider.tsx
+++ b/studio/components/ui/Flag/FlagProvider.tsx
@@ -16,20 +16,12 @@ const FlagProvider: FC = ({ children }) => {
const { Provider } = FlagContext
const [store, setStore] = useState({})
- useEffect(() => {
- // [Joshen] getFlags get triggered everytime the tab refocuses but this should be okay
- // as per https://configcat.com/docs/sdk-reference/js/#polling-modes:
- // The polling downloads the config.json at the set interval and are stored in the internal cache
- // which subsequently all getValueAsync() calls are served from there
- if (IS_PLATFORM) getFlags(profile)
- }, [profile])
-
const getFlags = async (user?: User) => {
if (!client) {
client = configcat.getClient(
process.env.NEXT_PUBLIC_CONFIGCAT_SDK_KEY ?? '',
configcat.PollingMode.AutoPoll,
- { pollIntervalSeconds: 10 }
+ { pollIntervalSeconds: 600 }
)
}
@@ -44,6 +36,14 @@ const FlagProvider: FC = ({ children }) => {
setStore(flagStore)
}
+ useEffect(() => {
+ // [Joshen] getFlags get triggered everytime the tab refocuses but this should be okay
+ // as per https://configcat.com/docs/sdk-reference/js/#polling-modes:
+ // The polling downloads the config.json at the set interval and are stored in the internal cache
+ // which subsequently all getValueAsync() calls are served from there
+ if (IS_PLATFORM) getFlags(profile)
+ }, [profile])
+
return {children}
}
diff --git a/studio/components/ui/UpgradeToPro.tsx b/studio/components/ui/UpgradeToPro.tsx
index ca7b1b549b6..ccee10b1295 100644
--- a/studio/components/ui/UpgradeToPro.tsx
+++ b/studio/components/ui/UpgradeToPro.tsx
@@ -2,8 +2,11 @@ import { Button } from 'ui'
import { observer } from 'mobx-react-lite'
import Link from 'next/link'
import { FC, ReactNode } from 'react'
-import { useStore } from 'hooks'
+import * as Tooltip from '@radix-ui/react-tooltip'
+
+import { checkPermissions, useStore, useFlag, useParams } from 'hooks'
import { PRICING_TIER_PRODUCT_IDS } from 'lib/constants'
+import { PermissionAction } from '@supabase/shared-types/out/constants'
interface Props {
icon?: ReactNode
@@ -14,8 +17,16 @@ interface Props {
const UpgradeToPro: FC = ({ icon, primaryText, projectRef, secondaryText }) => {
const { ui } = useStore()
+ const { ref } = useParams()
const tier = ui.selectedProject?.subscription_tier
+ const canUpdateSubscription = checkPermissions(
+ PermissionAction.BILLING_WRITE,
+ 'stripe.subscriptions'
+ )
+ const projectUpdateDisabled = useFlag('disableProjectCreationAndUpdate')
+ const isEnterprise = tier === PRICING_TIER_PRODUCT_IDS.ENTERPRISE
+
return (
= ({ icon, primaryText, projectRef, secondaryText
{secondaryText}
-
-
-
- {tier === PRICING_TIER_PRODUCT_IDS.FREE ? 'Upgrade to Pro' : 'Modify subscription'}
+
+
+
+
+
+ {tier === PRICING_TIER_PRODUCT_IDS.FREE
+ ? 'Upgrade to Pro'
+ : 'Modify subscription'}
+
+
-
-
+
+ {!canUpdateSubscription || projectUpdateDisabled ? (
+
+
+
+
+ {projectUpdateDisabled ? (
+ <>
+ Subscription changes are currently disabled.
+
+ Our engineers are working on a fix.
+ >
+ ) : !canUpdateSubscription ? (
+ 'You need additional permissions to amend subscriptions'
+ ) : (
+ ''
+ )}
+
+
+
+ ) : (
+ <>>
+ )}
+
diff --git a/studio/data/edge-functions/edge-functions-delete-mutation.ts b/studio/data/edge-functions/edge-functions-delete-mutation.ts
new file mode 100644
index 00000000000..d642758cd9c
--- /dev/null
+++ b/studio/data/edge-functions/edge-functions-delete-mutation.ts
@@ -0,0 +1,46 @@
+import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query'
+import { delete_ } from 'lib/common/fetch'
+import { API_ADMIN_URL } from 'lib/constants'
+import { edgeFunctionsKeys } from './keys'
+
+export type EdgeFunctionsDeleteVariables = {
+ projectRef: string
+ slug: string
+}
+
+export async function deleteEdgeFunction({ projectRef, slug }: EdgeFunctionsDeleteVariables) {
+ if (!projectRef) throw new Error('projectRef is required')
+
+ const response = await delete_(`${API_ADMIN_URL}/projects/${projectRef}/functions/${slug}`, {})
+ if (response.error) {
+ throw response.error
+ }
+
+ return response
+}
+
+type EdgeFunctionsDeleteData = Awaited>
+
+export const useEdgeFunctionDeleteMutation = ({
+ onSuccess,
+ ...options
+}: Omit<
+ UseMutationOptions,
+ 'mutationFn'
+> = {}) => {
+ const queryClient = useQueryClient()
+
+ return useMutation(
+ (vars) => deleteEdgeFunction(vars),
+ {
+ async onSuccess(data, variables, context) {
+ const { projectRef } = variables
+ await queryClient.invalidateQueries(edgeFunctionsKeys.list(projectRef), {
+ refetchType: 'all',
+ })
+ await onSuccess?.(data, variables, context)
+ },
+ ...options,
+ }
+ )
+}
diff --git a/studio/data/edge-functions/edge-functions-query.ts b/studio/data/edge-functions/edge-functions-query.ts
new file mode 100644
index 00000000000..28bc3f8c649
--- /dev/null
+++ b/studio/data/edge-functions/edge-functions-query.ts
@@ -0,0 +1,57 @@
+import { useQuery, useQueryClient, UseQueryOptions } from '@tanstack/react-query'
+import { get } from 'lib/common/fetch'
+import { API_ADMIN_URL } from 'lib/constants'
+import { useCallback } from 'react'
+import { edgeFunctionsKeys } from './keys'
+
+export type EdgeFunctionsVariables = { projectRef?: string }
+
+export type EdgeFunctionsResponse = {
+ id: string
+ name: string
+ slug: string
+ status: string
+ version: number
+ created_at: number
+ updated_at: number
+ verify_jwt: boolean
+ import_map: boolean
+}
+
+export async function getEdgeFunctions(
+ { projectRef }: EdgeFunctionsVariables,
+ signal?: AbortSignal
+) {
+ if (!projectRef) throw new Error('projectRef is required')
+
+ const response = await get(`${API_ADMIN_URL}/projects/${projectRef}/functions`, {
+ signal,
+ })
+ if (response.error) throw response.error
+ return response as EdgeFunctionsResponse[]
+}
+
+export type EdgeFunctionsData = Awaited>
+export type EdgeFunctionsError = unknown
+
+export const useEdgeFunctionsQuery = (
+ { projectRef }: EdgeFunctionsVariables,
+ { enabled = true, ...options }: UseQueryOptions = {}
+) =>
+ useQuery(
+ edgeFunctionsKeys.list(projectRef),
+ ({ signal }) => getEdgeFunctions({ projectRef }, signal),
+ { enabled: enabled && typeof projectRef !== 'undefined', ...options }
+ )
+
+export const useEdgeFunctionsPrefetch = ({ projectRef }: EdgeFunctionsVariables) => {
+ const client = useQueryClient()
+
+ return useCallback(() => {
+ if (projectRef) {
+ client.prefetchQuery(edgeFunctionsKeys.list(projectRef), ({ signal }) =>
+ getEdgeFunctions({ projectRef }, signal)
+ )
+ }
+ }, [projectRef])
+}
diff --git a/studio/data/edge-functions/keys.ts b/studio/data/edge-functions/keys.ts
new file mode 100644
index 00000000000..dacd8d4a8dd
--- /dev/null
+++ b/studio/data/edge-functions/keys.ts
@@ -0,0 +1,3 @@
+export const edgeFunctionsKeys = {
+ list: (projectRef: string | undefined) => ['projects', projectRef, 'edge-functions'] as const,
+}
diff --git a/studio/package.json b/studio/package.json
index db300011bdd..c135a48e5ea 100644
--- a/studio/package.json
+++ b/studio/package.json
@@ -15,6 +15,8 @@
"prettier:write": "prettier --write ."
},
"dependencies": {
+ "@graphiql/react": "^0.15.0",
+ "@graphiql/toolkit": "^0.8.0",
"@hcaptcha/react-hcaptcha": "^1.4.4",
"@headlessui/react": "^1.4.1",
"@monaco-editor/react": "^4.3.1",
@@ -37,12 +39,16 @@
"awesome-debounce-promise": "^2.1.0",
"blueimp-md5": "^2.19.0",
"clipboard": "^2.0.8",
+ "clsx": "^1.2.1",
"common": "*",
"config": "*",
"configcat-js": "^7.0.0",
"dayjs": "^1.11.0",
"file-saver": "^2.0.5",
"generate-password": "^1.7.0",
+ "graphiql": "^2.2.0",
+ "graphql": "^16.6.0",
+ "graphql-ws": "^5.11.3",
"html-to-image": "^1.10.8",
"immutability-helper": "^3.1.1",
"ip-address": "^8.1.0",
diff --git a/studio/pages/_app.tsx b/studio/pages/_app.tsx
index d97675d69ef..f542d30ca75 100644
--- a/studio/pages/_app.tsx
+++ b/studio/pages/_app.tsx
@@ -16,6 +16,7 @@ import dayjs from 'dayjs'
import customParseFormat from 'dayjs/plugin/customParseFormat'
import timezone from 'dayjs/plugin/timezone'
import utc from 'dayjs/plugin/utc'
+import relativeTime from 'dayjs/plugin/relativeTime'
// @ts-ignore
import Prism from 'prism-react-renderer/prism'
@@ -44,6 +45,7 @@ import useAutoAuthRedirect from 'hooks/misc/useAutoAuthRedirect'
dayjs.extend(customParseFormat)
dayjs.extend(utc)
dayjs.extend(timezone)
+dayjs.extend(relativeTime)
dart(Prism)
diff --git a/studio/pages/project/[ref]/api/graphiql.tsx b/studio/pages/project/[ref]/api/graphiql.tsx
new file mode 100644
index 00000000000..8b449994aee
--- /dev/null
+++ b/studio/pages/project/[ref]/api/graphiql.tsx
@@ -0,0 +1,88 @@
+import { observer } from 'mobx-react-lite'
+import { createGraphiQLFetcher } from '@graphiql/toolkit'
+
+import ExtensionCard from 'components/interfaces/Database/Extensions/ExtensionCard'
+import GraphiQL from 'components/interfaces/GraphQL/GraphiQL'
+import { DocsLayout } from 'components/layouts'
+import Connecting from 'components/ui/Loading/Loading'
+import { useParams, useStore } from 'hooks'
+import { useProjectApiQuery } from 'data/config/project-api-query'
+import { useEffect, useMemo } from 'react'
+import { NextPageWithLayout } from 'types'
+import { IconAlertCircle } from 'ui'
+
+const GraphiQLPage: NextPageWithLayout = () => {
+ const { ref: projectRef } = useParams()
+ const { ui, meta } = useStore()
+
+ const isExtensionsLoading = meta.extensions.isLoading
+ const pgGraphqlExtension = meta.extensions.byId('pg_graphql')
+
+ useEffect(() => {
+ if (ui.selectedProject?.ref) {
+ // Schemas may be needed when enabling the GraphQL extension
+ meta.schemas.load()
+ meta.extensions.load()
+ }
+ }, [ui.selectedProject?.ref])
+
+ const {
+ data: settings,
+ isError: isProjectSettingsError,
+ isLoading: isProjectSettingsLoading,
+ } = useProjectApiQuery({
+ projectRef,
+ })
+
+ const apiService = settings?.autoApiService
+ const anonKey = apiService?.service_api_keys.find((x) => x.name === 'anon key')
+ ? apiService.defaultApiKey
+ : undefined
+ const endpoint = settings?.autoApiService.app_config.endpoint ?? ''
+
+ const graphqlUrl = `https://${endpoint}/graphql/v1`
+
+ const fetcher = useMemo(() => {
+ return createGraphiQLFetcher({
+ url: graphqlUrl,
+ fetch,
+ })
+ }, [graphqlUrl])
+
+ if (isProjectSettingsLoading || (isExtensionsLoading && !pgGraphqlExtension)) {
+ return
+ }
+
+ if (isProjectSettingsError) {
+ return (
+
+
+
Failed to retrieve API keys
+
+ )
+ }
+
+ if (pgGraphqlExtension?.installed_version === null) {
+ return (
+
+
+
+
Enable the GraphQL Extension
+
+ Toggle the switch below to enable the GraphQL extension. You can then use the GraphQL
+ API with your Supabase Database.
+
+
+
+
+
+
+ )
+ }
+
+ return
+}
+
+GraphiQLPage.getLayout = (page) => {page}
+
+export default observer(GraphiQLPage)
diff --git a/studio/pages/project/[ref]/database/backups/pitr.tsx b/studio/pages/project/[ref]/database/backups/pitr.tsx
index 5f3eb4aadad..694e62f97c7 100644
--- a/studio/pages/project/[ref]/database/backups/pitr.tsx
+++ b/studio/pages/project/[ref]/database/backups/pitr.tsx
@@ -65,7 +65,7 @@ const PITR = () => {
primaryText="Point in time recovery is a Pro plan add-on."
secondaryText={
tier === PRICING_TIER_PRODUCT_IDS.FREE
- ? 'Please upgrade to the Pro plan with the PITR add-on selected to enable point in time recovery for your project.'
+ ? 'Upgrade to the Pro plan with the PITR add-on selected to enable point in time recovery for your project.'
: 'Please enable the add-on to enable point in time recovery for your project.'
}
/>
diff --git a/studio/pages/project/[ref]/database/wrappers/[id].tsx b/studio/pages/project/[ref]/database/wrappers/[id].tsx
index a5abeb5df61..bf197056957 100644
--- a/studio/pages/project/[ref]/database/wrappers/[id].tsx
+++ b/studio/pages/project/[ref]/database/wrappers/[id].tsx
@@ -9,6 +9,7 @@ import { EditWrapper } from 'components/interfaces/Database'
const DatabaseWrappersNew: NextPageWithLayout = () => {
const canReadWrappers = checkPermissions(PermissionAction.TENANT_SQL_ADMIN_READ, 'wrappers')
+
if (!canReadWrappers) {
return
}
diff --git a/studio/pages/project/[ref]/database/wrappers/index.tsx b/studio/pages/project/[ref]/database/wrappers/index.tsx
index ead5fba1e25..d8df5feae34 100644
--- a/studio/pages/project/[ref]/database/wrappers/index.tsx
+++ b/studio/pages/project/[ref]/database/wrappers/index.tsx
@@ -9,7 +9,7 @@ import NoPermission from 'components/ui/NoPermission'
import { FormsContainer } from 'components/ui/Forms'
const DatabaseWrappers: NextPageWithLayout = () => {
- const canReadWrappers = checkPermissions(PermissionAction.TENANT_SQL_ADMIN_READ, 'wrappers')
+ const canReadWrappers = checkPermissions(PermissionAction.TENANT_SQL_ADMIN_READ, 'tables')
if (!canReadWrappers) {
return
}
diff --git a/studio/pages/project/[ref]/database/wrappers/new.tsx b/studio/pages/project/[ref]/database/wrappers/new.tsx
index 4db0245131f..225b4051123 100644
--- a/studio/pages/project/[ref]/database/wrappers/new.tsx
+++ b/studio/pages/project/[ref]/database/wrappers/new.tsx
@@ -8,9 +8,9 @@ import NoPermission from 'components/ui/NoPermission'
import { CreateWrapper } from 'components/interfaces/Database'
const DatabaseWrappersNew: NextPageWithLayout = () => {
- const canReadWrappers = checkPermissions(PermissionAction.TENANT_SQL_ADMIN_READ, 'wrappers')
- if (!canReadWrappers) {
- return
+ const canCreateWrappers = checkPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'tables')
+ if (!canCreateWrappers) {
+ return
}
return
diff --git a/studio/pages/project/[ref]/functions/[id]/details.tsx b/studio/pages/project/[ref]/functions/[id]/details.tsx
index 613833e4f31..debcd5ebe99 100644
--- a/studio/pages/project/[ref]/functions/[id]/details.tsx
+++ b/studio/pages/project/[ref]/functions/[id]/details.tsx
@@ -1,244 +1,21 @@
-import dayjs from 'dayjs'
-import { useEffect, useState } from 'react'
import { observer } from 'mobx-react-lite'
-import { IconGlobe, IconTerminal } from 'ui'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { NextPageWithLayout } from 'types'
-import { checkPermissions, useParams, useStore } from 'hooks'
-import { useProjectApiQuery } from 'data/config/project-api-query'
+import { checkPermissions, useParams } from 'hooks'
import FunctionsLayout from 'components/layouts/FunctionsLayout'
-import CommandRender from 'components/interfaces/Functions/CommandRender'
import NoPermission from 'components/ui/NoPermission'
+import { EdgeFunctionDetails } from 'components/interfaces/Functions'
const PageLayout: NextPageWithLayout = () => {
- const { ref: projectRef, id } = useParams()
-
- const { functions, ui } = useStore()
-
- const [selectedFunction, setSelectedFunction] = useState(null)
-
- useEffect(() => {
- setSelectedFunction(functions.byId(id))
- }, [functions.isLoaded, ui.selectedProject])
-
- const { data: settings } = useProjectApiQuery({
- projectRef,
- })
-
- // get the .co or .net TLD from the restUrl
- const restUrl = ui.selectedProject?.restUrl
- const restUrlTld = new URL(restUrl as string).hostname.split('.').pop()
-
- const managementCommands: any = [
- {
- command: `supabase functions deploy ${selectedFunction?.slug}`,
- description: 'This will overwrite the deployed function with your new function',
- jsx: () => {
- return (
- <>
- supabase functions deploy{' '}
- {selectedFunction?.slug}
- >
- )
- },
- comment: 'Deploy a new version',
- },
- {
- command: `supabase functions delete ${selectedFunction?.slug}`,
- description: 'This will remove the function and all the logs associated with it',
- jsx: () => {
- return (
- <>
- supabase functions delete{' '}
- {selectedFunction?.slug}
- >
- )
- },
- comment: 'Delete the function',
- },
- ]
-
- const secretCommands: any = [
- {
- command: `supabase secrets list`,
- description: 'This will list all the secrets for your project',
- jsx: () => {
- return (
- <>
- supabase secrets list
- >
- )
- },
- comment: 'View all secrets',
- },
- {
- command: `supabase secrets set NAME1=VALUE1 NAME2=VALUE2`,
- description: 'This will set secrets for your project',
- jsx: () => {
- return (
- <>
- supabase secrets set NAME1=VALUE1 NAME2=VALUE2
- >
- )
- },
- comment: 'Set secrets for your project',
- },
- {
- command: `supabase secrets unset NAME1 NAME2 `,
- description: 'This will delete secrets for your project',
- jsx: () => {
- return (
- <>
- supabase secrets unset NAME1 NAME2
- >
- )
- },
- comment: 'Unset secrets for your project',
- },
- ]
-
- // Get the API service
- const apiService = settings?.autoApiService
- const anonKey = apiService?.service_api_keys.find((x) => x.name === 'anon key')
- ? apiService.defaultApiKey
- : undefined
-
- const endpoint = apiService?.app_config.endpoint ?? ''
- const endpointSections = endpoint.split('.')
- const functionsEndpoint = [
- ...endpointSections.slice(0, 1),
- 'functions',
- ...endpointSections.slice(1),
- ].join('.')
-
- const invokeCommands: any = [
- {
- command: `curl -L -X POST 'https://${projectRef}.functions.supabase.${restUrlTld}/${
- selectedFunction?.slug
- }' -H 'Authorization: Bearer ${anonKey ?? '[YOUR ANON KEY]'}' --data '{"name":"Functions"}'`,
- description: 'Invokes the hello function',
- jsx: () => {
- return (
- <>
- curl -L -X POST 'https://{functionsEndpoint}/
- {selectedFunction?.slug}' -H 'Authorization: Bearer [YOUR ANON KEY]'{' '}
- {`--data '{"name":"Functions"}'`}
- >
- )
- },
- comment: 'Invoke your function',
- },
- ]
+ const { id } = useParams()
const canReadFunction = checkPermissions(PermissionAction.FUNCTIONS_READ, id as string)
if (!canReadFunction) {
return
}
- return (
-
-
-
-
-
-
Function Name
-
{selectedFunction?.name}
-
-
-
-
Status
-
-
-
- {selectedFunction?.status}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Endpoint URL
-
- {`https://${projectRef}.functions.supabase.co/${selectedFunction?.slug}`}
-
-
-
-
-
Created At
-
- {selectedFunction?.created_at &&
- dayjs(selectedFunction.created_at).format('dddd, MMMM D, YYYY h:mm A')}
-
-
-
-
-
Updated At
-
- {selectedFunction?.updated_at &&
- dayjs(selectedFunction.updated_at).format('dddd, MMMM D, YYYY h:mm A')}
-
-
-
-
-
Version
-
v{selectedFunction?.version}
-
-
-
-
Regions
-
-
-
- Earth
-
-
All functions are deployed globally
-
-
-
-
-
-
-
-
-
-
-
-
Command line access
-
-
-
Deployment management
-
-
Invoke
-
-
Secrets management
-
-
-
-
- )
+ return
}
PageLayout.getLayout = (page) => {page}
diff --git a/studio/pages/project/[ref]/functions/index.tsx b/studio/pages/project/[ref]/functions/index.tsx
index 0532afd7a8f..e78b41e0996 100644
--- a/studio/pages/project/[ref]/functions/index.tsx
+++ b/studio/pages/project/[ref]/functions/index.tsx
@@ -1,59 +1,53 @@
import { observer } from 'mobx-react-lite'
+import { useParams } from 'hooks'
import { NextPageWithLayout } from 'types'
-import { useStore } from 'hooks'
-import FunctionsLayout from 'components/layouts/FunctionsLayout'
-
import Table from 'components/to-be-cleaned/Table'
-import { Function } from 'components/interfaces/Functions/Functions.types'
-import {
- FunctionsListItem,
- FunctionsEmptyState,
- TerminalInstructions,
-} from 'components/interfaces/Functions'
-
-const EdgeFunctionsList = ({ functions }: { functions: Function[] }) => {
- return (
- <>
-
-
- {`${functions.length} function${
- functions.length > 1 ? 's' : ''
- } deployed`}
-
-
-
- Name
- URL
- Created
- Last updated
- Version
- Status
- >
- }
- body={
- <>
- {functions.length > 0 &&
- functions.map((item: any) => )}
- >
- }
- />
-
-
- >
- )
-}
+import FunctionsLayout from 'components/layouts/FunctionsLayout'
+import { EdgeFunctionsListItem, FunctionsEmptyState } from 'components/interfaces/Functions'
+import ShimmeringLoader from 'components/ui/ShimmeringLoader'
+import { useEdgeFunctionsQuery } from 'data/edge-functions/edge-functions-query'
const PageLayout: NextPageWithLayout = () => {
- const { functions } = useStore()
- const hasFunctions = functions.list().length > 0
+ const { ref } = useParams()
+ const { isLoading, data: functions } = useEdgeFunctionsQuery({ projectRef: ref })
+
+ const hasFunctions = (functions ?? []).length > 0
+
+ if (isLoading || functions === undefined) {
+ return (
+
+
+
+
+
+ )
+ }
return hasFunctions ? (
-
-
-
+
+
+ {`${functions.length} function${
+ functions.length > 1 ? 's' : ''
+ } deployed`}
+
+
+ Name
+ URL
+ Created
+ Last updated
+ Deployments
+ >
+ }
+ body={
+ <>
+ {functions.length > 0 &&
+ functions.map((item: any) => )}
+ >
+ }
+ />
) : (
diff --git a/studio/pages/project/[ref]/reports/database.tsx b/studio/pages/project/[ref]/reports/database.tsx
index 20ba14361d2..51ff8dfb946 100644
--- a/studio/pages/project/[ref]/reports/database.tsx
+++ b/studio/pages/project/[ref]/reports/database.tsx
@@ -103,24 +103,25 @@ const DatabaseUsage: FC = () => {
/>
-
-
- {usage?.disk_volume_size_gb && (
-
Disk Size: {usage.disk_volume_size_gb} GB
- )}
+ {isPaidTier && (
+
+
+ {usage?.disk_volume_size_gb && (
+ Disk Size: {usage.disk_volume_size_gb} GB
+ )}
+ Auto-Scaling
+
- {isPaidTier &&
Auto-Scaling }
+
}>
+
+ What is disk size?
+
+
-
-
}>
-
- What is disk size?
-
-
-
+ )}
diff --git a/studio/pages/project/[ref]/settings/billing/subscription.tsx b/studio/pages/project/[ref]/settings/billing/subscription.tsx
index 750af452736..a47a62ad90d 100644
--- a/studio/pages/project/[ref]/settings/billing/subscription.tsx
+++ b/studio/pages/project/[ref]/settings/billing/subscription.tsx
@@ -36,7 +36,6 @@ interface SettingsProps {
const Settings: FC
= ({ project }) => {
const { ui } = useStore()
- const projectTier = ui.selectedProject?.subscription_tier
const {
data: subscription,
diff --git a/studio/pages/project/[ref]/storage/buckets/index.tsx b/studio/pages/project/[ref]/storage/buckets/index.tsx
index 80ca79f9f03..396a914ba2a 100644
--- a/studio/pages/project/[ref]/storage/buckets/index.tsx
+++ b/studio/pages/project/[ref]/storage/buckets/index.tsx
@@ -3,21 +3,21 @@ import { useRouter } from 'next/router'
import { observer } from 'mobx-react-lite'
import { API_URL } from 'lib/constants'
-import { useFlag, useStore } from 'hooks'
+import { useFlag, useParams, useStore } from 'hooks'
import { post } from 'lib/common/fetch'
import { PROJECT_STATUS } from 'lib/constants'
import { StorageLayout } from 'components/layouts'
import ProductEmptyState from 'components/to-be-cleaned/ProductEmptyState'
import { useStorageStore } from 'localStores/storageExplorer/StorageExplorerStore'
import { NextPageWithLayout } from 'types'
+import { useProjectApiQuery } from 'data/config/project-api-query'
+import { find } from 'lodash'
/**
* PageLayout is used to setup layout - as usual it will requires inject global store
*/
const PageLayout: NextPageWithLayout = ({}) => {
- const router = useRouter()
- const { ref } = router.query
-
+ const { ref } = useParams()
const { ui } = useStore()
const project = ui.selectedProject
@@ -26,6 +26,11 @@ const PageLayout: NextPageWithLayout = ({}) => {
const kpsEnabled = useFlag('initWithKps')
+ const { data: settings, isLoading } = useProjectApiQuery({ projectRef: ref })
+ const apiService = settings?.autoApiService
+ const serviceKey = find(apiService?.service_api_keys ?? [], (key) => key.tags === 'service_role')
+ const canAccessStorage = !isLoading && apiService && serviceKey !== undefined
+
useEffect(() => {
if (project && project.status === PROJECT_STATUS.INACTIVE) {
post(`${API_URL}/projects/${ref}/restore`, { kps_enabled: kpsEnabled })
@@ -42,6 +47,8 @@ const PageLayout: NextPageWithLayout = ({}) => {
infoButtonLabel="About storage"
infoButtonUrl="https://supabase.com/docs/guides/storage"
onClickCta={openCreateBucketModal}
+ disabled={!canAccessStorage}
+ disabledMessage="You need additional permissions to create buckets"
>
Create buckets to store and serve any type of digital content.
diff --git a/studio/public/img/graphql.svg b/studio/public/img/graphql.svg
new file mode 100644
index 00000000000..8060db470ea
--- /dev/null
+++ b/studio/public/img/graphql.svg
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/studio/sentry.client.config.js b/studio/sentry.client.config.js
index 456f62c88a0..23e597a09cb 100644
--- a/studio/sentry.client.config.js
+++ b/studio/sentry.client.config.js
@@ -39,6 +39,9 @@ Sentry.init({
// Safe to ignore since it's not a user-facing issue + we've not received any user feedback/report about it
// Ref: https://github.com/supabase/supabase/pull/9729
'The provided `href` (/org/[slug]/general) value is missing query values (slug)',
+ 'The provided `href` (/org/[slug]/team) value is missing query values (slug)',
+ 'The provided `href` (/org/[slug]/billing) value is missing query values (slug)',
+ 'The provided `href` (/org/[slug]/invoices) value is missing query values (slug)',
],
beforeSend: (event) => filterConsoleErrors(event),
})