diff --git a/.github/workflows/og_images.yml b/.github/workflows/og_images.yml index 2e1461461d..74ded0819c 100644 --- a/.github/workflows/og_images.yml +++ b/.github/workflows/og_images.yml @@ -25,4 +25,4 @@ jobs: with: version: 1.0.0 - - run: supabase functions deploy og-images --project-ref $PROJECT_ID + - run: supabase functions deploy og-images --project-ref $PROJECT_ID --no-verify-jwt diff --git a/README.md b/README.md index d6498236c8..fcf1cfe5ab 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ To see how to Contribute, visit [Getting Started](./DEVELOPERS.md) - [x] Alpha: We are testing Supabase with a closed set of customers - [x] Public Alpha: Anyone can sign up over at [app.supabase.com](https://app.supabase.com). But go easy on us, there are a few kinks - [x] Public Beta: Stable enough for most non-enterprise use-cases -- [ ] Public: Production-ready +- [ ] Public: General Availability [[status](https://supabase.com/docs/guides/getting-started/features#feature-status)] We are currently in Public Beta. Watch "releases" of this repo to get notified of major updates. diff --git a/apps/docs/components/Extensions.tsx b/apps/docs/components/Extensions.tsx index a3ab9bff99..b7b7260cd6 100644 --- a/apps/docs/components/Extensions.tsx +++ b/apps/docs/components/Extensions.tsx @@ -94,8 +94,15 @@ export default function Extensions() { filters.length === 0 ? x : x.tags.some((item) => filters.includes(item)) ) .map((extension) => ( - - + +

{extension.comment.charAt(0).toUpperCase() + extension.comment.slice(1)} diff --git a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts index 7385afd2d3..2dcd089bbc 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: [] }, @@ -635,7 +637,7 @@ export const platform = { url: undefined, items: [ { name: 'Access Control', url: '/guides/platform/access-control', items: [] }, - { name: 'Database Usage', url: '/guides/platform/database-usage', items: [] }, + { name: 'Database Size', url: '/guides/platform/database-size', items: [] }, { name: 'HTTP Status Codes', url: '/guides/platform/http-status-codes', items: [] }, { name: 'Logging', url: '/guides/platform/logs', items: [] }, { name: 'Metrics', url: '/guides/platform/metrics', 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 7e52d42483..c55ec3b6f0 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 3e610896b8..2d0b3e9696 100644 --- a/apps/docs/layouts/DefaultLayout.tsx +++ b/apps/docs/layouts/DefaultLayout.tsx @@ -1,34 +1,68 @@ import { MDXProvider } from '@mdx-js/react' +import { NextSeo } from 'next-seo' import Head from 'next/head' +import { useRouter } from 'next/router' import { FC } from 'react' import components from '~/components' import TableOfContents from '~/components/TableOfContents' interface Props { - meta: { title: string; description?: string; hide_table_of_contents?: boolean; video?: string } + meta: { + title: string + description?: string + hide_table_of_contents?: boolean + video?: string + canonical?: string + } children: any toc?: any menuItems: any } const Layout: FC = (props: Props) => { + const { asPath, basePath } = useRouter() + const hasTableOfContents = props.toc !== undefined && props.toc.json.filter((item) => item.lvl !== 1 && item.lvl <= 3).length > 0 + console.log('asPath', asPath) + return ( <> - {props.meta?.title} | Supabase Docs + + {asPath === '/' ? 'Supabase Docs' : `${props.meta?.title} | Supabase Docadsdsds`} + - - - - - - + + + - +

{/*

Tutorials

*/}
diff --git a/apps/docs/layouts/guides/index.tsx b/apps/docs/layouts/guides/index.tsx index 5899d282fc..d6f245d989 100644 --- a/apps/docs/layouts/guides/index.tsx +++ b/apps/docs/layouts/guides/index.tsx @@ -1,5 +1,6 @@ import { MDXProvider } from '@mdx-js/react' import { NextSeo } from 'next-seo' +import Head from 'next/head' import Link from 'next/link' import { useRouter } from 'next/router' import { FC, useEffect, useRef, useState } from 'react' @@ -63,17 +64,27 @@ const Layout: FC = (props) => { const hasTableOfContents = tocList.length > 0 + // page type, ie, Auth, Database, Storage etc const ogPageType = asPath.split('/')[2] + // open graph image url constructor + const ogImageUrl = `https://obuldanrptloktxcffvn.functions.supabase.co/og-images?site=docs${ + ogPageType ? `&type=${ogPageType}` : '' + }&title=${encodeURIComponent(props.meta?.title)}&description=${encodeURIComponent( + props.meta?.description + )}` return ( <> + + {props.meta?.title} | Supabase Docs + + + + + = (props) => { modifiedTime: new Date().toISOString(), authors: ['Supabase'], }, - images: [ - { - url: `https://obuldanrptloktxcffvn.functions.supabase.co/og-images?site=docs${ - ogPageType ? `&type=${ogPageType}` : '' - }&title=${encodeURIComponent(props.meta?.title)}&description=${encodeURIComponent( - props.meta?.description - )}`, - }, - ], }} />
diff --git a/apps/docs/layouts/tutorials/TutorialLayout.tsx b/apps/docs/layouts/tutorials/TutorialLayout.tsx index fc1a2fa8ea..2f58a612cc 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/_app.tsx b/apps/docs/pages/_app.tsx index 4a3f65a568..8c50d49a46 100644 --- a/apps/docs/pages/_app.tsx +++ b/apps/docs/pages/_app.tsx @@ -2,6 +2,7 @@ import { createBrowserSupabaseClient } from '@supabase/auth-helpers-nextjs' import { SessionContextProvider } from '@supabase/auth-helpers-react' import { ThemeProvider } from 'common/Providers' import { DefaultSeo } from 'next-seo' +import Head from 'next/head' import { useRouter } from 'next/router' import { useEffect, useState } from 'react' import { AppPropsWithLayout } from 'types' @@ -9,8 +10,8 @@ import ClippyProvider from '~/components/Clippy/ClippyProvider' import { SearchProvider } from '~/components/DocSearch' import Favicons from '~/components/Favicons' import SiteLayout from '~/layouts/SiteLayout' -import { post } from '~/lib/fetchWrappers' import { IS_PLATFORM } from '~/lib/constants' +import { post } from '~/lib/fetchWrappers' import '../styles/algolia-search.scss' import '../styles/ch.scss' import '../styles/docsearch.scss' @@ -59,34 +60,10 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) { }, [router.events]) const SITE_TITLE = 'Supabase Documentation' - const SITE_DESCRIPTION = 'The open source Firebase alternative.' - const { basePath } = useRouter() return ( <> - {IS_PLATFORM ? ( diff --git a/apps/docs/pages/_document.tsx b/apps/docs/pages/_document.tsx index 98acbb7991..ed9a8a70c1 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 7598037b2d..8d73f79e22 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/auth/managing-user-data.mdx b/apps/docs/pages/guides/auth/managing-user-data.mdx index 462f160b23..8b25736949 100644 --- a/apps/docs/pages/guides/auth/managing-user-data.mdx +++ b/apps/docs/pages/guides/auth/managing-user-data.mdx @@ -38,6 +38,12 @@ Primary keys are **guaranteed not to change**. Columns, indices, constraints or You may delete users directly or via the management console at Authentication > Users. Note that deleting a user from the `auth.users` table does not automatically sign out a user. As Supabase makes use of JSON Web Tokens (JWT), a user's JWT will remain "valid" until it has expired. Should you wish to immediately revoke access for a user, do considering making use of a Row Level Security policy as described below. + +You cannot delete a user if they are the owner of any objects in Supabase Storage. + +You will encounter an error when you try to delete an Auth user that owns any Storage objects. If this happens, try deleting all the objects for that user, or reassign ownership to another user. + + ## Public access Since Row Level Security is enabled, this table is accessible via the API but no data will be returned unless we set up some Policies. diff --git a/apps/docs/pages/guides/auth/row-level-security.mdx b/apps/docs/pages/guides/auth/row-level-security.mdx index 1be866869e..473bbb154d 100644 --- a/apps/docs/pages/guides/auth/row-level-security.mdx +++ b/apps/docs/pages/guides/auth/row-level-security.mdx @@ -329,6 +329,129 @@ Tip: Make sure to enable RLS for all your tables, so that your tables are inacce Supabase provides special "Service" keys, which can be used to bypass all RLS. These should never be used in the browser or exposed to customers, but they are useful for administrative tasks. +### Testing policies + +To test policies on the database itself (i.e., from the [SQL Editor](https://app.supabase.com/project/_/sql) or from `psql`) without switching to your frontend and logging in as different users, you can utilize the following helper SQL procedures ([credits](https://github.com/supabase/supabase/issues/7311#issuecomment-1398648114)): + +```sql +grant anon, authenticated to postgres; + +create or replace procedure auth.login_as_user (user_email text) + language plpgsql + as $$ +declare + auth_user auth.users; +begin + select + * into auth_user + from + auth.users + where + email = user_email; + execute format('set request.jwt.claim.sub=%L', (auth_user).id::text); + execute format('set request.jwt.claim.role=%I', (auth_user).role); + execute format('set request.jwt.claim.email=%L', (auth_user).email); + execute format('set request.jwt.claims=%L', json_strip_nulls(json_build_object('app_metadata', (auth_user).raw_app_meta_data))::text); + + raise notice '%', format( 'set role %I; -- logging in as %L (%L)', (auth_user).role, (auth_user).id, (auth_user).email); + execute format('set role %I', (auth_user).role); +end; +$$; + +create or replace procedure auth.login_as_anon () + language plpgsql + as $$ +begin + set request.jwt.claim.sub=''; + set request.jwt.claim.role=''; + set request.jwt.claim.email=''; + set request.jwt.claims=''; + set role anon; +end; +$$; + +create or replace procedure auth.logout () + language plpgsql + as $$ +begin + set request.jwt.claim.sub=''; + set request.jwt.claim.role=''; + set request.jwt.claim.email=''; + set request.jwt.claims=''; + set role postgres; +end; +$$; +``` + +To switch to a given user (by email), use `call auth.login_as_user('my@email.com');`. You can also switch to the `anon` role using `call auth.login_as_anon();`. When you are done, use `call auth.logout();` to return yourself to the `postgres` role. + +These procedures can also be used for writing [pgTAP](/docs/guides/database/extensions/pgtap) unit tests for policies. + +
+ Click here to see an example `psql` interaction using this. + +This example shows that the `public.profiles` table from the tutorial example can indeed be updated by the `postgres` role and the owner of the row but not from `anon` connections: + +```shell +postgres=> select id, email from auth.users; + id | email +--------------------------------------+------------------- + d4f0aa86-e6f6-41d1-bd32-391f077cf1b9 | user1@example.com + 15d6811a-16ee-4fa2-9b18-b63085688be4 | user2@example.com + 4e1010bb-eb37-4a4d-a05a-b0ee315c9d56 | user3@example.com +(3 rows) + +postgres=> table public.profiles; + id | updated_at | username | full_name | avatar_url | website +--------------------------------------+------------+----------+-----------+------------+--------- + d4f0aa86-e6f6-41d1-bd32-391f077cf1b9 | | user1 | User 1 | | + 15d6811a-16ee-4fa2-9b18-b63085688be4 | | user2 | User 2 | | + 4e1010bb-eb37-4a4d-a05a-b0ee315c9d56 | | user3 | User 3 | | +(3 rows) + +postgres=> call auth.login_as_anon(); +CALL +postgres=> update public.profiles set updated_at=now(); +UPDATE 0 -- anon users cannot update any profile but see all of them +postgres=> table public.profiles; + id | updated_at | username | full_name | avatar_url | website +--------------------------------------+------------+----------+-----------+------------+--------- + d4f0aa86-e6f6-41d1-bd32-391f077cf1b9 | | user1 | User 1 | | + 15d6811a-16ee-4fa2-9b18-b63085688be4 | | user2 | User 2 | | + 4e1010bb-eb37-4a4d-a05a-b0ee315c9d56 | | user3 | User 3 | | +(3 rows) + +postgres=> call auth.logout(); +CALL +postgres=> call auth.login_as_user('user1@example.com'); +NOTICE: set role authenticated; -- logging in as 'd4f0aa86-e6f6-41d1-bd32-391f077cf1b9' ('user1@example.com') +CALL +postgres=> update public.profiles set updated_at=now(); +UPDATE 1 -- authenticated users can update their own profile and see all of them +postgres=> table public.profiles; + id | updated_at | username | full_name | avatar_url | website +--------------------------------------+-------------------------------+----------+-----------+------------+--------- + 15d6811a-16ee-4fa2-9b18-b63085688be4 | | user1 | User 1 | | + 4e1010bb-eb37-4a4d-a05a-b0ee315c9d56 | | user2 | User 2 | | + d4f0aa86-e6f6-41d1-bd32-391f077cf1b9 | 2023-02-18 21:39:16.204612+00 | user3 | User 3 | | +(3 rows) + +postgres=> call auth.logout(); +CALL +postgres=> update public.profiles set updated_at=now(); +UPDATE 3 -- the 'postgres' role can update and see all profiles +postgres=> table public.profiles; + id | updated_at | username | full_name | avatar_url | website +--------------------------------------+-------------------------------+----------+-----------+------------+--------- + 15d6811a-16ee-4fa2-9b18-b63085688be4 | 2023-02-18 21:40:08.216324+00 | user1 | User 1 | | + 4e1010bb-eb37-4a4d-a05a-b0ee315c9d56 | 2023-02-18 21:40:08.216324+00 | user2 | User 2 | | + d4f0aa86-e6f6-41d1-bd32-391f077cf1b9 | 2023-02-18 21:40:08.216324+00 | user3 | User 3 | | +(3 rows) + +``` + +
+ ## Deprecated features We have deprecate some functions to ensure better performance and extensibilty of RLS policies. diff --git a/apps/docs/pages/guides/auth/server-side-rendering.mdx b/apps/docs/pages/guides/auth/server-side-rendering.mdx index 635407cd98..346984a63f 100644 --- a/apps/docs/pages/guides/auth/server-side-rendering.mdx +++ b/apps/docs/pages/guides/auth/server-side-rendering.mdx @@ -160,6 +160,12 @@ use this potentially stale information to render a page. ## Frequently Asked Questions +### No session on the server side with Next.js route prefetching? + +When you use route prefetching in Next.js using `` components or the `Router.push()` APIs can send server-side requests before the browser processes the access and refresh tokens. This means that those requests may not have any cookies set and your server code will render unauthenticated content. + +To improve experience for your users, we recommend redirecting users to one specific page after sign-in that does not include any route prefetching from Next.js. Once the Supabase client library running in the browser has obtained the access and refresh tokens from the URL fragment, you can send users to any pages that use prefetching. + ### How do I make the cookies `HttpOnly`? This is not necessary. Both the access token and refresh token are designed to diff --git a/apps/docs/pages/guides/functions.mdx b/apps/docs/pages/guides/functions.mdx index a382f7ccde..b19ddb8187 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 871c16bd6f..29ad150503 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 766da74ae8..7274d810b0 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 ef38552855..7cfcc5feb1 100644 --- a/apps/docs/pages/guides/functions/examples/cloudflare-turnstile.mdx +++ b/apps/docs/pages/guides/functions/examples/cloudflare-turnstile.mdx @@ -16,7 +16,7 @@ export const meta = { >
-[Clouflare Turnstile](https://www.cloudflare.com/products/turnstile/) is a friendly, free CAPTCHA replacement, and it works seamlessly with Supabase Edge Functions to protect your forms. [View on GitHub](https://github.com/supabase/supabase/tree/master/examples/edge-functions/supabase/functions/cloudflare-turnstile). +[Cloudflare Turnstile](https://www.cloudflare.com/products/turnstile/) is a friendly, free CAPTCHA replacement, and it works seamlessly with Supabase Edge Functions to protect your forms. [View on GitHub](https://github.com/supabase/supabase/tree/master/examples/edge-functions/supabase/functions/cloudflare-turnstile). ## Setup @@ -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 3b66a4affd..d99931b58e 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 0000000000..281a2ac0e3 --- /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 efa8eb99e5..be4f4dc46d 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 0000000000..bc24d6a151 --- /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/functions/examples/upstash-redis.mdx b/apps/docs/pages/guides/functions/examples/upstash-redis.mdx index bbe58dd4dd..77411c5b1d 100644 --- a/apps/docs/pages/guides/functions/examples/upstash-redis.mdx +++ b/apps/docs/pages/guides/functions/examples/upstash-redis.mdx @@ -73,7 +73,7 @@ serve(async (_req) => { ```bash supabase start -supabase functions serve upstash-redis-counter --no-verify-jwt --env-file supabase/functions/upstash-redis-counter/.env +supabase functions serve --no-verify-jwt --env-file supabase/functions/upstash-redis-counter/.env ``` Navigate to http://localhost:54321/functions/v1/upstash-redis-counter. diff --git a/apps/docs/pages/guides/functions/local-development.mdx b/apps/docs/pages/guides/functions/local-development.mdx index e342abe9b1..254cad7004 100644 --- a/apps/docs/pages/guides/functions/local-development.mdx +++ b/apps/docs/pages/guides/functions/local-development.mdx @@ -10,7 +10,7 @@ You can run your Edge Function locally using [`supabase functions serve`](/docs/ ```bash supabase start # start the supabase stack -supabase functions serve hello-world # start the Function watcher +supabase functions serve # start the Functions watcher ``` The `functions serve` command has hot-reloading capabilities. It will watch for any changes to your files and restart the Deno server. @@ -20,7 +20,7 @@ The `functions serve` command has hot-reloading capabilities. It will watch for While serving your local Edge Function, you can invoke it using curl: ```bash -curl --request POST 'http://localhost:54321/functions/v1/hello-world' \ +curl --request POST 'http://localhost:54321/functions/v1/function-name' \ --header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24ifQ.625_WdcF3KHqz5amU0x2X5WWHP-OEs_4qj0ssLNHzTs' \ --header 'Content-Type: application/json' \ --data '{ "name":"Functions" }' @@ -37,7 +37,7 @@ const supabase = createClient( 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0' ) -const { data, error } = await supabase.functions.invoke('hello-world', { +const { data, error } = await supabase.functions.invoke('function-name', { body: { name: 'Functions' }, }) ``` diff --git a/apps/docs/pages/guides/functions/secrets.mdx b/apps/docs/pages/guides/functions/secrets.mdx index 3d60a239a8..3487fc94d7 100644 --- a/apps/docs/pages/guides/functions/secrets.mdx +++ b/apps/docs/pages/guides/functions/secrets.mdx @@ -46,7 +46,7 @@ console.log(Deno.env.get('MY_NAME')) Now we can invoke our function locally, by serving it with our new `.env.local` file: ```bash -supabase functions serve hello-world --env-file ./supabase/.env.local +supabase functions serve --env-file ./supabase/.env.local ``` When the function starts you should see the name “Yoda” output to the terminal. diff --git a/apps/docs/pages/guides/getting-started/quickstarts/flutter.mdx b/apps/docs/pages/guides/getting-started/quickstarts/flutter.mdx index 339eb20dd2..476c581349 100644 --- a/apps/docs/pages/guides/getting-started/quickstarts/flutter.mdx +++ b/apps/docs/pages/guides/getting-started/quickstarts/flutter.mdx @@ -176,6 +176,7 @@ export const meta = { Run your app on a platform of your choosing! By default an app should launch in your web browser. Note that `supabase_flutter` is compatible with web, iOS, Android, macOS, and Windows apps. + Running the app on MacOS requires additional configuration to [set the entitlements](https://docs.flutter.dev/development/platform-integration/macos/building#setting-up-entitlements). diff --git a/apps/docs/pages/guides/getting-started/quickstarts/nuxtjs.mdx b/apps/docs/pages/guides/getting-started/quickstarts/nuxtjs.mdx index 366120f35e..f925918592 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 3be4542731..1daf9a881e 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 5480df2d4a..0cbdd921ca 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 () => {

Supabase + Vue 3

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 = () => { - @@ -190,10 +181,7 @@ const Home: NextPage = () => { name="email" placeholder="Email" /> - @@ -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 ![Where to find OneSignal User Auth Key](/docs/img/guides/integrations/onesignal/onesignal-api-key.png) - 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. ![Supabase Webhooks configuration](/docs/img/guides/integrations/onesignal/webhook.png) -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. ![Ordering app UI](/docs/img/guides/integrations/onesignal/app-ui.png) - ## 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/integrations/prisma.mdx b/apps/docs/pages/guides/integrations/prisma.mdx index c2f0d5a8f1..3623a52ec4 100644 --- a/apps/docs/pages/guides/integrations/prisma.mdx +++ b/apps/docs/pages/guides/integrations/prisma.mdx @@ -17,6 +17,7 @@ This guide explains how to quickly connect the Postgres database provided by Sup ## Step 1: Get the connection string from Supabase project settings Go to the settings page from the sidebar and navigate to the **Database** tab. You’ll find the database’s connection string with a placeholder for the password you provided when you created the project. + ![Getting the connection string](/docs/img/guides/integrations/prisma/zntcsh3ic91gf1gy8j73.png) ## Step 2: Testing the connection @@ -29,22 +30,17 @@ In case you don’t have a Prisma project or this is your first time working wit ### Cloning the starter project -Navigate into a directory of your choice and run the following command in your terminal if you’re on a Windows machine: +Navigate into a directory of your choice and run the following command in your terminal: ```bash -curl https://pris.ly/quickstart -L -o quickstart-main.tar.gz && tar -zxvf quickstart-main.tar.gz quickstart-main/typescript/starter && move quickstart-main\typescript\starter starter && rmdir /S /Q quickstart-main && del /Q quickstart-main.tar.gz -``` - -And if you’re using Mac OS or Linux, run the following command: - -```bash -curl -L https://pris.ly/quickstart | tar -xz --strip=2 quickstart-main/typescript/starter +curl https://codeload.github.com/prisma/prisma-examples/tar.gz/latest | tar -xz --strip=2 prisma-examples-latest/databases/postgresql-supabase ``` You can now navigate into the directory and install the project’s dependencies: ```bash -cd starter && npm install +cd postgresql-supabase +npm install ``` ### A look at the project’s structure @@ -52,53 +48,43 @@ cd starter && npm install This project comes with TypeScript configured and has the following structure. - A `prisma` directory which contains: - - A `dev.db` file: This is a SQLite database. - - A `schema.prisma` file: Where we define the different database models and relations between them. -- A `.env` file: Contains the `DATABASE_URL` variable, which Prisma will use. -- A `script.ts` file: where we will run some queries using Prisma Client. - This starter also comes with the following packages installed: + - A `seed.ts` file: This is the data used to seed your database. + - A `schema.prisma` file: Where you define the different database models and relations between them. +- A `script.ts` file: where you will run some queries using Prisma Client. + +This starter also comes with the following packages installed: - [`@prisma/client`](https://www.npmjs.com/package/@prisma/client): An auto-generated and type-safe query builder that’s _tailored_ to your data. - [`prisma`](https://www.npmjs.com/package/prisma): Prisma’s command-line interface (CLI). It allows you to initialize new project assets, generate Prisma Client, and analyze existing database structures through introspection to automatically create your application models. - > Note: Prisma works with both JavaScript and TypeScript. However, to get the best possible development experience, using TypeScript is highly recommended. -### Configuring the project to use PostgreSQL +> Note: Prisma works with both JavaScript and TypeScript. However, to get the best possible development experience, using TypeScript is highly recommended. -By default, Prisma migrations will try to drop the `postgres` database, which can lead to conflicts with Supabase databases. For this scenario, use [Prisma Shadow Databases](https://www.prisma.io/docs/concepts/components/prisma-migrate/shadow-database#cloud-hosted-shadow-databases-must-be-created-manually). +### Configuring the project -Create a shadow database in your PostgreSQL server within the same Supabase project using the `psql` CLI and the `DATABASE_URL` from the previous steps (or use the local database). +Create a `.env` file at the root of your project: ```bash -psql postgresql://postgres:[YOUR-PASSWORD]@db.[YOUR-PROJECT-REF].supabase.co:5432 +touch .env ``` -After you connect to your project's PostgreSQL instance, create another database (e.g., `postgres_shadow`): - -```bash -postgres=> CREATE DATABASE postgres_shadow; -postgres=> exit -``` - -Go ahead and delete the `prisma/dev.db` file because we will be switching to PostgreSQL. -In the `.env` file, update `DATABASE_URL` and `SHADOW_DATABASE_URL` to the connection string from **step 1**. The `.env` file should look like: +In the `.env` file, add a `DATABASE_URL` variable and add the connection string from **step 1**. The `.env` file should look like: ```env # .env DATABASE_URL="postgres://postgres:[YOUR-PASSWORD]@db.[YOUR-PROJECT-REF].supabase.co:5432/postgres" -SHADOW_DATABASE_URL="postgres://postgres:[YOUR-PASSWORD]@db.[YOUR-PROJECT-REF].supabase.co:5432/postgres_shadow" ``` -In the `schema.prisma` file, change the `provider` from "sqlite" to `"postgresql"` and add the `shadowDatabaseUrl` property. This is what your `schema.prisma` file should look like: ```go datasource db { - provider = "postgresql" - url = env("DATABASE_URL") - shadowDatabaseUrl = env("SHADOW_DATABASE_URL") + provider = "postgresql" + url = env("DATABASE_URL") } + generator client { provider = "prisma-client-js" } + model Post { id Int @id @default(autoincrement()) title String @@ -107,6 +93,7 @@ model Post { author User? @relation(fields: [authorId], references: [id]) authorId Int? } + model User { id Int @id @default(autoincrement()) email String @unique @@ -115,53 +102,81 @@ model User { } ``` + To test that everything works correctly, run the following command to create a migration: ```bash -prisma migrate dev --name init +npx prisma migrate dev --name init ``` -You can optionally give your migration a name, depending on the changes you made. Since this is the project’s first migration, you’re setting the `--name` flag to “init”. -If everything works correctly, you should get the following message in your terminal: +You can optionally give your migration a name, depending on the changes you made. Since this is the project’s first migration, you’re setting the `--name` flag to “init”. If everything works correctly, you should get the following message in your terminal: ```text Your database is now in sync with your schema. -:heavy_check_mark: Generated Prisma Client (2.x.x) to ./node_modules/@prisma/client in 111ms +:heavy_check_mark: Generated Prisma Client (4.x.x) to ./node_modules/@prisma/client in 111ms ``` This will create a `prisma/migrations` folder inside your `prisma` directory and synchronize your Prisma schema with your database schema. -> Note: if you want to skip the process of creating a migration history, you can use the [`db push`](https://www.prisma.io/docs/concepts/components/prisma-migrate/db-push) command instead of `migrate dev`. -> If you go to your Supabase project, in the table editor, you should see that two tables have been created, a `Post` and a `User` table. -> ![tables created in the UI](/docs/img/guides/integrations/prisma/7y4qq4wwvfrheti6r09u.png) -> That’s it! You have now successfully connected a Prisma project to a PostgreSQL database hosted on Supabase and ran your first migration. +> **Note**: If you want to skip the process of creating a migration history, you can use the [`prisma db push`](https://www.prisma.io/docs/concepts/components/prisma-migrate/db-push) command instead of `prisma migrate dev`. However, we recommend using `prisma migrate dev` to evolve your database schema in development. +> If you would like to get a conceptual overview of how Prisma Migrate works and which commands to use in what environment, refer to [this page in the Prisma documentation](https://www.prisma.io/docs/concepts/components/prisma-migrate/mental-model). + +If you go to your Supabase project, in the table editor, you should see that two tables have been created, a `Post`, `User`, and `_prisma_migrations` tables. The `_prisma_migrations` table is used to keep + +![tables created in the UI](/docs/img/guides/integrations/prisma/7y4qq4wwvfrheti6r09u.png) + +That’s it! You have now successfully connected a Prisma project to a PostgreSQL database hosted on Supabase and ran your first migration. ## Connection pooling with Supabase -If you’re working in a serverless environment (for example Node.js functions hosted on AWS Lambda, Vercel or Netlify Functions), you need to set up [connection pooling](https://www.prisma.io/docs/guides/performance-and-optimization/connection-management#serverless-environments-faas) using a tool like [PgBouncer](https://www.pgbouncer.org/). That’s because every function invocation may result in a [new connection to the database](https://www.prisma.io/docs/guides/performance-and-optimization/connection-management#the-serverless-challenge). Supabase [supports connection management using PgBouncer](https://supabase.io/blog/2021/04/02/supabase-pgbouncer#what-is-connection-pooling) and are enabled by default. -Go to the **Database** page from the sidebar in the Supabase dashboard and navigate to **connection pool** settings +If you’re working in a serverless environment (for example Node.js functions hosted on AWS Lambda, Vercel or Netlify Functions), you need to set up [connection pooling](https://www.prisma.io/docs/guides/performance-and-optimization/connection-management#serverless-environments-faas) using a tool like [PgBouncer](https://www.pgbouncer.org/). That’s because every function invocation may result in a [new connection to the database](https://www.prisma.io/docs/guides/performance-and-optimization/connection-management#the-serverless-challenge). + +Supabase [supports connection management using PgBouncer](/docs/guides/database/connecting-to-postgres#connection-pool) which prevents a traffic spike from overwhelming your database. + +Go to the **Database** page from the sidebar in the Supabase dashboard and navigate to **Connection pool** settings: + ![Connection pool settings](/docs/img/guides/integrations/prisma/w0oowg8vq435ob5c3gf0.png) -When migrating, you need to use the non-pooled connection URL (like the one used in **step 1**). However, when deploying your app, use the pooled connection URL and add the `?pgbouncer=true` flag to the PostgreSQL connection URL. It's also recommended to minimize the number of concurrent connections by setting the `connection_limit` to `1`. The `.env` file should look like: + +When updating your database schema, you need to use the non-pooled connection URL (like the one used in **step 1**). You can configure the non-pooled connection string by using the `directUrl` property in the datasource block. + +Update your `.env` file with the following changes: +1. Rename the `DATABASE_URL` environment variable to `DIRECT_URL` +1. Create a `DATABASE_URL` environment variable and paste in the new connection string from the dashboard as its value + +Append the `?pgbouncer=true` flag to the `DATABASE_URL` variable. + +Your `.env` file should resemble the following: ```env # .env -DATABASE_URL="postgres://postgres:[YOUR-PASSWORD]@db.[YOUR-PROJECT-REF].supabase.co:6543/postgres?pgbouncer=true&connection_limit=1" -SHADOW_DATABASE_URL="postgres://postgres:[YOUR-PASSWORD]@db.[YOUR-PROJECT-REF].supabase.co:5432/postgres_shadow" +# PostgreSQL connection string used for migrations +DIRECT_URL="postgres://postgres:[YOUR-PASSWORD]@db.[YOUR-PROJECT-REF].supabase.co:5432/postgres" +# PostgreSQL connection string with pgBouncer config — used by Prisma Client +DATABASE_URL="postgres://postgres:[YOUR-PASSWORD]@db.[YOUR-PROJECT-REF].supabase.co:6543/postgres?pgbouncer=true" + ``` -Prisma Migrate uses database transactions to check out the current state of the database and the migrations table. However, the Migration Engine is designed to use a single connection to the database, and does not support connection pooling with PgBouncer. If you attempt to run Prisma Migrate commands in any environment that uses PgBouncer for connection pooling, you might see the following error: +Update your Prisma schema by setting the `directUrl` in the datasource block: -```bash -Error: undefined: Database error -Error querying the database: db error: ERROR: prepared statement “s0” already exists +```go +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") + directURL = env("DIRECT_URL") +} ``` -This is a known issue and it is being worked on, you can follow the progress on this [GitHub issue](https://github.com/prisma/prisma/issues/6485). -If you want to learn more about Prisma, check out the [docs](https://www.prisma.io/docs). Also in case you have any questions or run into any issue, feel free to start a discussion in the repo’s [discussions section](https://github.com/prisma/prisma/discussions). +> **Note**: This feature is available from Prisma version [4.10.0](https://github.com/prisma/prisma/releases/tag/4.10.0) and higher. + +If you want to learn more about Prisma, check out the [docs](https://www.prisma.io/docs/reference/api-reference/prisma-schema-reference#fields). Also in case you have any questions or run into any issue, feel free to start a discussion in the repo’s [discussions section](https://github.com/prisma/prisma/discussions). ## Troubleshooting -If you run `prisma migrate dev --name init` multiple times, it sometimes asks if you want to recreate the whole schema. If you chose yes, it will delete the public schema and recreate it. The default grants are missing after this. If you run into this problem, add a helper SQL for fixing the grants: +### Missing grants + +If your database schema is out of sync from your migration history, `prisma migrate dev` will detect a migration history conflict or a [schema drift](https://www.prisma.io/docs/guides/database/developing-with-prisma-migrate/troubleshooting-development#schema-drift). When `prisma migrate dev` detects the drift, it might ask to to reset your database schema. If you choose yes, it will delete the `public` schema along with the default grants defined in your database. + +If you run into this problem, create a draft migration using `prisma migrate dev --create-only`, and add the following helper SQL: ```sql grant usage on schema public to postgres, anon, authenticated, service_role; @@ -175,6 +190,98 @@ alter default privileges in schema public grant all on functions to postgres, an alter default privileges in schema public grant all on sequences to postgres, anon, authenticated, service_role; ``` +Run `prisma migrate dev` to apply the draft migration to the database. + +### Using Supabase Auth with Prisma + +If you would like to use Supabase Auth and Prisma in your application, you will have to enable the `multiSchema` Preview feature flag in the `generator` block of your Prisma schema: + +```go +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") + directURL = env("DIRECT_URL") +} + +generator client { + provider = "prisma-client-js" + previewFeatures = ["multiSchema"] +} +``` + +Next, specify the database schemas you would like to include in your Prisma schema: + +```go +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") + directURL = env("DIRECT_URL") + schemas = ["public", "auth"] +} + +generator client { + provider = "prisma-client-js" + previewFeatures = ["multiSchema"] +} +``` + +You can then specify what schema a model or enum belongs to using the `@@schema` attribute: + +```go +model User { + id Int @id + // ... + + @@schema("auth") // or @@schema("public") +} +``` + +To learn more about using Prisma with multiple database schemas, refer to [this page in the Prisma docs](https://www.prisma.io/docs/guides/database/multi-schema#learn-more-about-the-multischema-preview-feature). + +### Using PostgreSQL Row Level Security with Prisma + + +If you would like to use Row Level Security (RLS) with Prisma, check out the [Prisma Client Extension - Row Level Security example](https://github.com/prisma/prisma-client-extensions/tree/main/row-level-security) that provides the primitives you could use to build and extend Prisma Client in PostgreSQL. + +Also check out [useSupabaseRowLevelSecurity](https://github.com/dthyresson/prisma-extension-supabase-rls) Prisma Client extension that supports [Supabase RLS](/docs/guides/auth/row-level-security#authrole) and policies written to use [Supabase auth](/docs/guides/auth/overview). + +The example and extension use [Prisma Client extensions](https://www.prisma.io/docs/concepts/components/prisma-client/client-extensions) Preview feature. + +### Enabling PosgreSQL extensions + +If you would like to use a PostgreSQL extension with Prisma, enable the `postgresqlExtensions` Preview feature flag in the `generator` block of your Prisma schema: + +```go +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") + directURL = env("DIRECT_URL") +} + +generator client { + provider = "prisma-client-js" + previewFeatures = ["postgresqlExtensions"] +} +``` + +Next, specify the extensions you need in the `datasource` block: + +```go +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") + directURL = env("DIRECT_URL") + extensions = [hstore(schema: "myHstoreSchema"), pg_trgm, postgis(version: "2.1")] +} + +generator client { + provider = "prisma-client-js" + previewFeatures = ["postgresqlExtensions"] +} +``` + +To learn more about using Prisma with PostgreSQL extensions, refer to [this page in the Prisma docs](https://www.prisma.io/docs/concepts/components/prisma-schema/postgresql-extensions). + ## Resources - [Prisma](https://prisma.io) official website. diff --git a/apps/docs/pages/guides/platform/custom-domains.mdx b/apps/docs/pages/guides/platform/custom-domains.mdx index 558496506d..441ebef2f8 100644 --- a/apps/docs/pages/guides/platform/custom-domains.mdx +++ b/apps/docs/pages/guides/platform/custom-domains.mdx @@ -155,6 +155,7 @@ As with the final activation stage of the process for setting up a vanity subdom - Edge functions do not honor the custom domain or the vanity subdomain setting and they still have to be invoked via the `foobarbaz.supabase.co` domain. - A Supabase project can—at this time—use either a Custom Domain or a Vanity Subdomain, but not both. +- Some authentication flows like Sign-in with Twitter set cookies to track the progress of the flow. Make sure you use only one domain in your frontend application for this reason. Mixing calls to the Supabase domain `foobarbaz.supabase.co` and your custom domain could cause those flows to stop working due to the [Same Origin Policy](https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy) enforced on cookies by the browser. export const Page = ({ children }) => diff --git a/apps/docs/pages/guides/platform/database-size.mdx b/apps/docs/pages/guides/platform/database-size.mdx new file mode 100644 index 0000000000..b827b92683 --- /dev/null +++ b/apps/docs/pages/guides/platform/database-size.mdx @@ -0,0 +1,101 @@ +import Layout from '~/layouts/DefaultGuideLayout' + +export const meta = { + id: 'database-size', + title: 'Database size', + description: 'Understanding how database size applies to your subscription.', +} + +Database size refers to the _monthly average storage usage_, as reported by Postgres. This metric is reported in your project's [billing usage](https://app.supabase.com/project/_/settings/billing/usage) and is updated daily. As you read this document we will refer to "database size" and "disk size": + +- "Database size" is the total size of used storage from your database. +- "Disk size" describes the size of the underlying available storage. + +## Database space management + +### Database size + +This SQL query will show the current size of your Postgres database: + +```sql +select + sum(pg_database_size (pg_database.datname)) / (1024 * 1024) as db_size_mb +from + pg_database; +``` + +This value is reported in the [database settings page](https://app.supabase.com/project/_/settings/database). + +Database Space is consumed primarily by your data, indexes, and materialized views. You can reduce your disk size by removing any of these and running a Vacuum operation. + + + +Depending on your billing tier, your database can go into read-only mode which can prevent you inserting and deleting data. There are instructions for managing read-only mode in the [Disk Management](#disk-management) section. + + + +### Vacuum operations + +Postgres does not immediately reclaim the physical space used by dead tuples (i.e., deleted rows) in the DB. They are marked as "removed" until a [vacuum operation](https://www.postgresql.org/docs/current/routine-vacuuming.html) is executed. As a result, deleting data from your database may not immediately reduce the reported disk usage. + + + +Vacuum operations can temporarily increase resource utilization, which may adversely impact the observed performance of your project until the maintenance is completed. + + + +Supabase projects have automatic vacuuming enabled, which ensures that these operations are performed regularly to keep the database healthy and performant. +It is possible to [fine-tune](https://www.percona.com/blog/2018/08/10/tuning-autovacuum-in-postgresql-and-autovacuum-internals/) +the [autovacuum parameters](https://www.enterprisedb.com/blog/postgresql-vacuum-and-analyze-best-practice-tips), +or [manually initiate](https://www.postgresql.org/docs/current/sql-vacuum.html) vacuum operations. +Running a manual vacuum after deleting large amounts of data from your DB could help reduce the database size reported by Postgres. + +### Preoccupied Space + +New Supabase projects have a database size of ~40-60mb. This space includes pre-installed extensions, schemas, and default Postgres data. Additional database size is used when installing extensions, even if those extensions are inactive. + +## Disk management + +Supabase uses network-attached storage to balance performance with scalability. The behavior of your disk depends on your billing tier. + +### Paid Tier Behavior + +Pro and Enterprise projects have auto-scaling Disk Storage. + +Disk 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 within those 6 hours you reach 95% of the disk space, your project will enter read-only mode. + + + +If you intend to import a lot of data into your database which requires multiple disk expansions then [reach out to our team](https://app.supabase.com/support/new). For example, uploading more than 1.5x the current size of your database storage will put your database into [read-only mode](#read-only-mode). + + + +The maximum Disk Size for Pro Tier is 1024TB. If you need more than this, [contact us](https://app.supabase.com/support/new) to learn more about the Enterprise plan. + +### Free Tier Behavior + +Free Tier projects enter [read-only](#read-only-mode) mode when you exceed the 500mb limit. Once in read-only mode, you have several options: + +- [Upgrade to the Pro or Enterprise tier](https://app.supabase.com/project/_/settings/billing/subscription) to enable auto-scaling and expand beyond the 500mb database size limit. +- [Disable read-only mode](#disabling-read-only-mode) and reduce your database size. + +### Read-only mode + +In some cases Supabase may put your database into read-only mode to prevent your database from exceeding the billing or disk limitations. + +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, + +### Disabling read-only mode + +You can manually override read-only mode to reduce disk size. To do this, run the following in the [SQL Editor](https://app.supabase.com/project/_/sql): + +```sql +SET + default_transaction_read_only = 'off'; +``` + +This allows you to delete data from within the session. After deleting data, you should run a vacuum to reclaim as much space as possible. + +export const Page = ({ children }) => + +export default Page diff --git a/apps/docs/pages/guides/platform/database-usage.mdx b/apps/docs/pages/guides/platform/database-usage.mdx deleted file mode 100644 index 41b6ec846f..0000000000 --- a/apps/docs/pages/guides/platform/database-usage.mdx +++ /dev/null @@ -1,73 +0,0 @@ -import Layout from '~/layouts/DefaultGuideLayout' - -export const meta = { - id: 'database-usage', - title: 'Database usage', - description: 'Understanding how database usage applies to your subscription.', -} - -Database size refers to the _monthly average storage usage_, as reported by Postgres. This metric is reported in your project's [billing usage](https://app.supabase.com/project/_/settings/billing/usage) and is updated daily. -Database size is the total size of used storage from your database, whereas disk size describes the size of the underlying available storage. - -For an instantaneous live view of the DB size, you can execute in Postgres: - -```sql -select - sum(pg_database_size(pg_database.datname)) / (1024 * 1024) as db_size_mb -from pg_database; -``` - -This value is also reported in the [database settings page](https://app.supabase.com/project/_/settings/database). - -## 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. - -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. - -### Increasing available disk size - -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. - -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: - - ```sql - SET - default_transaction_read_only = 'off'; - ``` - - This allows you to delete data from within the session. - -### Preoccupied Space - -When launching a new project, your database size will be roughly ~40-60mb. -The space is used up by preinstalled extensions, schema/data used by our services that are offered with each project and default Postgres data. - -When installing additional extensions, even if you don't actively use them, additional database size is used. - -## Vacuum operations - -Postgres does not immediately reclaim the physical space used by dead tuples (i.e., deleted rows) in the DB. Instead, they are internally marked as removed until a [vacuum operation](https://www.postgresql.org/docs/current/routine-vacuuming.html) is executed. -As a result, deleting data from your database may not immediately reduce the reported disk usage. - - - -Vacuum operations can temporarily increase resource utilization, which may adversely impact the observed performance of your project until the maintenance is completed. - - - -Supabase projects have automatic vacuuming enabled, which ensures that these operations are performed regularly to keep the database healthy and performant. -However, it can be necessary to either [fine-tune](https://www.percona.com/blog/2018/08/10/tuning-autovacuum-in-postgresql-and-autovacuum-internals/) -the [autovacuum parameters](https://www.enterprisedb.com/blog/postgresql-vacuum-and-analyze-best-practice-tips), -or [manually initiate](https://www.postgresql.org/docs/current/sql-vacuum.html) vacuum operations. -For example, running a manual vacuum after deleting large amounts of data from your DB could help reduce the reported disk usage by Postgres. - -export const Page = ({ children }) => - -export default Page 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 1b65745054..9b035e1264 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/pages/guides/platform/performance.mdx b/apps/docs/pages/guides/platform/performance.mdx index 8d7ae48e42..a00b509dbb 100644 --- a/apps/docs/pages/guides/platform/performance.mdx +++ b/apps/docs/pages/guides/platform/performance.mdx @@ -8,6 +8,159 @@ export const meta = { The Supabase platform automatically optimizes your Postgres database to take advantage of the compute resources of the tier your project is on. However, these optimizations are based on assumptions about the type of workflow the project is being utilized for, and it is likely that better results can be obtained by tuning the database for your particular workflow. +## Examining Query Performance + +Unoptimized queries are a major cause of poor database performance. The techniques on this page can help you identify and understand queries that take the most time and resources from your database. + +Database performance is a large topic and many factors can contribute. Some of the most common causes of poor performance include: + +* An inefficiently designed schema +* Inefficiently designed queries +* A lack of indexes causing slower than required queries over large tables +* Unused indexes causing slow `INSERT`, `UPDATE` and `DELETE` operations +* Not enough compute resources, such as memory, causing your database to go to disk for results too often +* Lock contention from multiple queries operating on highly utilized tables +* Large amount of bloat on your tables causing poor query planning + +Thankfully there are solutions to all these issues, which we will cover in the following sections. + +### Postgres Cumulative Statistics system + +Postgres collects data about its own operations using the [cumulative statistics system](https://www.postgresql.org/docs/current/monitoring-stats.html). In addition to this, every Supabase project has the [pg_stat_statements extension](/docs/guides/database/extensions/pg_stat_statements) enabled by default. This extension records query execution performance details and is the best way to find inefficient queries. This information can be combined with the Postgres query plan analyzer to develop more efficient queries. + +Here are some example queries to get you started. + +#### Most frequently called queries: + +```sql +select + auth.rolname, + statements.query, + statements.calls, + -- -- Postgres 13, 14, 15 + statements.total_exec_time + statements.total_plan_time as total_time, + statements.min_exec_time + statements.min_plan_time as min_time, + statements.max_exec_time + statements.max_plan_time as max_time, + statements.mean_exec_time + statements.mean_plan_time as mean_time, + -- -- Postgres <= 12 + -- total_time, + -- min_time, + -- max_time, + -- mean_time, + statements.rows / statements.calls as avg_rows + +from pg_stat_statements as statements + inner join pg_authid as auth on statements.userid = auth.oid +order by + statements.calls desc +limit + 100; +``` + +This query shows: + +- query statistics, ordered by the number of times each query has been executed +- the role that ran the query +- the number of times it has been called +- the average number of rows returned +- the cumulative total time the query has spent running +- the min, max and mean query times. + +This provides useful information about the queries you run most frequently. Queries that have high `max_time` or `mean_time` times and are being called often can be good candidates for optimization. + +#### Slowest queries by execution time: + +```sql +select + auth.rolname, + statements.query, + statements.calls, + -- -- Postgres 13, 14, 15 + statements.total_exec_time + statements.total_plan_time as total_time, + statements.min_exec_time + statements.min_plan_time as min_time, + statements.max_exec_time + statements.max_plan_time as max_time, + statements.mean_exec_time + statements.mean_plan_time as mean_time, + -- -- Postgres <= 12 + -- total_time, + -- min_time, + -- max_time, + -- mean_time, + statements.rows / statements.calls as avg_rows +from pg_stat_statements as statements + inner join pg_authid as auth on statements.userid = auth.oid + order by + max_time desc + limit + 100; +``` + +This query will show you statistics about queries ordered by the maximum execution time. It is similar to the query above ordered by calls, but this one highlights outliers that may have high executions times. Queries which have high or mean execution times are good candidates for optimisation. + +#### Most time consuming queries: + +```sql +select + auth.rolname, + statements.query, + statements.calls, + statements.total_exec_time + statements.total_plan_time as total_time, + to_char(((statements.total_exec_time + statements.total_plan_time)/sum(statements.total_exec_time + statements.total_plan_time) over()) * 100, 'FM90D0') || '%' as prop_total_time +from pg_stat_statements as statements + inner join pg_authid as auth on statements.userid = auth.oid +order by + total_time desc +limit + 100; +``` + +This query will show you statistics about queries ordered by the cumulative total execution time. It shows the total time the query has spent running as well as the proportion of total execution time the query has taken up. + +Queries which are the most time consuming are not necessarily bad, you may have a very effiecient and frequently ran queries that end up taking a large total % time, but it can be useful to help spot queries that are taking up more time than they should. + +### Hit rate + +Generally for most applications a small percentage of data is accessed more regularly than the rest. To make sure that your regularly accessed data is available, Postgres tracks your data access patterns and keeps this in its [shared_buffers](https://www.postgresql.org/docs/15/runtime-config-resource.html#RUNTIME-CONFIG-RESOURCE-MEMORY) cache. + +Applications with lower cache hit rates generally perform more poorly since they have to hit the disk to get results rather than serving them from memory. Very poor hit rates can also cause you to burst past your [Disk I/O limits](https://supabase.com/docs/guides/platform/compute-add-ons#disk-io-bandwidth) causing significant performance issues. + +You can view your cache and index hit rate by executing the following query: + +```sql +select + 'index hit rate' as name, + (sum(idx_blks_hit)) / nullif(sum(idx_blks_hit + idx_blks_read),0) * 100 as ratio +from pg_statio_user_indexes +union all +select + 'table hit rate' as name, + sum(heap_blks_hit) / nullif(sum(heap_blks_hit) + sum(heap_blks_read),0) * 100 as ratio +from pg_statio_user_tables; +``` + +This shows the ratio of data blocks fetched from the Postgres [shared_buffers](https://www.postgresql.org/docs/15/runtime-config-resource.html#RUNTIME-CONFIG-RESOURCE-MEMORY) cache against the data blocks that were read from disk/OS cache. + +If either of your index or table hit rate are < 99% then this can indicate your compute plan is too small for your current workload and you would benefit from more memory. [Upgrading your compute](https://supabase.com/docs/guides/platform/compute-add-ons) is easy and can be done from your [project dashboard](https://app.supabase.com/project/_/settings/billing/subscription). + + +### Optimizing poor performing queries + +Postgres has built in tooling to help you optimize poorly performing queries. You can use the [query plan analyzer](https://www.postgresql.org/docs/current/sql-explain.html) on any expensive queries that you have identified: + +```sql +explain analyze ; +``` + +Be careful using `explain analyze` with `insert`/`update`/`delete` queries, because the query will actually run, and could have unintended side-effects. + +Using the query plan analyzer to optimize your queries is a large topic, with a number of online resources available: + +- [Official docs.](https://www.postgresql.org/docs/current/using-explain.html) +- [The Art of PostgreSQL.](https://theartofpostgresql.com/explain-plan-visualizer/) +- [Postgres Wiki.](https://wiki.postgresql.org/wiki/Using_EXPLAIN) +- [Enterprise DB.](https://www.enterprisedb.com/blog/postgresql-query-optimization-performance-tuning-with-explain-analyze) + +You can pair the information available from `pg_stat_statements` with the detailed system metrics available [via your metrics endpoint](../platform/metrics) to better understand the behavior of your DB and the queries you're executing against it. + ## Optimizing the number of connections By default, the number of connections allowed to Postgres and PgBouncer is configured based on the resources available to the database. @@ -64,33 +217,6 @@ alter system reset ; Configuring the number of PgBouncer connections is not supported at this time. -## Examining Query Performance - -Every Supabase project has [the pg_stat_statements extension](https://www.postgresql.org/docs/14/pgstatstatements.html) enabled by default. This extension records query execution performance details and is the best way to find queries that take the most time to execute. This information can be combined with the Postgres query plan analyzer to develop more efficient queries. - -Obtaining information from pg_stat_statements: - -```sql -select mean_exec_time + stddev_exec_time, * from pg_stat_statements order by 1 desc; -``` - -Using the query plan analyzer on your expensive queries: - -```sql -explain analyze ; -``` - -Be careful using `explain analyze` with `insert`/`update`/`delete` queries, because the query will actually run, and could have unintended side-effects. - -Using the query plan analyzer to optimize your queries is a large topic, with a number of online resources available: - -- [Official docs.](https://www.postgresql.org/docs/current/using-explain.html) -- [The Art of PostgreSQL.](https://theartofpostgresql.com/explain-plan-visualizer/) -- [Postgres Wiki.](https://wiki.postgresql.org/wiki/Using_EXPLAIN) -- [Enterprise DB.](https://www.enterprisedb.com/blog/postgresql-query-optimization-performance-tuning-with-explain-analyze) - -You can pair the information available from `pg_stat_statements` with the detailed system metrics available [via your metrics endpoint](../platform/metrics) to better understand the behavior of your DB and the queries you're executing against it. - export const Page = ({ children }) => export default Page diff --git a/apps/docs/pages/guides/resources/examples.mdx b/apps/docs/pages/guides/resources/examples.mdx index 23b102fb21..18c835d829 100644 --- a/apps/docs/pages/guides/resources/examples.mdx +++ b/apps/docs/pages/guides/resources/examples.mdx @@ -113,6 +113,7 @@ Build a basic Todo List with Supabase and your favorite frontend framework: - In-depth self-hosting guide using Nginx [Blog](https://dev.to/chronsyn/self-hosting-with-supabase-1aii) - Build an Email and Social Auth for Next JS with Supabase, Tailwind CSS 3.0 and TypeScript [Blog](https://creativedesignsguru.com/next-js-supabase-auth/) - Link Shortener using Supabase and Ory [3-part Blog Series](https://www.ory.sh/tutorial-url-shortener-supabase-ory-integration-backend/) +- Building a CRUD API with FastAPI and Supabase: A Step-by-Step Guide [Blog](https://blog.theinfosecguy.xyz/building-a-crud-api-with-fastapi-and-supabase-a-step-by-step-guide) ### Example apps diff --git a/apps/docs/public/humans.txt b/apps/docs/public/humans.txt index 2daf85cdf4..01df68c58f 100644 --- a/apps/docs/public/humans.txt +++ b/apps/docs/public/humans.txt @@ -17,6 +17,7 @@ Dave Wilson Div Arora Egor Romanov Fabrizio Fenoglio +Francesco Sansalvadore Haydn Maley Hieu Pham Inian P diff --git a/apps/docs/public/img/guides/integrations/prisma/w0oowg8vq435ob5c3gf0.png b/apps/docs/public/img/guides/integrations/prisma/w0oowg8vq435ob5c3gf0.png index 9bfb34db47..05db2d109b 100644 Binary files a/apps/docs/public/img/guides/integrations/prisma/w0oowg8vq435ob5c3gf0.png and b/apps/docs/public/img/guides/integrations/prisma/w0oowg8vq435ob5c3gf0.png differ 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 0000000000..c5596878b0 Binary files /dev/null and b/apps/docs/public/img/integrations/logos/openai_logo.png differ diff --git a/apps/docs/public/sitemap.xml b/apps/docs/public/sitemap.xml index 35bf4d7ff5..598060f55a 100644 --- a/apps/docs/public/sitemap.xml +++ b/apps/docs/public/sitemap.xml @@ -18,12 +18,6 @@ 0.5 - - https://supabase.com/docs/guides/database/api - weekly - 0.5 - - https://supabase.com/docs/guides/cli weekly @@ -36,12 +30,6 @@ 0.5 - - https://supabase.com/docs/guides/examples - weekly - 0.5 - - https://supabase.com/docs/guides/functions weekly @@ -90,12 +78,6 @@ 0.5 - - https://supabase.com/docs/reference/index - weekly - 0.5 - - https://supabase.com/docs/handbook/contributing weekly @@ -108,30 +90,12 @@ 0.5 - - https://supabase.com/docs/new/auth - weekly - 0.5 - - https://supabase.com/docs/reference/index weekly 0.5 - - https://supabase.com/docs/tutorials/nextjs - weekly - 0.5 - - - - https://supabase.com/docs/guides/database/api/generating-types - weekly - 0.5 - - https://supabase.com/docs/guides/auth/auth-captcha weekly @@ -216,6 +180,12 @@ 0.5 + + https://supabase.com/docs/guides/database/api + weekly + 0.5 + + https://supabase.com/docs/guides/database/arrays weekly @@ -318,60 +288,6 @@ 0.5 - - https://supabase.com/docs/guides/functions/auth - weekly - 0.5 - - - - https://supabase.com/docs/guides/functions/cicd-workflow - weekly - 0.5 - - - - https://supabase.com/docs/guides/functions/cors - weekly - 0.5 - - - - https://supabase.com/docs/guides/functions/debugging - weekly - 0.5 - - - - https://supabase.com/docs/guides/functions/import-maps - weekly - 0.5 - - - - https://supabase.com/docs/guides/functions/local-development - weekly - 0.5 - - - - https://supabase.com/docs/guides/functions/quickstart - weekly - 0.5 - - - - https://supabase.com/docs/guides/functions/schedule-functions - weekly - 0.5 - - - - https://supabase.com/docs/guides/functions/secrets - weekly - 0.5 - - https://supabase.com/docs/guides/integrations/appsmith weekly @@ -534,6 +450,60 @@ 0.5 + + https://supabase.com/docs/guides/functions/auth + weekly + 0.5 + + + + https://supabase.com/docs/guides/functions/cicd-workflow + weekly + 0.5 + + + + https://supabase.com/docs/guides/functions/cors + weekly + 0.5 + + + + https://supabase.com/docs/guides/functions/debugging + weekly + 0.5 + + + + https://supabase.com/docs/guides/functions/import-maps + weekly + 0.5 + + + + https://supabase.com/docs/guides/functions/local-development + weekly + 0.5 + + + + https://supabase.com/docs/guides/functions/quickstart + weekly + 0.5 + + + + https://supabase.com/docs/guides/functions/schedule-functions + weekly + 0.5 + + + + https://supabase.com/docs/guides/functions/secrets + weekly + 0.5 + + https://supabase.com/docs/guides/platform/access-control weekly @@ -559,7 +529,7 @@ - https://supabase.com/docs/guides/platform/database-usage + https://supabase.com/docs/guides/platform/database-size weekly 0.5 @@ -612,6 +582,12 @@ 0.5 + + https://supabase.com/docs/guides/platform/ssl-enforcement + weekly + 0.5 + + https://supabase.com/docs/guides/platform/sso weekly @@ -624,12 +600,24 @@ 0.5 + + https://supabase.com/docs/guides/realtime/broadcast + weekly + 0.5 + + https://supabase.com/docs/guides/realtime/postgres-changes weekly 0.5 + + https://supabase.com/docs/guides/realtime/presence + weekly + 0.5 + + https://supabase.com/docs/guides/realtime/quickstart weekly @@ -642,6 +630,12 @@ 0.5 + + https://supabase.com/docs/guides/resources/examples + weekly + 0.5 + + https://supabase.com/docs/guides/resources/glossary weekly @@ -715,49 +709,139 @@ - https://supabase.com/docs/guides/database/extensions/http + https://supabase.com/docs/guides/auth/auth-helpers/auth-ui weekly 0.5 - https://supabase.com/docs/guides/database/extensions/pgcron + https://supabase.com/docs/guides/auth/auth-helpers/nextjs-server-components weekly 0.5 - https://supabase.com/docs/guides/database/extensions/pgnet + https://supabase.com/docs/guides/auth/auth-helpers/nextjs weekly 0.5 - https://supabase.com/docs/guides/database/extensions/pgtap + https://supabase.com/docs/guides/auth/auth-helpers/remix weekly 0.5 - https://supabase.com/docs/guides/database/extensions/plv8 + https://supabase.com/docs/guides/auth/auth-helpers/sveltekit weekly 0.5 - https://supabase.com/docs/guides/database/extensions/postgis + https://supabase.com/docs/guides/auth/social-login/auth-apple weekly 0.5 - https://supabase.com/docs/guides/database/extensions/rum + https://supabase.com/docs/guides/auth/social-login/auth-azure weekly 0.5 - https://supabase.com/docs/guides/database/extensions/uuid-ossp + https://supabase.com/docs/guides/auth/social-login/auth-bitbucket + weekly + 0.5 + + + + https://supabase.com/docs/guides/auth/social-login/auth-discord + weekly + 0.5 + + + + https://supabase.com/docs/guides/auth/social-login/auth-facebook + weekly + 0.5 + + + + https://supabase.com/docs/guides/auth/social-login/auth-github + weekly + 0.5 + + + + https://supabase.com/docs/guides/auth/social-login/auth-gitlab + weekly + 0.5 + + + + https://supabase.com/docs/guides/auth/social-login/auth-google + weekly + 0.5 + + + + https://supabase.com/docs/guides/auth/social-login/auth-keycloak + weekly + 0.5 + + + + https://supabase.com/docs/guides/auth/social-login/auth-linkedin + weekly + 0.5 + + + + https://supabase.com/docs/guides/auth/social-login/auth-notion + weekly + 0.5 + + + + https://supabase.com/docs/guides/auth/social-login/auth-slack + weekly + 0.5 + + + + https://supabase.com/docs/guides/auth/social-login/auth-spotify + weekly + 0.5 + + + + https://supabase.com/docs/guides/auth/social-login/auth-twitch + weekly + 0.5 + + + + https://supabase.com/docs/guides/auth/social-login/auth-twitter + weekly + 0.5 + + + + https://supabase.com/docs/guides/auth/social-login/auth-workos + weekly + 0.5 + + + + https://supabase.com/docs/guides/auth/social-login/auth-zoom + weekly + 0.5 + + + + https://supabase.com/docs/guides/database/api/generating-types weekly 0.5 @@ -888,36 +972,6 @@ 0.5 - - https://supabase.com/docs/guides/auth/auth-helpers/auth-ui - weekly - 0.5 - - - - https://supabase.com/docs/guides/auth/auth-helpers/nextjs-server-components - weekly - 0.5 - - - - https://supabase.com/docs/guides/auth/auth-helpers/nextjs - weekly - 0.5 - - - - https://supabase.com/docs/guides/auth/auth-helpers/remix - weekly - 0.5 - - - - https://supabase.com/docs/guides/auth/auth-helpers/sveltekit - weekly - 0.5 - - https://supabase.com/docs/guides/auth/phone-login/messagebird weekly @@ -948,6 +1002,12 @@ 0.5 + + https://supabase.com/docs/guides/functions/examples/discord-bot + weekly + 0.5 + + https://supabase.com/docs/guides/functions/examples/github-actions weekly @@ -960,6 +1020,12 @@ 0.5 + + https://supabase.com/docs/guides/functions/examples/openai + weekly + 0.5 + + https://supabase.com/docs/guides/functions/examples/storage-caching weekly @@ -978,6 +1044,186 @@ 0.5 + + https://supabase.com/docs/guides/functions/examples/upstash-redis + weekly + 0.5 + + + + https://supabase.com/docs/guides/platform/sso/azure + weekly + 0.5 + + + + https://supabase.com/docs/guides/platform/sso/gsuite + weekly + 0.5 + + + + https://supabase.com/docs/guides/platform/sso/okta + weekly + 0.5 + + + + https://supabase.com/docs/guides/database/extensions/http + weekly + 0.5 + + + + https://supabase.com/docs/guides/database/extensions/hypopg + weekly + 0.5 + + + + https://supabase.com/docs/guides/database/extensions/pg-safeupdate + weekly + 0.5 + + + + https://supabase.com/docs/guides/database/extensions/pg_graphql + weekly + 0.5 + + + + https://supabase.com/docs/guides/database/extensions/pg_hashids + weekly + 0.5 + + + + https://supabase.com/docs/guides/database/extensions/pg_jsonschema + weekly + 0.5 + + + + https://supabase.com/docs/guides/database/extensions/pg_plan_filter + weekly + 0.5 + + + + https://supabase.com/docs/guides/database/extensions/pg_stat_monitor + weekly + 0.5 + + + + https://supabase.com/docs/guides/database/extensions/pg_stat_statements + weekly + 0.5 + + + + https://supabase.com/docs/guides/database/extensions/pgaudit + weekly + 0.5 + + + + https://supabase.com/docs/guides/database/extensions/pgcron + weekly + 0.5 + + + + https://supabase.com/docs/guides/database/extensions/pgjwt + weekly + 0.5 + + + + https://supabase.com/docs/guides/database/extensions/pgnet + weekly + 0.5 + + + + https://supabase.com/docs/guides/database/extensions/pgrepack + weekly + 0.5 + + + + https://supabase.com/docs/guides/database/extensions/pgroonga + weekly + 0.5 + + + + https://supabase.com/docs/guides/database/extensions/pgrouting + weekly + 0.5 + + + + https://supabase.com/docs/guides/database/extensions/pgsodium + weekly + 0.5 + + + + https://supabase.com/docs/guides/database/extensions/pgtap + weekly + 0.5 + + + + https://supabase.com/docs/guides/database/extensions/pgvector + weekly + 0.5 + + + + https://supabase.com/docs/guides/database/extensions/plpgsql_check + weekly + 0.5 + + + + https://supabase.com/docs/guides/database/extensions/plv8 + weekly + 0.5 + + + + https://supabase.com/docs/guides/database/extensions/postgis + weekly + 0.5 + + + + https://supabase.com/docs/guides/database/extensions/rum + weekly + 0.5 + + + + https://supabase.com/docs/guides/database/extensions/timescaledb + weekly + 0.5 + + + + https://supabase.com/docs/guides/database/extensions/uuid-ossp + weekly + 0.5 + + + + https://supabase.com/docs/guides/database/extensions/wrappers + weekly + 0.5 + + https://supabase.com/docs/guides/resources/migrating-to-supabase/firebase-auth weekly @@ -1003,25 +1249,7 @@ - https://supabase.com/docs/guides/self-hosting/realtime/config - weekly - 0.5 - - - - https://supabase.com/docs/guides/platform/sso/azure - weekly - 0.5 - - - - https://supabase.com/docs/guides/platform/sso/gsuite - weekly - 0.5 - - - - https://supabase.com/docs/guides/platform/sso/okta + https://supabase.com/docs/guides/resources/migrating-to-supabase/render weekly 0.5 @@ -1032,114 +1260,36 @@ 0.5 + + https://supabase.com/docs/guides/resources/postgres/dropping-all-tables-in-schema + weekly + 0.5 + + + + https://supabase.com/docs/guides/resources/postgres/first-row-in-group + weekly + 0.5 + + + + https://supabase.com/docs/guides/resources/postgres/which-version-of-postgres + weekly + 0.5 + + + + https://supabase.com/docs/guides/self-hosting/realtime/config + weekly + 0.5 + + https://supabase.com/docs/guides/self-hosting/storage/config weekly 0.5 - - https://supabase.com/docs/guides/auth/social-login/auth-apple - weekly - 0.5 - - - - https://supabase.com/docs/guides/auth/social-login/auth-azure - weekly - 0.5 - - - - https://supabase.com/docs/guides/auth/social-login/auth-bitbucket - weekly - 0.5 - - - - https://supabase.com/docs/guides/auth/social-login/auth-discord - weekly - 0.5 - - - - https://supabase.com/docs/guides/auth/social-login/auth-facebook - weekly - 0.5 - - - - https://supabase.com/docs/guides/auth/social-login/auth-github - weekly - 0.5 - - - - https://supabase.com/docs/guides/auth/social-login/auth-gitlab - weekly - 0.5 - - - - https://supabase.com/docs/guides/auth/social-login/auth-google - weekly - 0.5 - - - - https://supabase.com/docs/guides/auth/social-login/auth-keycloak - weekly - 0.5 - - - - https://supabase.com/docs/guides/auth/social-login/auth-linkedin - weekly - 0.5 - - - - https://supabase.com/docs/guides/auth/social-login/auth-notion - weekly - 0.5 - - - - https://supabase.com/docs/guides/auth/social-login/auth-slack - weekly - 0.5 - - - - https://supabase.com/docs/guides/auth/social-login/auth-spotify - weekly - 0.5 - - - - https://supabase.com/docs/guides/auth/social-login/auth-twitch - weekly - 0.5 - - - - https://supabase.com/docs/guides/auth/social-login/auth-twitter - weekly - 0.5 - - - - https://supabase.com/docs/guides/auth/social-login/auth-workos - weekly - 0.5 - - - - https://supabase.com/docs/guides/auth/social-login/auth-zoom - weekly - 0.5 - - https://supabase.com/docs/reference/javascript/initializing weekly @@ -1562,6 +1712,12 @@ 0.5 + + https://supabase.com/docs/reference/javascript/db-returns + weekly + 0.5 + + https://supabase.com/docs/reference/javascript/functions-invoke weekly @@ -3867,4 +4023,4 @@ weekly 0.5 - \ No newline at end of file + diff --git a/apps/www/_blog/2023-02-07-chatgpt-supabase-docs.mdx b/apps/www/_blog/2023-02-07-chatgpt-supabase-docs.mdx index 0773f7c971..85b061947c 100644 --- a/apps/www/_blog/2023-02-07-chatgpt-supabase-docs.mdx +++ b/apps/www/_blog/2023-02-07-chatgpt-supabase-docs.mdx @@ -11,7 +11,7 @@ date: '2023-02-07' toc_depth: 3 --- -We all know that Microsoft's real agenda for pouring billions into OpenAI to revive their favorite friend Clippy. +We all know that Microsoft's real agenda for pouring billions into OpenAI is to revive their favorite friend Clippy. Today, we're doing our part to support the momentum by releasing “Supabase Clippy” for our docs (and we don't expect this name to last long before the lawyers catch on). ![Clippy](/images/blog/docsgpt/clippy.png) @@ -30,7 +30,7 @@ Our product suite has grown in the past 2 years and our docs have grown as a res ### The “ask” interface -Developers have recently gained an the ability to trust a bot. Where Clippy failed, ChatGPT succeeded. +Developers have recently gained the ability to trust a bot. Where Clippy failed, ChatGPT succeeded. This is convenient timing for us, since our documentation content is more than the average developer wants to consume in one go. Today we're providing a similar interface to ChatGPT which is trained on our own docs. diff --git a/apps/www/data/Community.tsx b/apps/www/data/Community.tsx index ea9573cdcc..89013d080f 100644 --- a/apps/www/data/Community.tsx +++ b/apps/www/data/Community.tsx @@ -30,7 +30,7 @@ const data: CommunityItem[] = [ }, { title: 'GitHub', - stat: '44,000+', + stat: '46,000+', statLabel: 'GitHub stars', img: 'github.png', invertImgDarkMode: true, @@ -42,7 +42,7 @@ const data: CommunityItem[] = [ }, { title: 'Twitter', - stat: '54,000+', + stat: '60,000+', statLabel: 'Followers', img: 'twitter.png', detail: () => ( diff --git a/apps/www/lib/redirects.js b/apps/www/lib/redirects.js index e73aa509b5..00b9b15e5c 100644 --- a/apps/www/lib/redirects.js +++ b/apps/www/lib/redirects.js @@ -1857,4 +1857,9 @@ module.exports = [ source: '/docs/reference/javascript/v0/rpc', destination: '/docs/reference/javascript/rpc', }, + { + permanent: true, + source: '/docs/guides/platform/database-usage', + destination: '/docs/guides/platform/database-size', + }, ] diff --git a/apps/www/pages/_app.tsx b/apps/www/pages/_app.tsx index b4fb7a031c..9698060202 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 9b209dc901..bb359df65a 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/apps/www/pages/sla.mdx b/apps/www/pages/sla.mdx index 0c367489da..e535758d63 100644 --- a/apps/www/pages/sla.mdx +++ b/apps/www/pages/sla.mdx @@ -87,8 +87,8 @@ An Information request about Supabase or feature request. | Severity Level | Standard | Priority | Priority Plus | | -------------- | ------------------------------------- | ------------------------------------- | -------------------------------------- | -| 1. Urgent | 1 business hour
24/7 × 365 | 1 business hour
24/7 × 365 | 1 business hour
24/7 × 365 | -| 2. High | 4 business hours
Monday - Friday | 2 business hours
Monday - Friday | 2 business hours
24/7 × 365 | +| 1. Urgent | 1 hour
24/7 × 365 | 1 hour
24/7 × 365 | 1 hour
24/7 × 365 | +| 2. High | 4 business hours
Monday - Friday | 2 business hours
Monday - Friday | 2 hours
24/7 × 365 | | 3. Normal | 1 business day
Monday - Friday | 1 business day
Monday - Friday | 12 business hours
Monday - Friday | | 4. Low | 2 business days
Monday - Friday | 2 business days
Monday - Friday | 1 business day
Monday - Friday | diff --git a/examples/edge-functions/README.md b/examples/edge-functions/README.md index f32e56e235..47e624783a 100644 --- a/examples/edge-functions/README.md +++ b/examples/edge-functions/README.md @@ -6,23 +6,17 @@ ## Example Functions -The function examples are located in [`./supabase/functions`](./supabase/functions): - -- [`browser-with-cors`](./supabase/functions/browser-with-cors/index.ts): Handle CORS headers for function invocations from browser environments. -- [`select-from-table-with-auth-rls`](./supabase/functions/select-from-table-with-auth-rls/index.ts): Retrieve data from an authenticated user via RLS. -- [`send-email-smtp`](./supabase/functions/send-email-smtp/index.ts): Send an email using SMTP credentials. -- [`stripe-webhooks`](./supabase/functions/stripe-webhooks/index.ts): Handle Stripe Webhooks. -- [`telegram-bot`](./supabase/functions/telegram-bot/index.ts): Webhook handler for Telegram bots using [grammY](https://grammy.dev/). +We're constantly adding new Function Examples, [check our docs](https://supabase.com/docs/guides/functions#examples) for a complete list! ## Develop locally - Run `supabase start` (make sure your Docker daemon is running.) -- Run `mv ./supabase/.env.local.example ./supabase/.env.local` to rename the local `.env` file. -- Set the required variables to run the desired edge functions in the `.env.local` file. -- Run `supabase functions serve your-function-name --env-file ./supabase/.env.local` +- Run `cp ./supabase/.env.local.example ./supabase/.env.local` to create your local `.env` file. +- Set the required variables for the corresponding edge functions in the `.env.local` file. +- Run `supabase functions serve --env-file ./supabase/.env.local --no-verify-jwt` - Run the CURL command in the example function, or use the [invoke method](https://supabase.com/docs/reference/javascript/invoke) on the Supabase client or use the test client [app](./app/). -## Test +## Test Client This example includes a create-react-app in the [`./app/`](./app/) directory which you can use as a sort of postman to make test requests both locally and to your deployed functions. @@ -45,10 +39,13 @@ Note: when testing locally, the select dropdown doesn't have any effect, and inv - Link your project - Within your project root run `supabase link --project-ref your-project-ref` - Set up your secrets + - Run `supabase secrets set --env-file ./supabase/.env.local` to set the environment variables. - + (This is assuming your local and production secrets are the same. The recommended way is to create a separate `.env` file for storing production secrets, and then use it to set the environment variables while deploying.) + - You can run `supabase secrets list` to check that it worked and also to see what other env vars are set by default. + - Deploy the function - Within your project root run `supabase functions deploy your-function-name` - In your [`./app/.env`](./app/.env) file remove the `SUPA_FUNCTION_LOCALHOST` variable and restart your Expo app. diff --git a/examples/edge-functions/edge-functions.code-workspace b/examples/edge-functions/edge-functions.code-workspace index 80054980de..d3e694fd61 100644 --- a/examples/edge-functions/edge-functions.code-workspace +++ b/examples/edge-functions/edge-functions.code-workspace @@ -18,6 +18,7 @@ "node_modules/": true, "app/": true, "supabase/functions/": true - } + }, + "deno.importMap": "./supabase/functions/import_map.json" } } diff --git a/examples/edge-functions/supabase/.env.local.example b/examples/edge-functions/supabase/.env.local.example index 4456221dc6..0031244f9f 100644 --- a/examples/edge-functions/supabase/.env.local.example +++ b/examples/edge-functions/supabase/.env.local.example @@ -1,3 +1,21 @@ +# cloudflare-turnstile +CLOUDFLARE_TURNSTILE_SECRET_KEY=your_secret_key + +# discord-bot +DISCORD_PUBLIC_KEY= + +# location +IPINFO_TOKEN="your https://ipinfo.io token" + +# openai +OPENAI_API_KEY="" + +# postgres-on-the-edge +DATABASE_URL= + +# puppeteer +PUPPETEER_BROWSERLESS_IO_TOKEN= + # send-email-smtp SMTP_HOSTNAME="your.hostname.com" SMTP_PORT="2587" @@ -5,13 +23,14 @@ SMTP_USERNAME="your_username" SMTP_PASSWORD="your_password" SMTP_FROM="no-reply@example.com" -# telegram-bot -TELEGRAM_BOT_TOKEN="get it from https://t.me/BotFather" -FUNCTION_SECRET="random secret" - # stripe-webhooks STRIPE_API_KEY="" STRIPE_WEBHOOK_SIGNING_SECRET="" -# openai -OPENAI_API_KEY="" +# telegram-bot +TELEGRAM_BOT_TOKEN="get it from https://t.me/BotFather" +FUNCTION_SECRET="random secret" + +# upstash-redis-counter +UPSTASH_REDIS_REST_URL= +UPSTASH_REDIS_REST_TOKEN= diff --git a/examples/edge-functions/supabase/functions/.vscode/settings.json b/examples/edge-functions/supabase/functions/.vscode/settings.json index e40716fdd7..1d800e858b 100644 --- a/examples/edge-functions/supabase/functions/.vscode/settings.json +++ b/examples/edge-functions/supabase/functions/.vscode/settings.json @@ -1,5 +1,5 @@ { "deno.enable": true, - "deno.lint": true, - "deno.unstable": true + "deno.unstable": true, + "deno.importMap": "./import_map.json" } diff --git a/examples/edge-functions/supabase/functions/browser-with-cors/index.ts b/examples/edge-functions/supabase/functions/browser-with-cors/index.ts index 1ab04de4e1..d022ef6035 100644 --- a/examples/edge-functions/supabase/functions/browser-with-cors/index.ts +++ b/examples/edge-functions/supabase/functions/browser-with-cors/index.ts @@ -2,7 +2,7 @@ // https://deno.land/manual/getting_started/setup_your_environment // This enables autocomplete, go to definition, etc. -import { serve } from 'https://deno.land/std@0.131.0/http/server.ts' +import { serve } from 'std/server' import { corsHeaders } from '../_shared/cors.ts' console.log(`Function "browser-with-cors" up and running!`) diff --git a/examples/edge-functions/supabase/functions/cloudflare-turnstile/README.md b/examples/edge-functions/supabase/functions/cloudflare-turnstile/README.md index 4d8bc5008c..3ec3bf986b 100644 --- a/examples/edge-functions/supabase/functions/cloudflare-turnstile/README.md +++ b/examples/edge-functions/supabase/functions/cloudflare-turnstile/README.md @@ -2,6 +2,10 @@ Turnstile is Cloudflare's CAPTCHA alternative: https://developers.cloudflare.com/turnstile/get-started/ +## Watch the Video Tutorial + +[![video tutorial](https://img.youtube.com/vi/OwW0znboh60/0.jpg)](https://www.youtube.com/watch?v=OwW0znboh60) + ## Setup - Follow these steps to set up a new site: https://developers.cloudflare.com/turnstile/get-started/ diff --git a/examples/edge-functions/supabase/functions/cloudflare-turnstile/index.ts b/examples/edge-functions/supabase/functions/cloudflare-turnstile/index.ts index 5d831d18ab..02e440f346 100644 --- a/examples/edge-functions/supabase/functions/cloudflare-turnstile/index.ts +++ b/examples/edge-functions/supabase/functions/cloudflare-turnstile/index.ts @@ -2,7 +2,7 @@ // https://deno.land/manual/getting_started/setup_your_environment // This enables autocomplete, go to definition, etc. -import { serve } from 'https://deno.land/std@0.131.0/http/server.ts' +import { serve } from 'std/server' import { corsHeaders } from '../_shared/cors.ts' console.log(`Function "cloudflare-turnstile" up and running!`) @@ -55,7 +55,7 @@ serve(async (req) => { }) // To invoke: -// curl -i --location --request POST 'http://localhost:54321/functions/v1/' \ +// curl -i --location --request POST 'http://localhost:54321/functions/v1/cloudflare-turnstile' \ // --header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0' \ // --header 'Content-Type: application/json' \ // --data '{"token":"cf-turnstile-response"}' diff --git a/examples/edge-functions/supabase/functions/discord-bot/README.md b/examples/edge-functions/supabase/functions/discord-bot/README.md index 5e9ad79f29..7d66deb798 100644 --- a/examples/edge-functions/supabase/functions/discord-bot/README.md +++ b/examples/edge-functions/supabase/functions/discord-bot/README.md @@ -5,6 +5,10 @@ - https://deno.com/deploy/docs/tutorial-discord-slash - https://discord.com/developers/docs/interactions/application-commands#slash-commands +## Watch the Video Tutorial + +[![video tutorial](https://img.youtube.com/vi/J24Bvo_m7DM/0.jpg)](https://www.youtube.com/watch?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). @@ -59,6 +63,6 @@ Open Discord, type `/Promise` and press **Enter**. ## Run locally ```bash -supabase functions serve discord-bot --no-verify-jwt --env-file ./supabase/functions/discord-bot/.env +supabase functions serve discord-bot --no-verify-jwt --env-file ./supabase/.env.local ngrok http 54321 ``` diff --git a/examples/edge-functions/supabase/functions/discord-bot/index.ts b/examples/edge-functions/supabase/functions/discord-bot/index.ts index 140bb30b76..abbf5985f2 100644 --- a/examples/edge-functions/supabase/functions/discord-bot/index.ts +++ b/examples/edge-functions/supabase/functions/discord-bot/index.ts @@ -5,10 +5,10 @@ // 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 'https://deno.land/x/sift@0.6.0/mod.ts' +import { json, serve, validateRequest } from 'sift' // TweetNaCl is a cryptography library that we use to verify requests // from Discord. -import nacl from 'https://cdn.skypack.dev/tweetnacl@v1.0.3?dts' +import nacl from 'nacl' enum DiscordCommandType { Ping = 1, diff --git a/examples/edge-functions/supabase/functions/file-upload-storage/index.ts b/examples/edge-functions/supabase/functions/file-upload-storage/index.ts index 5593c0b658..208619a7b4 100644 --- a/examples/edge-functions/supabase/functions/file-upload-storage/index.ts +++ b/examples/edge-functions/supabase/functions/file-upload-storage/index.ts @@ -1,8 +1,8 @@ // This example shows how to use Edge Functions to read incoming multipart/form-data request, // and write files to Supabase Storage and other fields to a database table. -import { Application, Router } from 'https://deno.land/x/oak/mod.ts' -import { createClient } from 'https://esm.sh/@supabase/supabase-js@2' +import { Application } from 'oak' +import { createClient } from '@supabase/supabase-js' const MB = 1024 * 1024 @@ -25,9 +25,9 @@ app.use(async (ctx) => { const supabaseClient = createClient( // Supabase API URL - env var exported by default. - Deno.env.get('SUPABASE_URL') ?? '', + Deno.env.get('SUPABASE_URL')!, // Supabase API ANON KEY - env var exported by default. - Deno.env.get('SUPABASE_ANON_KEY') ?? '' + Deno.env.get('SUPABASE_ANON_KEY')! ) //upload image to Storage diff --git a/examples/edge-functions/supabase/functions/get-tshirt-competition/README.md b/examples/edge-functions/supabase/functions/get-tshirt-competition/README.md index 8d0bcb0dd7..b83a049526 100644 --- a/examples/edge-functions/supabase/functions/get-tshirt-competition/README.md +++ b/examples/edge-functions/supabase/functions/get-tshirt-competition/README.md @@ -15,9 +15,11 @@ Good luck trying to GET a T-Shirt! ### Serve this function locally ```bash -supabase functions serve --no-verify-jwt get-tshirt-competition +supabase functions serve --no-verify-jwt ``` +Navigate to http://localhost:54321/functions/v1/get-tshirt-competition?email=testr@test.de&twitter=thorwebdev&size=2XL&answer=20 + ### Deploy this function ```bash diff --git a/examples/edge-functions/supabase/functions/get-tshirt-competition/index.ts b/examples/edge-functions/supabase/functions/get-tshirt-competition/index.ts index 01e0360544..76b09b974c 100644 --- a/examples/edge-functions/supabase/functions/get-tshirt-competition/index.ts +++ b/examples/edge-functions/supabase/functions/get-tshirt-competition/index.ts @@ -2,8 +2,8 @@ // https://deno.land/manual/getting_started/setup_your_environment // This enables autocomplete, go to definition, etc. -import { serve } from 'https://deno.land/std@0.131.0/http/server.ts' -import { createClient } from 'https://esm.sh/@supabase/supabase-js@2' +import { serve } from 'std/server' +import { createClient } from '@supabase/supabase-js' import { corsHeaders } from '../_shared/cors.ts' console.log(`Function "get-tshirt-competition" up and running!`) diff --git a/examples/edge-functions/supabase/functions/github-action-deploy/index.ts b/examples/edge-functions/supabase/functions/github-action-deploy/index.ts index 2dc629d0cf..423445b160 100644 --- a/examples/edge-functions/supabase/functions/github-action-deploy/index.ts +++ b/examples/edge-functions/supabase/functions/github-action-deploy/index.ts @@ -2,23 +2,18 @@ // https://deno.land/manual/getting_started/setup_your_environment // This enables autocomplete, go to definition, etc. -import { serve } from "https://deno.land/std@0.131.0/http/server.ts"; +import { serve } from 'std/server' -console.log("Hello from Functions!"); +console.log('Hello from Functions!') -serve(async (req) => { - const { name } = await req.json(); +serve((_req) => { const data = { message: `I was deployed via GitHub Actions!`, - }; + } return new Response(JSON.stringify(data), { - headers: { "Content-Type": "application/json" }, - }); -}); + headers: { 'Content-Type': 'application/json' }, + }) +}) -// To invoke: -// curl -i --location --request POST 'http://localhost:54321/functions/v1/' \ -// --header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24ifQ.625_WdcF3KHqz5amU0x2X5WWHP-OEs_4qj0ssLNHzTs' \ -// --header 'Content-Type: application/json' \ -// --data '{"name":"Functions"}' +// To invoke: http://localhost:54321/functions/v1/github-action-deploy diff --git a/examples/edge-functions/supabase/functions/import_map.json b/examples/edge-functions/supabase/functions/import_map.json new file mode 100644 index 0000000000..be4b4d8411 --- /dev/null +++ b/examples/edge-functions/supabase/functions/import_map.json @@ -0,0 +1,20 @@ +{ + "imports": { + "denomailer": "https://deno.land/x/denomailer@0.12.0/mod.ts", + "nacl": "https://cdn.skypack.dev/tweetnacl@v1.0.3?dts", + "oak": "https://deno.land/x/oak@v11.1.0/mod.ts", + "og_edge": "https://deno.land/x/og_edge@0.0.4/mod.ts", + "openai": "https://esm.sh/openai@3.1.0", + "grammy": "https://deno.land/x/grammy@v1.8.3/mod.ts", + "react": "https://esm.sh/react@18.2.0", + "std/server": "https://deno.land/std@0.177.0/http/server.ts", + "stripe": "https://esm.sh/stripe@11.1.0?target=deno", + "sift": "https://deno.land/x/sift@0.6.0/mod.ts", + "@supabase/supabase-js": "https://esm.sh/@supabase/supabase-js@2.7.1", + "postgres": "https://deno.land/x/postgres@v0.14.2/mod.ts", + "puppeteer": "https://deno.land/x/puppeteer@16.2.0/mod.ts", + "React": "https://esm.sh/react@18.2.0?deno-std=0.177.0", + "upstash_redis": "https://deno.land/x/upstash_redis@v1.19.3/mod.ts", + "xhr_polyfill": "https://deno.land/x/xhr@0.3.0/mod.ts" + } +} diff --git a/examples/edge-functions/supabase/functions/location/README.md b/examples/edge-functions/supabase/functions/location/README.md index b2c0bacceb..de3010f444 100644 --- a/examples/edge-functions/supabase/functions/location/README.md +++ b/examples/edge-functions/supabase/functions/location/README.md @@ -4,6 +4,14 @@ This example shows how you can get user location based on the IP provided in X-F You will need to signup for an account in https://ipinfo.io and provide it as `IPINFO_TOKEN` environment variable ([learn how to set environment variables to your functions](https://supabase.com/docs/guides/functions#secrets-and-environment-variables)). +## Develop locally + +```bash +supabase functions serve --env-file ./supabase/.env.local --no-verify-jwt +``` + +Navigate to http://localhost:54321/functions/v1/location + ## Deploy ```bash diff --git a/examples/edge-functions/supabase/functions/location/index.ts b/examples/edge-functions/supabase/functions/location/index.ts index c9df62475a..cda85ab783 100644 --- a/examples/edge-functions/supabase/functions/location/index.ts +++ b/examples/edge-functions/supabase/functions/location/index.ts @@ -2,20 +2,29 @@ // https://deno.land/manual/getting_started/setup_your_environment // This enables autocomplete, go to definition, etc. -import { serve } from "https://deno.land/std@0.131.0/http/server.ts" +import { serve } from 'std/server' function ips(req: Request) { - return req.headers.get("x-forwarded-for")?.split(/\s*,\s*/); + return req.headers.get('x-forwarded-for')?.split(/\s*,\s*/) } serve(async (req) => { - const clientIps = ips(req) || ['']; - const res = await fetch(`https://ipinfo.io/${clientIps[0]}?token=${Deno.env.get('IPINFO_TOKEN')}`, { - headers: { 'Content-Type': 'application/json'}}); - const { city, country } = await res.json(); - - return new Response( - JSON.stringify(`You're accessing from ${city}, ${country}`), - { headers: { "Content-Type": "application/json" } }, + const clientIps = ips(req) || [''] + const res = await fetch( + `https://ipinfo.io/${clientIps[0]}?token=${Deno.env.get('IPINFO_TOKEN')}`, + { + headers: { 'Content-Type': 'application/json' }, + } ) + if (res.ok) { + const { city, country } = await res.json() + + return new Response(JSON.stringify(`You're accessing from ${city}, ${country}`), { + headers: { 'Content-Type': 'application/json' }, + }) + } else { + return new Response(await res.text(), { + status: 400, + }) + } }) diff --git a/examples/edge-functions/supabase/functions/oak-server/README.md b/examples/edge-functions/supabase/functions/oak-server/README.md index b1db616623..ed2795cfc7 100644 --- a/examples/edge-functions/supabase/functions/oak-server/README.md +++ b/examples/edge-functions/supabase/functions/oak-server/README.md @@ -5,7 +5,7 @@ This example shows how you can write functions using Oak server middleware (http ## Run locally ```bash -supabase functions serve oak-server +supabase functions serve --no-verify-jwt ``` Use cURL or Postman to make a POST request to http://localhost:54321/functions/v1/oak-server. @@ -20,5 +20,5 @@ Use cURL or Postman to make a POST request to http://localhost:54321/functions/v ## Deploy ```bash -supabase functions deploy oak-server +supabase functions deploy oak-server --no-verify-jwt ``` diff --git a/examples/edge-functions/supabase/functions/oak-server/index.ts b/examples/edge-functions/supabase/functions/oak-server/index.ts index 9be189be65..965da32e4a 100644 --- a/examples/edge-functions/supabase/functions/oak-server/index.ts +++ b/examples/edge-functions/supabase/functions/oak-server/index.ts @@ -1,4 +1,4 @@ -import { Application, Router } from 'https://deno.land/x/oak/mod.ts' +import { Application, Router } from 'oak' const router = new Router() router diff --git a/examples/edge-functions/supabase/functions/og-image-with-storage-cdn/README.md b/examples/edge-functions/supabase/functions/og-image-with-storage-cdn/README.md index 2adb83328c..05158fd957 100644 --- a/examples/edge-functions/supabase/functions/og-image-with-storage-cdn/README.md +++ b/examples/edge-functions/supabase/functions/og-image-with-storage-cdn/README.md @@ -9,7 +9,7 @@ Generate Open Graph images with Deno and Supabase Edge Functions and cache the g ## Run locally ```bash -supabase functions serve og-image-with-storage-cdn --no-verify-jwt +supabase functions serve --no-verify-jwt ``` Navigate to http://localhost:54321/functions/v1/og-image-with-storage-cdn?ticketNumber=3524&username=thorwebdev&name=Thor%20%E9%9B%B7%E7%A5%9E%20Schaeff diff --git a/examples/edge-functions/supabase/functions/og-image-with-storage-cdn/handler.tsx b/examples/edge-functions/supabase/functions/og-image-with-storage-cdn/handler.tsx index 80ef24e2cc..202684370e 100644 --- a/examples/edge-functions/supabase/functions/og-image-with-storage-cdn/handler.tsx +++ b/examples/edge-functions/supabase/functions/og-image-with-storage-cdn/handler.tsx @@ -1,6 +1,6 @@ -import React from 'https://esm.sh/react@18.2.0?deno-std=0.140.0' -import { ImageResponse } from 'https://deno.land/x/og_edge@0.0.4/mod.ts' -import { createClient } from 'https://esm.sh/@supabase/supabase-js@2' +import React from 'React' +import { ImageResponse } from 'og_edge' +import { createClient } from '@supabase/supabase-js' import { corsHeaders } from '../_shared/cors.ts' const STORAGE_URL = 'https://obuldanrptloktxcffvn.supabase.co/storage/v1/object/public/images/lw6' @@ -213,7 +213,7 @@ export async function handler(req: Request) { }) if (error) throw error - return generatedImage + return await fetch(`${STORAGE_URL}/tickets/${username}.png?v=3`) } catch (error) { return new Response(JSON.stringify({ error: error.message }), { headers: { ...corsHeaders, 'Content-Type': 'application/json' }, diff --git a/examples/edge-functions/supabase/functions/og-image-with-storage-cdn/index.ts b/examples/edge-functions/supabase/functions/og-image-with-storage-cdn/index.ts index 5b2cf3da0b..0cf64b6e55 100644 --- a/examples/edge-functions/supabase/functions/og-image-with-storage-cdn/index.ts +++ b/examples/edge-functions/supabase/functions/og-image-with-storage-cdn/index.ts @@ -2,7 +2,7 @@ // https://deno.land/manual/getting_started/setup_your_environment // This enables autocomplete, go to definition, etc. -import { serve } from 'https://deno.land/std@0.140.0/http/server.ts' +import { serve } from 'std/server' import { handler } from './handler.tsx' diff --git a/examples/edge-functions/supabase/functions/openai/README.md b/examples/edge-functions/supabase/functions/openai/README.md index 4433aa6abb..33894bacb4 100644 --- a/examples/edge-functions/supabase/functions/openai/README.md +++ b/examples/edge-functions/supabase/functions/openai/README.md @@ -9,7 +9,7 @@ cp supabase/.env.local.example supabase/.env.local ## Run locally ```bash -supabase functions serve --no-verify-jwt --env-file ./supabase/.env.local openai +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. diff --git a/examples/edge-functions/supabase/functions/openai/index.ts b/examples/edge-functions/supabase/functions/openai/index.ts index 5bab2eafbb..a7418d73e1 100644 --- a/examples/edge-functions/supabase/functions/openai/index.ts +++ b/examples/edge-functions/supabase/functions/openai/index.ts @@ -1,6 +1,6 @@ -import 'https://deno.land/x/xhr@0.3.0/mod.ts' -import { serve } from 'https://deno.land/std@0.168.0/http/server.ts' -import { CreateCompletionRequest } from 'https://esm.sh/openai@3.1.0' +import 'xhr_polyfill' +import { serve } from 'std/server' +import { CreateCompletionRequest } from 'openai' serve(async (req) => { const { query } = await req.json() diff --git a/examples/edge-functions/supabase/functions/opengraph/README.md b/examples/edge-functions/supabase/functions/opengraph/README.md index 9242524394..866f222465 100644 --- a/examples/edge-functions/supabase/functions/opengraph/README.md +++ b/examples/edge-functions/supabase/functions/opengraph/README.md @@ -8,7 +8,7 @@ Generate Open Graph images with Deno and Supabase Edge Functions, no framework n ## Run locally ```bash -supabase functions serve opengraph --no-verify-jwt +supabase functions serve --no-verify-jwt ``` Navigate to http://localhost:54321/functions/v1/opengraph diff --git a/examples/edge-functions/supabase/functions/opengraph/handler.tsx b/examples/edge-functions/supabase/functions/opengraph/handler.tsx index ef64dc4b8e..d1bbcd5b2c 100644 --- a/examples/edge-functions/supabase/functions/opengraph/handler.tsx +++ b/examples/edge-functions/supabase/functions/opengraph/handler.tsx @@ -1,5 +1,5 @@ -import React from 'https://esm.sh/react@18.2.0?deno-std=0.140.0' -import { ImageResponse } from 'https://deno.land/x/og_edge@0.0.4/mod.ts' +import React from 'React' +import { ImageResponse } from 'og_edge' export function handler(req: Request) { return new ImageResponse( diff --git a/examples/edge-functions/supabase/functions/opengraph/index.ts b/examples/edge-functions/supabase/functions/opengraph/index.ts index 5a7c90237c..0a928de50a 100644 --- a/examples/edge-functions/supabase/functions/opengraph/index.ts +++ b/examples/edge-functions/supabase/functions/opengraph/index.ts @@ -2,7 +2,7 @@ // https://deno.land/manual/getting_started/setup_your_environment // This enables autocomplete, go to definition, etc. -import { serve } from 'https://deno.land/std@0.140.0/http/server.ts' +import { serve } from 'std/server' import { handler } from './handler.tsx' diff --git a/examples/edge-functions/supabase/functions/postgres-on-the-edge/index.ts b/examples/edge-functions/supabase/functions/postgres-on-the-edge/index.ts index 6ce14f6990..871d24679f 100644 --- a/examples/edge-functions/supabase/functions/postgres-on-the-edge/index.ts +++ b/examples/edge-functions/supabase/functions/postgres-on-the-edge/index.ts @@ -1,5 +1,5 @@ -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 * as postgres from 'postgres' +import { serve } from 'std/server' // Get the connection string from the environment variable "DATABASE_URL" const databaseUrl = Deno.env.get('DATABASE_URL')! diff --git a/examples/edge-functions/supabase/functions/puppeteer/README.md b/examples/edge-functions/supabase/functions/puppeteer/README.md index 353df27fd0..57e99a7478 100644 --- a/examples/edge-functions/supabase/functions/puppeteer/README.md +++ b/examples/edge-functions/supabase/functions/puppeteer/README.md @@ -4,6 +4,14 @@ This example shows how you can use Puppeteer and a headless-browser to generate Since Edge Functions cannot run a Headless Browser instance due to resource constraints, you will need to use a hosted browser service like https://browserless.io. +## Develop locally + +```bash +supabase functions serve --env-file ./supabase/.env.local --no-verify-jwt +``` + +Navigate to http://localhost:54321/functions/v1/puppeteer + ## Deploy ```bash diff --git a/examples/edge-functions/supabase/functions/puppeteer/index.ts b/examples/edge-functions/supabase/functions/puppeteer/index.ts index 7eac140f29..72efb56a72 100644 --- a/examples/edge-functions/supabase/functions/puppeteer/index.ts +++ b/examples/edge-functions/supabase/functions/puppeteer/index.ts @@ -1,11 +1,14 @@ -import { serve } from 'https://deno.land/std@0.131.0/http/server.ts' -import puppeteer from 'https://deno.land/x/puppeteer@16.2.0/mod.ts' +import { serve } from 'std/server' +import puppeteer from 'puppeteer' serve(async (req) => { try { + console.log(`wss://chrome.browserless.io?token=${Deno.env.get('PUPPETEER_BROWSERLESS_IO_KEY')}`) // Visit browserless.io to get your free API token const browser = await puppeteer.connect({ - browserWSEndpoint: 'wss://chrome.browserless.io?token=YOUR_API_TOKEN', + browserWSEndpoint: `wss://chrome.browserless.io?token=${Deno.env.get( + 'PUPPETEER_BROWSERLESS_IO_KEY' + )}`, }) const page = await browser.newPage() @@ -17,7 +20,7 @@ serve(async (req) => { return new Response(screenshot, { headers: { 'Content-Type': 'image/png' } }) } catch (e) { console.error(e) - return new Response(JSON.stringify(`Error occurred when generating the screenshot`), { + return new Response(JSON.stringify({ error: e.message }), { headers: { 'Content-Type': 'application/json' }, status: 500, }) diff --git a/examples/edge-functions/supabase/functions/read-storage/index.ts b/examples/edge-functions/supabase/functions/read-storage/index.ts index d7247b800d..c32ccfd91b 100644 --- a/examples/edge-functions/supabase/functions/read-storage/index.ts +++ b/examples/edge-functions/supabase/functions/read-storage/index.ts @@ -2,8 +2,8 @@ // https://deno.land/manual/getting_started/setup_your_environment // This enables autocomplete, go to definition, etc. -import { serve } from 'https://deno.land/std@0.131.0/http/server.ts' -import { createClient } from 'https://esm.sh/@supabase/supabase-js@2' +import { serve } from 'std/server' +import { createClient } from '@supabase/supabase-js' const corsHeaders = { 'Access-Control-Allow-Origin': '*', diff --git a/examples/edge-functions/supabase/functions/restful-tasks/index.ts b/examples/edge-functions/supabase/functions/restful-tasks/index.ts index 67c36ed0a9..eea5219a07 100644 --- a/examples/edge-functions/supabase/functions/restful-tasks/index.ts +++ b/examples/edge-functions/supabase/functions/restful-tasks/index.ts @@ -2,8 +2,8 @@ // https://deno.land/manual/getting_started/setup_your_environment // This enables autocomplete, go to definition, etc. -import { serve } from 'https://deno.land/std@0.131.0/http/server.ts' -import { createClient, SupabaseClient } from 'https://esm.sh/@supabase/supabase-js@2' +import { serve } from 'std/server' +import { createClient, SupabaseClient } from '@supabase/supabase-js' const corsHeaders = { 'Access-Control-Allow-Origin': '*', diff --git a/examples/edge-functions/supabase/functions/select-from-table-with-auth-rls/index.ts b/examples/edge-functions/supabase/functions/select-from-table-with-auth-rls/index.ts index f9dedb16a7..72b95575cb 100644 --- a/examples/edge-functions/supabase/functions/select-from-table-with-auth-rls/index.ts +++ b/examples/edge-functions/supabase/functions/select-from-table-with-auth-rls/index.ts @@ -2,8 +2,8 @@ // https://deno.land/manual/getting_started/setup_your_environment // This enables autocomplete, go to definition, etc. -import { serve } from 'https://deno.land/std@0.131.0/http/server.ts' -import { createClient } from 'https://esm.sh/@supabase/supabase-js@2' +import { serve } from 'std/server' +import { createClient } from '@supabase/supabase-js' import { corsHeaders } from '../_shared/cors.ts' console.log(`Function "select-from-table-with-auth-rls" up and running!`) diff --git a/examples/edge-functions/supabase/functions/send-email-smtp/README.md b/examples/edge-functions/supabase/functions/send-email-smtp/README.md index 02e6d5a6b0..68b7dbad82 100644 --- a/examples/edge-functions/supabase/functions/send-email-smtp/README.md +++ b/examples/edge-functions/supabase/functions/send-email-smtp/README.md @@ -13,4 +13,4 @@ Note: `SMTP_PORT` must be a port other than `25`, `465`, and `587` as Deno Deplo ## Test locally - `cp ./supabase/.env.local.example ./supabase/.env.local` -- `supabase functions serve send-email-smtp --env-file ./supabase/.env.local` \ No newline at end of file +- `supabase functions serve --env-file ./supabase/.env.local` diff --git a/examples/edge-functions/supabase/functions/send-email-smtp/index.ts b/examples/edge-functions/supabase/functions/send-email-smtp/index.ts index 4958b8fa15..d0abf11712 100644 --- a/examples/edge-functions/supabase/functions/send-email-smtp/index.ts +++ b/examples/edge-functions/supabase/functions/send-email-smtp/index.ts @@ -2,14 +2,14 @@ // https://deno.land/manual/getting_started/setup_your_environment // This enables autocomplete, go to definition, etc. -import { serve } from 'https://deno.land/std@0.131.0/http/server.ts' -import { SmtpClient } from 'https://deno.land/x/denomailer@0.12.0/mod.ts' +import { serve } from 'std/server' +import { SmtpClient } from 'denomailer' const smtp = new SmtpClient() console.log(`Function "send-email-smtp" up and running!`) -serve(async (req) => { +serve(async (_req) => { await smtp.connect({ hostname: Deno.env.get('SMTP_HOSTNAME')!, port: Number(Deno.env.get('SMTP_PORT')!), @@ -24,7 +24,7 @@ serve(async (req) => { subject: `Hello from Supabase Edge Functions`, content: `Hello Functions \o/`, }) - } catch (error: any) { + } catch (error) { return new Response(error.message, { status: 500 }) } @@ -41,7 +41,7 @@ serve(async (req) => { }) // To invoke: -// curl -i --location --request POST 'http://localhost:54321/functions/v1/' \ +// curl -i --location --request POST 'http://localhost:54321/functions/v1/send-email-smtp' \ // --header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24ifQ.625_WdcF3KHqz5amU0x2X5WWHP-OEs_4qj0ssLNHzTs' \ // --header 'Content-Type: application/json' \ // --data '{"name":"Functions"}' diff --git a/examples/edge-functions/supabase/functions/streams/README.md b/examples/edge-functions/supabase/functions/streams/README.md index 52cd230a36..1be0ec27b2 100644 --- a/examples/edge-functions/supabase/functions/streams/README.md +++ b/examples/edge-functions/supabase/functions/streams/README.md @@ -3,7 +3,7 @@ ## Run locally ```bash -supabase functions serve --no-verify-jwt streams +supabase functions serve --no-verify-jwt ``` Use cURL or Postman to make a GET request to http://localhost:54321/functions/v1/streams. diff --git a/examples/edge-functions/supabase/functions/streams/index.ts b/examples/edge-functions/supabase/functions/streams/index.ts index 15d3c2ace8..dafbf83c7d 100644 --- a/examples/edge-functions/supabase/functions/streams/index.ts +++ b/examples/edge-functions/supabase/functions/streams/index.ts @@ -1,4 +1,4 @@ -import { serve } from 'https://deno.land/std@0.168.0/http/server.ts' +import { serve } from 'std/server' const msg = new TextEncoder().encode('data: hello\r\n\r\n') diff --git a/examples/edge-functions/supabase/functions/stripe-webhooks/README.md b/examples/edge-functions/supabase/functions/stripe-webhooks/README.md index 2d5a7a24d5..337352277f 100644 --- a/examples/edge-functions/supabase/functions/stripe-webhooks/README.md +++ b/examples/edge-functions/supabase/functions/stripe-webhooks/README.md @@ -9,7 +9,7 @@ Also check out our full Stripe Payments examples for [React Native (Expo)](https ## Test locally - Terminal 1: - - `supabase functions serve --no-verify-jwt stripe-webhooks --env-file ./supabase/.env.local` + - `supabase functions serve --no-verify-jwt --env-file ./supabase/.env.local` - Terminal 2: - `stripe listen --forward-to localhost:54321/functions/v1/` - Terminal 3 (optional): diff --git a/examples/edge-functions/supabase/functions/stripe-webhooks/index.ts b/examples/edge-functions/supabase/functions/stripe-webhooks/index.ts index 6b159a2a5b..a0bbb7dc95 100644 --- a/examples/edge-functions/supabase/functions/stripe-webhooks/index.ts +++ b/examples/edge-functions/supabase/functions/stripe-webhooks/index.ts @@ -2,20 +2,21 @@ // https://deno.land/manual/getting_started/setup_your_environment // This enables autocomplete, go to definition, etc. -import { serve } from 'https://deno.land/std@0.132.0/http/server.ts' +import { serve } from 'std/server' -// esm.sh is used to compile stripe-node to be compatible with ES modules. -import Stripe from 'https://esm.sh/stripe@10.13.0?target=deno&no-check&deno-std=0.132.0' +// Import via bare specifier thanks to the import_map.json file. +import Stripe from 'stripe' -const stripe = Stripe(Deno.env.get('STRIPE_API_KEY'), { +const stripe = new Stripe(Deno.env.get('STRIPE_API_KEY') as string, { // This is needed to use the Fetch API rather than relying on the Node http // package. + apiVersion: '2022-11-15', httpClient: Stripe.createFetchHttpClient(), }) // This is needed in order to use the Web Crypto API in Deno. const cryptoProvider = Stripe.createSubtleCryptoProvider() -console.log(`Function "stripe-webhooks" up and running!`) +console.log('Hello from Stripe Webhook!') serve(async (request) => { const signature = request.headers.get('Stripe-Signature') @@ -27,8 +28,8 @@ serve(async (request) => { try { receivedEvent = await stripe.webhooks.constructEventAsync( body, - signature, - Deno.env.get('STRIPE_WEBHOOK_SIGNING_SECRET'), + signature!, + Deno.env.get('STRIPE_WEBHOOK_SIGNING_SECRET')!, undefined, cryptoProvider ) @@ -36,25 +37,5 @@ serve(async (request) => { return new Response(err.message, { status: 400 }) } console.log(`🔔 Event received: ${receivedEvent.id}`) - - // Secondly, we use this event to query the Stripe API in order to avoid - // handling any forged event. If available, we use the idempotency key. - const requestOptions = - receivedEvent.request && receivedEvent.request.idempotency_key - ? { - idempotencyKey: receivedEvent.request.idempotency_key, - } - : {} - - let retrievedEvent - try { - retrievedEvent = await stripe.events.retrieve(receivedEvent.id, requestOptions) - } catch (err) { - return new Response(err.message, { status: 400 }) - } - - return new Response(JSON.stringify({ id: retrievedEvent.id, status: 'ok' }), { - status: 200, - headers: { 'Content-Type': 'application/json' }, - }) + return new Response(JSON.stringify({ ok: true }), { status: 200 }) }) diff --git a/examples/edge-functions/supabase/functions/telegram-bot/index.ts b/examples/edge-functions/supabase/functions/telegram-bot/index.ts index eab5aed9fd..ecaa991632 100644 --- a/examples/edge-functions/supabase/functions/telegram-bot/index.ts +++ b/examples/edge-functions/supabase/functions/telegram-bot/index.ts @@ -2,34 +2,28 @@ // https://deno.land/manual/getting_started/setup_your_environment // This enables autocomplete, go to definition, etc. -import { serve } from "https://deno.land/std@0.131.0/http/server.ts" +import { serve } from 'std/server' console.log(`Function "telegram-bot" up and running!`) -import { Bot, webhookCallback } from "https://deno.land/x/grammy@v1.8.3/mod.ts"; +import { Bot, webhookCallback } from 'grammy' -const bot = new Bot(Deno.env.get('TELEGRAM_BOT_TOKEN') || ''); +const bot = new Bot(Deno.env.get('TELEGRAM_BOT_TOKEN') || '') -bot.command("start", (ctx) => ctx.reply("Welcome! Up and running.")); +bot.command('start', (ctx) => ctx.reply('Welcome! Up and running.')) bot.command('ping', (ctx) => ctx.reply(`Pong! ${new Date()} ${Date.now()}`)) -const handleUpdate = webhookCallback(bot, "std/http"); +const handleUpdate = webhookCallback(bot, 'std/http') serve(async (req) => { try { - const url = new URL(req.url); - if (url.searchParams.get('secret') !== Deno.env.get('FUNCTION_SECRET')) + const url = new URL(req.url) + if (url.searchParams.get('secret') !== Deno.env.get('FUNCTION_SECRET')) return new Response('not allowed', { status: 405 }) - return await handleUpdate(req); + return await handleUpdate(req) } catch (err) { - console.error(err); + console.error(err) } }) - -// To invoke: -// curl -i --location --request POST 'http://localhost:54321/functions/v1/' \ -// --header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24ifQ.625_WdcF3KHqz5amU0x2X5WWHP-OEs_4qj0ssLNHzTs' \ -// --header 'Content-Type: application/json' \ -// --data '{"name":"Functions"}' diff --git a/examples/edge-functions/supabase/functions/upstash-redis-counter/.env.example b/examples/edge-functions/supabase/functions/upstash-redis-counter/.env.example deleted file mode 100644 index d0785c1997..0000000000 --- a/examples/edge-functions/supabase/functions/upstash-redis-counter/.env.example +++ /dev/null @@ -1,2 +0,0 @@ -UPSTASH_REDIS_REST_URL= -UPSTASH_REDIS_REST_TOKEN= \ No newline at end of file diff --git a/examples/edge-functions/supabase/functions/upstash-redis-counter/README.md b/examples/edge-functions/supabase/functions/upstash-redis-counter/README.md index 3d33a9e7dd..dd854b5a07 100644 --- a/examples/edge-functions/supabase/functions/upstash-redis-counter/README.md +++ b/examples/edge-functions/supabase/functions/upstash-redis-counter/README.md @@ -9,7 +9,7 @@ Create a Redis database using the [Upstash Console](https://console.upstash.com/ Select the `Global` type to minimize the latency from all edge locations. Copy the `UPSTASH_REDIS_REST_URL` and `UPSTASH_REDIS_REST_TOKEN` to your .env file. You'll find them under **Details > REST API > .env**. ```bash -cp supabase/functions/upstash-redis-counter/.env.example supabase/functions/upstash-redis-counter/.env +cp supabase/.env.local.example supabase/.env.local ``` ## Run locally @@ -18,7 +18,7 @@ Make sure you have the latest version of the [Supabase CLI installed](https://su ```bash supabase start -supabase functions serve upstash-redis-counter --no-verify-jwt --env-file supabase/functions/upstash-redis-counter/.env +supabase functions serve --no-verify-jwt --env-file supabase/.env.local ``` Navigate to http://localhost:54321/functions/v1/upstash-redis-counter. @@ -27,5 +27,5 @@ Navigate to http://localhost:54321/functions/v1/upstash-redis-counter. ```bash supabase functions deploy upstash-redis-counter --no-verify-jwt -supabase secrets set --env-file supabase/functions/upstash-redis-counter/.env +supabase secrets set --env-file supabase/.env.local ``` diff --git a/examples/edge-functions/supabase/functions/upstash-redis-counter/index.ts b/examples/edge-functions/supabase/functions/upstash-redis-counter/index.ts index 911424209c..bf630d0287 100644 --- a/examples/edge-functions/supabase/functions/upstash-redis-counter/index.ts +++ b/examples/edge-functions/supabase/functions/upstash-redis-counter/index.ts @@ -2,8 +2,8 @@ // https://deno.land/manual/getting_started/setup_your_environment // This enables autocomplete, go to definition, etc. -import { serve } from 'https://deno.land/std@0.177.0/http/server.ts' -import { Redis } from 'https://deno.land/x/upstash_redis@v1.19.3/mod.ts' +import { serve } from 'std/server' +import { Redis } from 'upstash_redis' console.log(`Function "upstash-redis-counter" up and running!`) diff --git a/examples/todo-list/nextjs-todo-list/.env.local.example b/examples/todo-list/nextjs-todo-list/.env.local.example new file mode 100644 index 0000000000..477da3d401 --- /dev/null +++ b/examples/todo-list/nextjs-todo-list/.env.local.example @@ -0,0 +1,3 @@ +# Update these with your Supabase details from your project settings > API +NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co +NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key \ No newline at end of file diff --git a/examples/todo-list/nextjs-todo-list/.eslintrc.json b/examples/todo-list/nextjs-todo-list/.eslintrc.json new file mode 100644 index 0000000000..bffb357a71 --- /dev/null +++ b/examples/todo-list/nextjs-todo-list/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "next/core-web-vitals" +} diff --git a/examples/todo-list/nextjs-todo-list/.gitignore b/examples/todo-list/nextjs-todo-list/.gitignore new file mode 100644 index 0000000000..c87c9b392c --- /dev/null +++ b/examples/todo-list/nextjs-todo-list/.gitignore @@ -0,0 +1,36 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/examples/todo-list/nextjs-todo-list/README.md b/examples/todo-list/nextjs-todo-list/README.md new file mode 100644 index 0000000000..83ba22e28b --- /dev/null +++ b/examples/todo-list/nextjs-todo-list/README.md @@ -0,0 +1,75 @@ +# Todo example using Supabase + +- Frontend: + - [Next.js](https://github.com/vercel/next.js) - a React framework for production. + - [Tailwind](https://tailwindcss.com/) for styling and layout. + - [Supabase.js](https://supabase.com/docs/library/getting-started) for user management and realtime data syncing. +- Backend: + - [app.supabase.com](https://app.supabase.com/): hosted Postgres database with restful API for usage with Supabase.js. + +## Deploy with Vercel + +The Vercel deployment will guide you through creating a Supabase account and project. After installation of the Supabase integration, all relevant environment variables will be set up so that the project is usable immediately after deployment 🚀 + +[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/git/external?repository-url=https%3A%2F%2Fgithub.com%2Fsupabase%2Fexamples%2Ftree%2Fmain%2Fsupabase-js-v1%2Ftodo-list%2Fnextjs-todo-list&project-name=supabase-todo-list&repository-name=supabase-todo-list&demo-title=Todo%20list&demo-description=An%20example%20web%20app%20using%20Supabase%20and%20Next.js&demo-url=https%3A%2F%2Fsupabase-nextjs-todo-list.vercel.app&demo-image=https%3A%2F%2Fi.imgur.com%2FGJauPlN.png&integration-ids=oac_jUduyjQgOyzev1fjrW83NYOv&external-id=supabase-todo-list) + +## Build from scratch + +### 1. Create new project + +Sign up to Supabase - [https://app.supabase.com](https://app.supabase.com) and create a new project. Wait for your database to start. + +### 2. Run "Todo List" Quickstart + +Once your database has started, run the "Todo List" quickstart. Inside of your project, enter the `SQL editor` tab and scroll down until you see `TODO LIST: Build a basic todo list with Row Level Security`. + +### 3. Get the URL and Key + +Go to the Project Settings (the cog icon), open the API tab, and find your API URL and `anon` key, you'll need these in the next step. + +The `anon` key is your client-side API key. It allows "anonymous access" to your database, until the user has logged in. Once they have logged in, the keys will switch to the user's own login token. This enables row level security for your data. Read more about this [below](#postgres-row-level-security). + +![image](https://user-images.githubusercontent.com/10214025/88916245-528c2680-d298-11ea-8a71-708f93e1ce4f.png) + +**_NOTE_**: The `service_role` key has full access to your data, bypassing any security policies. These keys have to be kept secret and are meant to be used in server environments and never on a client or browser. + +## Supabase details + +### Postgres Row level security + +This project uses very high-level Authorization using Postgres' Role Level Security. +When you start a Postgres database on Supabase, we populate it with an `auth` schema, and some helper functions. +When a user logs in, they are issued a JWT with the role `authenticated` and their UUID. +We can use these details to provide fine-grained control over what each user can and cannot do. + +This is a trimmed-down schema, with the policies: + +```sql +create table todos ( + id bigint generated by default as identity primary key, + user_id uuid references auth.users not null, + task text check (char_length(task) > 3), + is_complete boolean default false, + inserted_at timestamp with time zone default timezone('utc'::text, now()) not null +); + +alter table todos enable row level security; + +create policy "Individuals can create todos." on todos for + insert with check (auth.uid() = user_id); + +create policy "Individuals can view their own todos. " on todos for + select using (auth.uid() = user_id); + +create policy "Individuals can update their own todos." on todos for + update using (auth.uid() = user_id); + +create policy "Individuals can delete their own todos." on todos for + delete using (auth.uid() = user_id); +``` + +## Authors + +- [Supabase](https://supabase.com) + +Supabase is open source. We'd love for you to follow along and get involved at https://github.com/supabase/supabase diff --git a/examples/todo-list/nextjs-todo-list/components/TodoList.tsx b/examples/todo-list/nextjs-todo-list/components/TodoList.tsx new file mode 100644 index 0000000000..6d060b5a96 --- /dev/null +++ b/examples/todo-list/nextjs-todo-list/components/TodoList.tsx @@ -0,0 +1,151 @@ +import { Database } from '@/lib/schema' +import { Session, useSupabaseClient } from '@supabase/auth-helpers-react' +import { useEffect, useState } from 'react' + +type Todos = Database['public']['Tables']['todos']['Row'] + +export default function TodoList({ session }: { session: Session }) { + const supabase = useSupabaseClient() + const [todos, setTodos] = useState([]) + const [newTaskText, setNewTaskText] = useState('') + const [errorText, setErrorText] = useState('') + + const user = session.user + + useEffect(() => { + const fetchTodos = async () => { + const { data: todos, error } = await supabase + .from('todos') + .select('*') + .order('id', { ascending: true }) + + if (error) console.log('error', error) + else setTodos(todos) + } + + fetchTodos() + }, [supabase]) + + const addTodo = async (taskText: string) => { + let task = taskText.trim() + if (task.length) { + const { data: todo, error } = await supabase + .from('todos') + .insert({ task, user_id: user.id }) + .select() + .single() + + if (error) { + setErrorText(error.message) + } else { + setTodos([...todos, todo]) + setNewTaskText('') + } + } + } + + const deleteTodo = async (id: number) => { + try { + await supabase.from('todos').delete().eq('id', id).throwOnError() + setTodos(todos.filter((x) => x.id != id)) + } catch (error) { + console.log('error', error) + } + } + + return ( +
+

Todo List.

+
{ + e.preventDefault() + addTodo(newTaskText) + }} + className="flex gap-2 my-2" + > + { + setErrorText('') + setNewTaskText(e.target.value) + }} + /> + +
+ {!!errorText && } +
+
    + {todos.map((todo) => ( + deleteTodo(todo.id)} /> + ))} +
+
+
+ ) +} + +const Todo = ({ todo, onDelete }: { todo: Todos; onDelete: () => void }) => { + const supabase = useSupabaseClient() + const [isCompleted, setIsCompleted] = useState(todo.is_complete) + + const toggle = async () => { + try { + const { data } = await supabase + .from('todos') + .update({ is_complete: !isCompleted }) + .eq('id', todo.id) + .throwOnError() + .select() + .single() + + if (data) setIsCompleted(data.is_complete) + } catch (error) { + console.log('error', error) + } + } + + return ( +
  • +
    +
    +
    {todo.task}
    +
    +
    + toggle()} + type="checkbox" + checked={isCompleted ? true : false} + /> +
    + +
    +
  • + ) +} + +const Alert = ({ text }: { text: string }) => ( +
    +
    {text}
    +
    +) diff --git a/examples/todo-list/nextjs-todo-list/lib/initSupabase.ts b/examples/todo-list/nextjs-todo-list/lib/initSupabase.ts new file mode 100644 index 0000000000..5457bdff41 --- /dev/null +++ b/examples/todo-list/nextjs-todo-list/lib/initSupabase.ts @@ -0,0 +1,7 @@ +import { createClient } from '@supabase/supabase-js' +import { Database } from './schema' + +export const supabase = createClient( + process.env.NEXT_PUBLIC_SUPABASE_URL, + process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY +) diff --git a/examples/todo-list/nextjs-todo-list/lib/schema.ts b/examples/todo-list/nextjs-todo-list/lib/schema.ts new file mode 100644 index 0000000000..ec8b8f854b --- /dev/null +++ b/examples/todo-list/nextjs-todo-list/lib/schema.ts @@ -0,0 +1,49 @@ +export type Json = + | string + | number + | boolean + | null + | { [key: string]: Json } + | Json[] + +export interface Database { + public: { + Tables: { + todos: { + Row: { + id: number + inserted_at: string + is_complete: boolean | null + task: string | null + user_id: string + } + Insert: { + id?: number + inserted_at?: string + is_complete?: boolean | null + task?: string | null + user_id: string + } + Update: { + id?: number + inserted_at?: string + is_complete?: boolean | null + task?: string | null + user_id?: string + } + } + } + Views: { + [_ in never]: never + } + Functions: { + [_ in never]: never + } + Enums: { + [_ in never]: never + } + CompositeTypes: { + [_ in never]: never + } + } +} diff --git a/examples/todo-list/nextjs-todo-list/next.config.js b/examples/todo-list/nextjs-todo-list/next.config.js new file mode 100644 index 0000000000..a843cbee09 --- /dev/null +++ b/examples/todo-list/nextjs-todo-list/next.config.js @@ -0,0 +1,6 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + reactStrictMode: true, +} + +module.exports = nextConfig diff --git a/examples/todo-list/nextjs-todo-list/package-lock.json b/examples/todo-list/nextjs-todo-list/package-lock.json new file mode 100644 index 0000000000..98500e935e --- /dev/null +++ b/examples/todo-list/nextjs-todo-list/package-lock.json @@ -0,0 +1,4276 @@ +{ + "name": "nextjs-todo-list", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "nextjs-todo-list", + "version": "0.1.0", + "dependencies": { + "@next/font": "13.1.6", + "@supabase/auth-helpers-react": "^0.3.1", + "@supabase/auth-ui-react": "^0.2.8", + "@supabase/supabase-js": "^2.8.0", + "@types/node": "18.14.0", + "@types/react": "18.0.28", + "@types/react-dom": "18.0.11", + "eslint": "8.34.0", + "eslint-config-next": "13.1.6", + "next": "13.1.6", + "react": "18.2.0", + "react-dom": "18.2.0", + "typescript": "4.9.5" + }, + "devDependencies": { + "concurrently": "^7.6.0", + "tailwindcss": "^3.2.7" + } + }, + "node_modules/@babel/runtime": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.21.0.tgz", + "integrity": "sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw==", + "dependencies": { + "regenerator-runtime": "^0.13.11" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.4.1.tgz", + "integrity": "sha512-XXrH9Uarn0stsyldqDYq8r++mROmWRI1xKMXa640Bb//SY1+ECYX6VzT6Lcx5frD0V30XieqJ0oX9I2Xj5aoMA==", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.4.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.8", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz", + "integrity": "sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g==", + "dependencies": { + "@humanwhocodes/object-schema": "^1.2.1", + "debug": "^4.1.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", + "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==" + }, + "node_modules/@next/env": { + "version": "13.1.6", + "resolved": "https://registry.npmjs.org/@next/env/-/env-13.1.6.tgz", + "integrity": "sha512-s+W9Fdqh5MFk6ECrbnVmmAOwxKQuhGMT7xXHrkYIBMBcTiOqNWhv5KbJIboKR5STXxNXl32hllnvKaffzFaWQg==" + }, + "node_modules/@next/eslint-plugin-next": { + "version": "13.1.6", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-13.1.6.tgz", + "integrity": "sha512-o7cauUYsXjzSJkay8wKjpKJf2uLzlggCsGUkPu3lP09Pv97jYlekTC20KJrjQKmSv5DXV0R/uks2ZXhqjNkqAw==", + "dependencies": { + "glob": "7.1.7" + } + }, + "node_modules/@next/font": { + "version": "13.1.6", + "resolved": "https://registry.npmjs.org/@next/font/-/font-13.1.6.tgz", + "integrity": "sha512-AITjmeb1RgX1HKMCiA39ztx2mxeAyxl4ljv2UoSBUGAbFFMg8MO7YAvjHCgFhD39hL7YTbFjol04e/BPBH5RzQ==" + }, + "node_modules/@next/swc-android-arm-eabi": { + "version": "13.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-android-arm-eabi/-/swc-android-arm-eabi-13.1.6.tgz", + "integrity": "sha512-F3/6Z8LH/pGlPzR1AcjPFxx35mPqjE5xZcf+IL+KgbW9tMkp7CYi1y7qKrEWU7W4AumxX/8OINnDQWLiwLasLQ==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-android-arm64": { + "version": "13.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-android-arm64/-/swc-android-arm64-13.1.6.tgz", + "integrity": "sha512-cMwQjnB8vrYkWyK/H0Rf2c2pKIH4RGjpKUDvbjVAit6SbwPDpmaijLio0LWFV3/tOnY6kvzbL62lndVA0mkYpw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "13.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.1.6.tgz", + "integrity": "sha512-KKRQH4DDE4kONXCvFMNBZGDb499Hs+xcFAwvj+rfSUssIDrZOlyfJNy55rH5t2Qxed1e4K80KEJgsxKQN1/fyw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "13.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-13.1.6.tgz", + "integrity": "sha512-/uOky5PaZDoaU99ohjtNcDTJ6ks/gZ5ykTQDvNZDjIoCxFe3+t06bxsTPY6tAO6uEAw5f6vVFX5H5KLwhrkZCA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-freebsd-x64": { + "version": "13.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-freebsd-x64/-/swc-freebsd-x64-13.1.6.tgz", + "integrity": "sha512-qaEALZeV7to6weSXk3Br80wtFQ7cFTpos/q+m9XVRFggu+8Ib895XhMWdJBzew6aaOcMvYR6KQ6JmHA2/eMzWw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm-gnueabihf": { + "version": "13.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm-gnueabihf/-/swc-linux-arm-gnueabihf-13.1.6.tgz", + "integrity": "sha512-OybkbC58A1wJ+JrJSOjGDvZzrVEQA4sprJejGqMwiZyLqhr9Eo8FXF0y6HL+m1CPCpPhXEHz/2xKoYsl16kNqw==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "13.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.1.6.tgz", + "integrity": "sha512-yCH+yDr7/4FDuWv6+GiYrPI9kcTAO3y48UmaIbrKy8ZJpi7RehJe3vIBRUmLrLaNDH3rY1rwoHi471NvR5J5NQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "13.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.1.6.tgz", + "integrity": "sha512-ECagB8LGX25P9Mrmlc7Q/TQBb9rGScxHbv/kLqqIWs2fIXy6Y/EiBBiM72NTwuXUFCNrWR4sjUPSooVBJJ3ESQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "13.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.1.6.tgz", + "integrity": "sha512-GT5w2mruk90V/I5g6ScuueE7fqj/d8Bui2qxdw6lFxmuTgMeol5rnzAv4uAoVQgClOUO/MULilzlODg9Ib3Y4Q==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "13.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.1.6.tgz", + "integrity": "sha512-keFD6KvwOPzmat4TCnlnuxJCQepPN+8j3Nw876FtULxo8005Y9Ghcl7ACcR8GoiKoddAq8gxNBrpjoxjQRHeAQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "13.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.1.6.tgz", + "integrity": "sha512-OwertslIiGQluFvHyRDzBCIB07qJjqabAmINlXUYt7/sY7Q7QPE8xVi5beBxX/rxTGPIbtyIe3faBE6Z2KywhQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-ia32-msvc": { + "version": "13.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.1.6.tgz", + "integrity": "sha512-g8zowiuP8FxUR9zslPmlju7qYbs2XBtTLVSxVikPtUDQedhcls39uKYLvOOd1JZg0ehyhopobRoH1q+MHlIN/w==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "13.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.1.6.tgz", + "integrity": "sha512-Ls2OL9hi3YlJKGNdKv8k3X/lLgc3VmLG3a/DeTkAd+lAituJp8ZHmRmm9f9SL84fT3CotlzcgbdaCDfFwFA6bA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@pkgr/utils": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@pkgr/utils/-/utils-2.3.1.tgz", + "integrity": "sha512-wfzX8kc1PMyUILA+1Z/EqoE4UCXGy0iRGMhPwdfae1+f0OXlLqCk+By+aMzgJBzR9AzS4CDizioG6Ss1gvAFJw==", + "dependencies": { + "cross-spawn": "^7.0.3", + "is-glob": "^4.0.3", + "open": "^8.4.0", + "picocolors": "^1.0.0", + "tiny-glob": "^0.2.9", + "tslib": "^2.4.0" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, + "node_modules/@rushstack/eslint-patch": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.2.0.tgz", + "integrity": "sha512-sXo/qW2/pAcmT43VoRKOJbDOfV3cYpq3szSVfIThQXNt+E4DfKj361vaAt3c88U5tPUxzEswam7GW48PJqtKAg==" + }, + "node_modules/@stitches/core": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@stitches/core/-/core-1.2.8.tgz", + "integrity": "sha512-Gfkvwk9o9kE9r9XNBmJRfV8zONvXThnm1tcuojL04Uy5uRyqg93DC83lDebl0rocZCfKSjUv+fWYtMQmEDJldg==" + }, + "node_modules/@stitches/react": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@stitches/react/-/react-1.2.8.tgz", + "integrity": "sha512-9g9dWI4gsSVe8bNLlb+lMkBYsnIKCZTmvqvDG+Avnn69XfmHZKiaMrx7cgTaddq7aTPPmXiTsbFcUy0xgI4+wA==", + "peerDependencies": { + "react": ">= 16.3.0" + } + }, + "node_modules/@supabase/auth-helpers-react": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@supabase/auth-helpers-react/-/auth-helpers-react-0.3.1.tgz", + "integrity": "sha512-g3SFv08Dz9FapNif/ZY1b7qKGlMJDyTLSayHBz3kb3FuYxg7aLWgQtydDhm5AGbc0XtvpIBuhGTIOVevwpdosA==", + "peerDependencies": { + "@supabase/supabase-js": "^2.0.4" + } + }, + "node_modules/@supabase/auth-ui-react": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/@supabase/auth-ui-react/-/auth-ui-react-0.2.8.tgz", + "integrity": "sha512-CfN9Zt2t1PMfri8htLCFYYIWho6F15ZvbWICE3RuOZ348z1P15iP6scjxB3704Jbu334Q+ykCQP+UzdnB7cXHQ==", + "dependencies": { + "@stitches/core": "^1.2.8", + "@stitches/react": "^1.2.8", + "prop-types": "^15.7.2" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.1 || ^18.0.0", + "react-dom": "^16.13.1 || ^17.0.1 || ^18.0.0" + } + }, + "node_modules/@supabase/functions-js": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.0.0.tgz", + "integrity": "sha512-ozb7bds2yvf5k7NM2ZzUkxvsx4S4i2eRKFSJetdTADV91T65g4gCzEs9L3LUXSrghcGIkUaon03VPzOrFredqg==", + "dependencies": { + "cross-fetch": "^3.1.5" + } + }, + "node_modules/@supabase/gotrue-js": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/@supabase/gotrue-js/-/gotrue-js-2.12.1.tgz", + "integrity": "sha512-r8Jfq8FvP6q4kp7sI33X1RWfEEHzJFu9uM1Q6HgiDVkY89NNgqYy2kxaRGtidPFllND7vpcJUcpoWS5oq+4u0g==", + "dependencies": { + "cross-fetch": "^3.1.5" + } + }, + "node_modules/@supabase/postgrest-js": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-1.4.1.tgz", + "integrity": "sha512-aruqwV/aTggkM7OVv2JinCeXmRMKHJCZpkuS1nuoa0NgLw7g3NyILSyWOKYTBJ/PxE/zXtWsBhdxFzaaNz5uxg==", + "dependencies": { + "cross-fetch": "^3.1.5" + } + }, + "node_modules/@supabase/realtime-js": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.6.0.tgz", + "integrity": "sha512-tOVulMobhpxyDuu8VIImpL8FXmZOKsGNOSyS5ihJdj2xYmPPvYG+D2J51Ewfl+MFF65tweiB6p9N9bNIW1cDNA==", + "dependencies": { + "@types/phoenix": "^1.5.4", + "websocket": "^1.0.34" + } + }, + "node_modules/@supabase/storage-js": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.3.0.tgz", + "integrity": "sha512-YGWVCEYYYF3+UiyL8O4xC78N9n9paLbT0hHl8dmYAtd3DqyWtu5Eph9JTu0PWm+/29Zhns5TbhUZW4xpWjJfPQ==", + "dependencies": { + "cross-fetch": "^3.1.5" + } + }, + "node_modules/@supabase/supabase-js": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.8.0.tgz", + "integrity": "sha512-uzf4J+qAKdUMhB2tnJl6BrQRUQBinwjJ2eWo2ZsDw9EUUP5JcHsxTamiq6p91DpqzmTIRg3xRAT+bItTzbfa0w==", + "dependencies": { + "@supabase/functions-js": "^2.0.0", + "@supabase/gotrue-js": "^2.12.0", + "@supabase/postgrest-js": "^1.1.1", + "@supabase/realtime-js": "^2.4.0", + "@supabase/storage-js": "^2.1.0", + "cross-fetch": "^3.1.5" + } + }, + "node_modules/@swc/helpers": { + "version": "0.4.14", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.4.14.tgz", + "integrity": "sha512-4C7nX/dvpzB7za4Ql9K81xK3HPxCpHMgwTZVyf+9JQ6VUbn9jjZVN7/Nkdz/Ugzs2CSjqnL/UPXroiVBVHUWUw==", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==" + }, + "node_modules/@types/node": { + "version": "18.14.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.14.0.tgz", + "integrity": "sha512-5EWrvLmglK+imbCJY0+INViFWUHg1AHel1sq4ZVSfdcNqGy9Edv3UB9IIzzg+xPaUcAgZYcfVs2fBcwDeZzU0A==" + }, + "node_modules/@types/phoenix": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.5.5.tgz", + "integrity": "sha512-1eWWT19k0L4ZiTvdXjAvJ9KvW0B8SdiVftQmFPJGTEx78Q4PCSIQDpz+EfkFVR1N4U9gREjlW4JXL8YCIlY0bw==" + }, + "node_modules/@types/prop-types": { + "version": "15.7.5", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", + "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==" + }, + "node_modules/@types/react": { + "version": "18.0.28", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.28.tgz", + "integrity": "sha512-RD0ivG1kEztNBdoAK7lekI9M+azSnitIn85h4iOiaLjaTrMjzslhaqCGaI4IyCJ1RljWiLCEu4jyrLLgqxBTew==", + "dependencies": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.0.11", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.0.11.tgz", + "integrity": "sha512-O38bPbI2CWtgw/OoQoY+BRelw7uysmXbWvw3nLWO21H1HSh+GOlqPuXshJfjmpNlKiiSDG9cc1JZAaMmVdcTlw==", + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/scheduler": { + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", + "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==" + }, + "node_modules/@typescript-eslint/parser": { + "version": "5.53.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.53.0.tgz", + "integrity": "sha512-MKBw9i0DLYlmdOb3Oq/526+al20AJZpANdT6Ct9ffxcV8nKCHz63t/S0IhlTFNsBIHJv+GY5SFJ0XfqVeydQrQ==", + "dependencies": { + "@typescript-eslint/scope-manager": "5.53.0", + "@typescript-eslint/types": "5.53.0", + "@typescript-eslint/typescript-estree": "5.53.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "5.53.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.53.0.tgz", + "integrity": "sha512-Opy3dqNsp/9kBBeCPhkCNR7fmdSQqA+47r21hr9a14Bx0xnkElEQmhoHga+VoaoQ6uDHjDKmQPIYcUcKJifS7w==", + "dependencies": { + "@typescript-eslint/types": "5.53.0", + "@typescript-eslint/visitor-keys": "5.53.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "5.53.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.53.0.tgz", + "integrity": "sha512-5kcDL9ZUIP756K6+QOAfPkigJmCPHcLN7Zjdz76lQWWDdzfOhZDTj1irs6gPBKiXx5/6O3L0+AvupAut3z7D2A==", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "5.53.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.53.0.tgz", + "integrity": "sha512-eKmipH7QyScpHSkhbptBBYh9v8FxtngLquq292YTEQ1pxVs39yFBlLC1xeIZcPPz1RWGqb7YgERJRGkjw8ZV7w==", + "dependencies": { + "@typescript-eslint/types": "5.53.0", + "@typescript-eslint/visitor-keys": "5.53.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "5.53.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.53.0.tgz", + "integrity": "sha512-JqNLnX3leaHFZEN0gCh81sIvgrp/2GOACZNgO4+Tkf64u51kTpAyWFOY8XHx8XuXr3N2C9zgPPHtcpMg6z1g0w==", + "dependencies": { + "@typescript-eslint/types": "5.53.0", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/acorn": { + "version": "8.8.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", + "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-node": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/acorn-node/-/acorn-node-1.8.2.tgz", + "integrity": "sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A==", + "dev": true, + "dependencies": { + "acorn": "^7.0.0", + "acorn-walk": "^7.0.0", + "xtend": "^4.0.2" + } + }, + "node_modules/acorn-node/node_modules/acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz", + "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "node_modules/aria-query": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz", + "integrity": "sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==", + "dependencies": { + "deep-equal": "^2.0.5" + } + }, + "node_modules/array-includes": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.6.tgz", + "integrity": "sha512-sgTbLvL6cNnw24FnbaDyjmvddQ2ML8arZsgaJhoABMoplz/4QRhtrYS+alr1BUM1Bwp6dhx8vVCBSLG+StwOFw==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4", + "get-intrinsic": "^1.1.3", + "is-string": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "engines": { + "node": ">=8" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.1.tgz", + "integrity": "sha512-roTU0KWIOmJ4DRLmwKd19Otg0/mT3qPNt0Qb3GWW8iObuZXxrjB/pzn0R3hqpRSWg4HCwqx+0vwOnWnvlOyeIA==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.1.tgz", + "integrity": "sha512-8UGn9O1FDVvMNB0UlLv4voxRMze7+FpHyF5mSMRjWHUMlpoDViniy05870VlxhfgTnLbpuwTzvD76MTtWxB/mQ==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.tosorted": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.1.tgz", + "integrity": "sha512-pZYPXPRl2PqWcsUs6LOMn+1f1532nEoPTYowBtqLwAW+W8vSVhkIGnmOX1t/UQjD6YGI0vcD2B1U7ZFGQH9jnQ==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4", + "es-shim-unscopables": "^1.0.0", + "get-intrinsic": "^1.1.3" + } + }, + "node_modules/ast-types-flow": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz", + "integrity": "sha512-eBvWn1lvIApYMhzQMsu9ciLfkBY499mFZlNqG+/9WR7PVlroQw0vG30cOQQbaKz3sCEc44TAOu2ykzqXSNnwag==" + }, + "node_modules/available-typed-arrays": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", + "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/axe-core": { + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.6.3.tgz", + "integrity": "sha512-/BQzOX780JhsxDnPpH4ZiyrJAzcd8AfzFPkv+89veFSr1rcMjuq2JDCwypKaPeB6ljHp9KjXhPpjgCvQlWYuqg==", + "engines": { + "node": ">=4" + } + }, + "node_modules/axobject-query": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.1.1.tgz", + "integrity": "sha512-goKlv8DZrK9hUh975fnHzhNIO4jUnFCfv/dszV5VwUGDFjI6vQ2VwoyjYjYNEbBE8AH87TduWP5uyDR1D+Iteg==", + "dependencies": { + "deep-equal": "^2.0.5" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/bufferutil": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.7.tgz", + "integrity": "sha512-kukuqc39WOHtdxtw4UScxF/WVnMFVSQVKhtx3AjZJzhd0RGZZldcrfSEbVsWWe6KNH253574cq5F+wpv0G9pJw==", + "hasInstallScript": true, + "dependencies": { + "node-gyp-build": "^4.3.0" + }, + "engines": { + "node": ">=6.14.2" + } + }, + "node_modules/call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dependencies": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001457", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001457.tgz", + "integrity": "sha512-SDIV6bgE1aVbK6XyxdURbUE89zY7+k1BBBaOwYwkNCglXlel/E7mELiHC64HQ+W0xSKlqWhV9Wh7iHxUjMs4fA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + } + ] + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==" + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "node_modules/concurrently": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-7.6.0.tgz", + "integrity": "sha512-BKtRgvcJGeZ4XttiDiNcFiRlxoAeZOseqUvyYRUp/Vtd+9p1ULmeoSqGsDA+2ivdeDFpqrJvGvmI+StKfKl5hw==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "date-fns": "^2.29.1", + "lodash": "^4.17.21", + "rxjs": "^7.0.0", + "shell-quote": "^1.7.3", + "spawn-command": "^0.0.2-1", + "supports-color": "^8.1.0", + "tree-kill": "^1.2.2", + "yargs": "^17.3.1" + }, + "bin": { + "conc": "dist/bin/concurrently.js", + "concurrently": "dist/bin/concurrently.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + } + }, + "node_modules/concurrently/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/cross-fetch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz", + "integrity": "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==", + "dependencies": { + "node-fetch": "2.6.7" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz", + "integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==" + }, + "node_modules/d": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz", + "integrity": "sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==", + "dependencies": { + "es5-ext": "^0.10.50", + "type": "^1.0.1" + } + }, + "node_modules/damerau-levenshtein": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", + "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==" + }, + "node_modules/date-fns": { + "version": "2.29.3", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.29.3.tgz", + "integrity": "sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA==", + "dev": true, + "engines": { + "node": ">=0.11" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/date-fns" + } + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-equal": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.0.tgz", + "integrity": "sha512-RdpzE0Hv4lhowpIUKKMJfeH6C1pXdtT1/it80ubgWqwI3qpuxUBpC1S4hnHg+zjnuOoDkzUtUCEEkG+XG5l3Mw==", + "dependencies": { + "call-bind": "^1.0.2", + "es-get-iterator": "^1.1.2", + "get-intrinsic": "^1.1.3", + "is-arguments": "^1.1.1", + "is-array-buffer": "^3.0.1", + "is-date-object": "^1.0.5", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "isarray": "^2.0.5", + "object-is": "^1.1.5", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.4.3", + "side-channel": "^1.0.4", + "which-boxed-primitive": "^1.0.2", + "which-collection": "^1.0.1", + "which-typed-array": "^1.1.9" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==" + }, + "node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "engines": { + "node": ">=8" + } + }, + "node_modules/define-properties": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz", + "integrity": "sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==", + "dependencies": { + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/defined": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.1.tgz", + "integrity": "sha512-hsBd2qSVCRE+5PmNdHt1uzyrFu5d3RwmFDKzyNZMFq/EwDNJF7Ee5+D5oEKF0hU6LhtoUF1macFvOe4AskQC1Q==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/detective": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/detective/-/detective-5.2.1.tgz", + "integrity": "sha512-v9XE1zRnz1wRtgurGu0Bs8uHKFSTdteYZNbIPFVhUZ39L/S79ppMpdmVOZAnoz1jfEFodc48n6MX483Xo3t1yw==", + "dev": true, + "dependencies": { + "acorn-node": "^1.8.2", + "defined": "^1.0.0", + "minimist": "^1.2.6" + }, + "bin": { + "detective": "bin/detective.js" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" + }, + "node_modules/enhanced-resolve": { + "version": "5.12.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.12.0.tgz", + "integrity": "sha512-QHTXI/sZQmko1cbDoNAa3mJ5qhWUUNAq3vR0/YiD379fWQrcfuoX1+HW2S0MTt7XmoPLapdaDKUtelUSPic7hQ==", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/es-abstract": { + "version": "1.21.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.21.1.tgz", + "integrity": "sha512-QudMsPOz86xYz/1dG1OuGBKOELjCh99IIWHLzy5znUB6j8xG2yMA7bfTV86VSqKF+Y/H08vQPR+9jyXpuC6hfg==", + "dependencies": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "es-set-tostringtag": "^2.0.1", + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "function.prototype.name": "^1.1.5", + "get-intrinsic": "^1.1.3", + "get-symbol-description": "^1.0.0", + "globalthis": "^1.0.3", + "gopd": "^1.0.1", + "has": "^1.0.3", + "has-property-descriptors": "^1.0.0", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.4", + "is-array-buffer": "^3.0.1", + "is-callable": "^1.2.7", + "is-negative-zero": "^2.0.2", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "is-string": "^1.0.7", + "is-typed-array": "^1.1.10", + "is-weakref": "^1.0.2", + "object-inspect": "^1.12.2", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.4.3", + "safe-regex-test": "^1.0.0", + "string.prototype.trimend": "^1.0.6", + "string.prototype.trimstart": "^1.0.6", + "typed-array-length": "^1.0.4", + "unbox-primitive": "^1.0.2", + "which-typed-array": "^1.1.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-get-iterator": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", + "integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==", + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3", + "has-symbols": "^1.0.3", + "is-arguments": "^1.1.1", + "is-map": "^2.0.2", + "is-set": "^2.0.2", + "is-string": "^1.0.7", + "isarray": "^2.0.5", + "stop-iteration-iterator": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz", + "integrity": "sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==", + "dependencies": { + "get-intrinsic": "^1.1.3", + "has": "^1.0.3", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz", + "integrity": "sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==", + "dependencies": { + "has": "^1.0.3" + } + }, + "node_modules/es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dependencies": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es5-ext": { + "version": "0.10.62", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.62.tgz", + "integrity": "sha512-BHLqn0klhEpnOKSrzn/Xsz2UIW8j+cGmo9JLzr8BiUapV8hPL9+FliFqjwr9ngW7jWdnxv6eO+/LqyhJVqgrjA==", + "hasInstallScript": true, + "dependencies": { + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.3", + "next-tick": "^1.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/es6-iterator": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", + "integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==", + "dependencies": { + "d": "1", + "es5-ext": "^0.10.35", + "es6-symbol": "^3.1.1" + } + }, + "node_modules/es6-symbol": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz", + "integrity": "sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==", + "dependencies": { + "d": "^1.0.1", + "ext": "^1.1.2" + } + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.34.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.34.0.tgz", + "integrity": "sha512-1Z8iFsucw+7kSqXNZVslXS8Ioa4u2KM7GPwuKtkTFAqZ/cHMcEaR+1+Br0wLlot49cNxIiZk5wp8EAbPcYZxTg==", + "dependencies": { + "@eslint/eslintrc": "^1.4.1", + "@humanwhocodes/config-array": "^0.11.8", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "ajv": "^6.10.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.1.1", + "eslint-utils": "^3.0.0", + "eslint-visitor-keys": "^3.3.0", + "espree": "^9.4.0", + "esquery": "^1.4.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "grapheme-splitter": "^1.0.4", + "ignore": "^5.2.0", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-sdsl": "^4.1.4", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.1", + "regexpp": "^3.2.0", + "strip-ansi": "^6.0.1", + "strip-json-comments": "^3.1.0", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-next": { + "version": "13.1.6", + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-13.1.6.tgz", + "integrity": "sha512-0cg7h5wztg/SoLAlxljZ0ZPUQ7i6QKqRiP4M2+MgTZtxWwNKb2JSwNc18nJ6/kXBI6xYvPraTbQSIhAuVw6czw==", + "dependencies": { + "@next/eslint-plugin-next": "13.1.6", + "@rushstack/eslint-patch": "^1.1.3", + "@typescript-eslint/parser": "^5.42.0", + "eslint-import-resolver-node": "^0.3.6", + "eslint-import-resolver-typescript": "^3.5.2", + "eslint-plugin-import": "^2.26.0", + "eslint-plugin-jsx-a11y": "^6.5.1", + "eslint-plugin-react": "^7.31.7", + "eslint-plugin-react-hooks": "^4.5.0" + }, + "peerDependencies": { + "eslint": "^7.23.0 || ^8.0.0", + "typescript": ">=3.3.1" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.7", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.7.tgz", + "integrity": "sha512-gozW2blMLJCeFpBwugLTGyvVjNoeo1knonXAcatC6bjPBZitotxdWf7Gimr25N4c0AAOo4eOUfaG82IJPDpqCA==", + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.11.0", + "resolve": "^1.22.1" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-import-resolver-typescript": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.5.3.tgz", + "integrity": "sha512-njRcKYBc3isE42LaTcJNVANR3R99H9bAxBDMNDr2W7yq5gYPxbU3MkdhsQukxZ/Xg9C2vcyLlDsbKfRDg0QvCQ==", + "dependencies": { + "debug": "^4.3.4", + "enhanced-resolve": "^5.10.0", + "get-tsconfig": "^4.2.0", + "globby": "^13.1.2", + "is-core-module": "^2.10.0", + "is-glob": "^4.0.3", + "synckit": "^0.8.4" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts/projects/eslint-import-resolver-ts" + }, + "peerDependencies": { + "eslint": "*", + "eslint-plugin-import": "*" + } + }, + "node_modules/eslint-import-resolver-typescript/node_modules/globby": { + "version": "13.1.3", + "resolved": "https://registry.npmjs.org/globby/-/globby-13.1.3.tgz", + "integrity": "sha512-8krCNHXvlCgHDpegPzleMq07yMYTO2sXKASmZmquEYWEmCx6J5UTRbp5RwMJkTJGtcQ44YpiUYUiN0b9mzy8Bw==", + "dependencies": { + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.11", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-import-resolver-typescript/node_modules/slash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", + "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-module-utils": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.7.4.tgz", + "integrity": "sha512-j4GT+rqzCoRKHwURX7pddtIPGySnX9Si/cgMI5ztrcqOPtk5dDEeZ34CQVPphnqkJytlc97Vuk05Um2mJ3gEQA==", + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.27.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.27.5.tgz", + "integrity": "sha512-LmEt3GVofgiGuiE+ORpnvP+kAm3h6MLZJ4Q5HCyHADofsb4VzXFsRiWj3c0OFiV+3DWFh0qg3v9gcPlfc3zRow==", + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "array.prototype.flatmap": "^1.3.1", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.7", + "eslint-module-utils": "^2.7.4", + "has": "^1.0.3", + "is-core-module": "^2.11.0", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.values": "^1.1.6", + "resolve": "^1.22.1", + "semver": "^6.3.0", + "tsconfig-paths": "^3.14.1" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-import/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-plugin-jsx-a11y": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.7.1.tgz", + "integrity": "sha512-63Bog4iIethyo8smBklORknVjB0T2dwB8Mr/hIC+fBS0uyHdYYpzM/Ed+YC8VxTjlXHEWFOdmgwcDn1U2L9VCA==", + "dependencies": { + "@babel/runtime": "^7.20.7", + "aria-query": "^5.1.3", + "array-includes": "^3.1.6", + "array.prototype.flatmap": "^1.3.1", + "ast-types-flow": "^0.0.7", + "axe-core": "^4.6.2", + "axobject-query": "^3.1.1", + "damerau-levenshtein": "^1.0.8", + "emoji-regex": "^9.2.2", + "has": "^1.0.3", + "jsx-ast-utils": "^3.3.3", + "language-tags": "=1.0.5", + "minimatch": "^3.1.2", + "object.entries": "^1.1.6", + "object.fromentries": "^2.0.6", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=4.0" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8" + } + }, + "node_modules/eslint-plugin-jsx-a11y/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-plugin-react": { + "version": "7.32.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.32.2.tgz", + "integrity": "sha512-t2fBMa+XzonrrNkyVirzKlvn5RXzzPwRHtMvLAtVZrt8oxgnTQaYbU6SXTOO1mwQgp1y5+toMSKInnzGr0Knqg==", + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flatmap": "^1.3.1", + "array.prototype.tosorted": "^1.1.1", + "doctrine": "^2.1.0", + "estraverse": "^5.3.0", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.6", + "object.fromentries": "^2.0.6", + "object.hasown": "^1.1.2", + "object.values": "^1.1.6", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.4", + "semver": "^6.3.0", + "string.prototype.matchall": "^4.0.8" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz", + "integrity": "sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/eslint-plugin-react/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-react/node_modules/resolve": { + "version": "2.0.0-next.4", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.4.tgz", + "integrity": "sha512-iMDbmAWtfU+MHpxt/I5iWI7cY6YVEZUQ3MBgPQ++XD1PELuJHIl82xBmObyP2KyQmkNB2dsqF7seoQQiAn5yDQ==", + "dependencies": { + "is-core-module": "^2.9.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/eslint-plugin-react/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-scope": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz", + "integrity": "sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/eslint-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", + "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", + "dependencies": { + "eslint-visitor-keys": "^2.0.0" + }, + "engines": { + "node": "^10.0.0 || ^12.0.0 || >= 14.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=5" + } + }, + "node_modules/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", + "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/espree": { + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.4.1.tgz", + "integrity": "sha512-XwctdmTO6SIvCzd9810yyNzIrOrqNYV9Koizx4C/mRhf9uq0o4yHoCEU/670pOxOL/MSraektvSAji79kX90Vg==", + "dependencies": { + "acorn": "^8.8.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.2.tgz", + "integrity": "sha512-JVSoLdTlTDkmjFmab7H/9SL9qGSyjElT3myyKp7krqjVFQCDLmj1QFaCLRFBszBKI0XVZaiiXvuPIX3ZwHe1Ng==", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ext": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz", + "integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==", + "dependencies": { + "type": "^2.7.2" + } + }, + "node_modules/ext/node_modules/type": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/type/-/type-2.7.2.tgz", + "integrity": "sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw==" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "node_modules/fast-glob": { + "version": "3.2.12", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", + "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==" + }, + "node_modules/fastq": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", + "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", + "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "dependencies": { + "flatted": "^3.1.0", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz", + "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==" + }, + "node_modules/for-each": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "dependencies": { + "is-callable": "^1.1.3" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "node_modules/function.prototype.name": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz", + "integrity": "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.0", + "functions-have-names": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz", + "integrity": "sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==", + "dependencies": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-symbol-description": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", + "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-tsconfig": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.4.0.tgz", + "integrity": "sha512-0Gdjo/9+FzsYhXCEFueo2aY1z1tpXrxWZzP7k8ul9qt1U5o8rYJwTJYmaeHdrVosYIVYkOy2iwCJ9FdpocJhPQ==", + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/glob": { + "version": "7.1.7", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", + "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "13.20.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz", + "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz", + "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==", + "dependencies": { + "define-properties": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/globalyzer": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/globalyzer/-/globalyzer-0.1.0.tgz", + "integrity": "sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==" + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globrex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", + "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==" + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==" + }, + "node_modules/grapheme-splitter": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", + "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==" + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", + "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", + "dependencies": { + "get-intrinsic": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", + "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ignore": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", + "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/internal-slot": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz", + "integrity": "sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==", + "dependencies": { + "get-intrinsic": "^1.2.0", + "has": "^1.0.3", + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-arguments": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", + "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.1.tgz", + "integrity": "sha512-ASfLknmY8Xa2XtB4wmbz13Wu202baeA18cJBCeCy0wXUHZF0IPyVEXqKEcd+t2fNSLLL1vC6k7lxZEojNbISXQ==", + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3", + "is-typed-array": "^1.1.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dependencies": { + "has-bigints": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz", + "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==", + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-map": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.2.tgz", + "integrity": "sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", + "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", + "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.2.tgz", + "integrity": "sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", + "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.10.tgz", + "integrity": "sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A==", + "dependencies": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==" + }, + "node_modules/is-weakmap": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.1.tgz", + "integrity": "sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.2.tgz", + "integrity": "sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg==", + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + }, + "node_modules/js-sdsl": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.3.0.tgz", + "integrity": "sha512-mifzlm2+5nZ+lEcLJMoBK0/IH/bDg8XnJfd/Wq6IP+xoCjLZsTOnV2QpxlVbX9bMnkl5PdEjNtBJ9Cj1NjifhQ==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==" + }, + "node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/jsx-ast-utils": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.3.tgz", + "integrity": "sha512-fYQHZTZ8jSfmWZ0iyzfwiU4WDX4HpHbMCZ3gPlWYiCl3BoeOTsqKBqnTVfH2rYT7eP5c3sVbeSPHnnJOaTrWiw==", + "dependencies": { + "array-includes": "^3.1.5", + "object.assign": "^4.1.3" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/language-subtag-registry": { + "version": "0.3.22", + "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.22.tgz", + "integrity": "sha512-tN0MCzyWnoz/4nHS6uxdlFWoUZT7ABptwKPQ52Ea7URk6vll88bWBVhodtnlfEuCcKWNGoc+uGbw1cwa9IKh/w==" + }, + "node_modules/language-tags": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.5.tgz", + "integrity": "sha512-qJhlO9cGXi6hBGKoxEG/sKZDAHD5Hnu9Hs4WbOY3pCWXDhw0N8x1NenNzm2EnNLkLkk7J2SdxAkDSbb6ftT+UQ==", + "dependencies": { + "language-subtag-registry": "~0.3.2" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lilconfig": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.6.tgz", + "integrity": "sha512-9JROoBW7pobfsx+Sq2JsASvCo6Pfo6WWoUW79HuB1BCoBXD4PLWJPqDF6fNj67pqBYTbAHkE57M1kS/+L1neOg==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/nanoid": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", + "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==" + }, + "node_modules/next": { + "version": "13.1.6", + "resolved": "https://registry.npmjs.org/next/-/next-13.1.6.tgz", + "integrity": "sha512-hHlbhKPj9pW+Cymvfzc15lvhaOZ54l+8sXDXJWm3OBNBzgrVj6hwGPmqqsXg40xO1Leq+kXpllzRPuncpC0Phw==", + "dependencies": { + "@next/env": "13.1.6", + "@swc/helpers": "0.4.14", + "caniuse-lite": "^1.0.30001406", + "postcss": "8.4.14", + "styled-jsx": "5.1.1" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": ">=14.6.0" + }, + "optionalDependencies": { + "@next/swc-android-arm-eabi": "13.1.6", + "@next/swc-android-arm64": "13.1.6", + "@next/swc-darwin-arm64": "13.1.6", + "@next/swc-darwin-x64": "13.1.6", + "@next/swc-freebsd-x64": "13.1.6", + "@next/swc-linux-arm-gnueabihf": "13.1.6", + "@next/swc-linux-arm64-gnu": "13.1.6", + "@next/swc-linux-arm64-musl": "13.1.6", + "@next/swc-linux-x64-gnu": "13.1.6", + "@next/swc-linux-x64-musl": "13.1.6", + "@next/swc-win32-arm64-msvc": "13.1.6", + "@next/swc-win32-ia32-msvc": "13.1.6", + "@next/swc-win32-x64-msvc": "13.1.6" + }, + "peerDependencies": { + "fibers": ">= 3.1.0", + "node-sass": "^6.0.0 || ^7.0.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "fibers": { + "optional": true + }, + "node-sass": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/next-tick": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", + "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==" + }, + "node_modules/node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-gyp-build": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.6.0.tgz", + "integrity": "sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ==", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/object-inspect": { + "version": "1.12.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", + "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-is": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz", + "integrity": "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", + "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.6.tgz", + "integrity": "sha512-leTPzo4Zvg3pmbQ3rDK69Rl8GQvIqMWubrkxONG9/ojtFE2rD9fjMKfSI5BxW3osRH1m6VdzmqK8oAY9aT4x5w==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.6.tgz", + "integrity": "sha512-VciD13dswC4j1Xt5394WR4MzmAQmlgN72phd/riNp9vtD7tp4QQWJ0R4wvclXcafgcYK8veHRed2W6XeGBvcfg==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.hasown": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.2.tgz", + "integrity": "sha512-B5UIT3J1W+WuWIU55h0mjlwaqxiE5vYENJXIXZ4VFe05pNYrkKuK0U/6aFcb0pKywYJh7IhfoqUfKVmrJJHZHw==", + "dependencies": { + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.values": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.6.tgz", + "integrity": "sha512-FVVTkD1vENCsAcwNs9k6jea2uHC/X0+JcjG8YA60FN5CMaJmG95wT9jek/xX9nornqGRrBkKtzuAu2wuHpKqvw==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", + "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.3" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postcss": { + "version": "8.4.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.14.tgz", + "integrity": "sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + } + ], + "dependencies": { + "nanoid": "^3.3.4", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-14.1.0.tgz", + "integrity": "sha512-flwI+Vgm4SElObFVPpTIT7SU7R3qk2L7PyduMcokiaVKuWv9d/U+Gm/QAd8NDLuykTWTkcrjOeD2Pp1rMeBTGw==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-load-config": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.4.tgz", + "integrity": "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==", + "dev": true, + "dependencies": { + "lilconfig": "^2.0.5", + "yaml": "^1.10.2" + }, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.0.tgz", + "integrity": "sha512-0DkamqrPcmkBDsLn+vQDIrtkSbNkv5AD/M322ySo9kqFkCIYklym2xEmWkwo+Y3/qZo34tzEPNUw4y7yMCdv5w==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.10" + }, + "engines": { + "node": ">=12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.0.11", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.11.tgz", + "integrity": "sha512-zbARubNdogI9j7WY4nQJBiNqQf3sLS3wCP4WfOidu+p28LofJqDH1tcXypGrcmMHhDk2t9wGhCsYe/+szLTy1g==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/punycode": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", + "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/react": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", + "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", + "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.0" + }, + "peerDependencies": { + "react": "^18.2.0" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" + }, + "node_modules/regexp.prototype.flags": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz", + "integrity": "sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "functions-have-names": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexpp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", + "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", + "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", + "dependencies": { + "is-core-module": "^2.9.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/rxjs": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.0.tgz", + "integrity": "sha512-F2+gxDshqmIub1KdvZkaEfGDwLNpPvk9Fs6LD/MyQxNgMds/WH9OdDDXOmxUZpME+iSK3rQCctkL0DYyytUqMg==", + "dev": true, + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-regex-test": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz", + "integrity": "sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==", + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3", + "is-regex": "^1.1.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/scheduler": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", + "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "engines": { + "node": ">=8" + } + }, + "node_modules/shell-quote": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.0.tgz", + "integrity": "sha512-QHsz8GgQIGKlRi24yFc6a6lN69Idnx634w49ay6+jA5yFh7a1UY+4Rp6HPx/L/1zcEDPEij8cIsiqR6bQsE5VQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/spawn-command": { + "version": "0.0.2-1", + "resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2-1.tgz", + "integrity": "sha512-n98l9E2RMSJ9ON1AKisHzz7V42VDiBQGY6PB1BwRglz99wpVsSuGzQ+jOi6lFXBGVTCrRpltvjm+/XA+tpeJrg==", + "dev": true + }, + "node_modules/stop-iteration-iterator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz", + "integrity": "sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==", + "dependencies": { + "internal-slot": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/string.prototype.matchall": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.8.tgz", + "integrity": "sha512-6zOCOcJ+RJAQshcTvXPHoxoQGONa3e/Lqx90wUA+wEzX78sg5Bo+1tQo4N0pohS0erG9qtCqJDjNCQBjeWVxyg==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4", + "get-intrinsic": "^1.1.3", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.3", + "regexp.prototype.flags": "^1.4.3", + "side-channel": "^1.0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz", + "integrity": "sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.6.tgz", + "integrity": "sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/styled-jsx": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz", + "integrity": "sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/synckit": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.5.tgz", + "integrity": "sha512-L1dapNV6vu2s/4Sputv8xGsCdAVlb5nRDMFU/E27D44l5U6cw1g0dGd45uLc+OXjNMmF4ntiMdCimzcjFKQI8Q==", + "dependencies": { + "@pkgr/utils": "^2.3.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, + "node_modules/tailwindcss": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.2.7.tgz", + "integrity": "sha512-B6DLqJzc21x7wntlH/GsZwEXTBttVSl1FtCzC8WP4oBc/NKef7kaax5jeihkkCEWc831/5NDJ9gRNDK6NEioQQ==", + "dev": true, + "dependencies": { + "arg": "^5.0.2", + "chokidar": "^3.5.3", + "color-name": "^1.1.4", + "detective": "^5.2.1", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.2.12", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "lilconfig": "^2.0.6", + "micromatch": "^4.0.5", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.0.0", + "postcss": "^8.0.9", + "postcss-import": "^14.1.0", + "postcss-js": "^4.0.0", + "postcss-load-config": "^3.1.4", + "postcss-nested": "6.0.0", + "postcss-selector-parser": "^6.0.11", + "postcss-value-parser": "^4.2.0", + "quick-lru": "^5.1.1", + "resolve": "^1.22.1" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=12.13.0" + }, + "peerDependencies": { + "postcss": "^8.0.9" + } + }, + "node_modules/tailwindcss/node_modules/postcss": { + "version": "8.4.21", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.21.tgz", + "integrity": "sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + } + ], + "dependencies": { + "nanoid": "^3.3.4", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/tailwindcss/node_modules/postcss-js": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", + "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", + "dev": true, + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==" + }, + "node_modules/tiny-glob": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/tiny-glob/-/tiny-glob-0.2.9.tgz", + "integrity": "sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg==", + "dependencies": { + "globalyzer": "0.1.0", + "globrex": "^0.1.2" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/tsconfig-paths": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz", + "integrity": "sha512-fxDhWnFSLt3VuTwtvJt5fpwxBHg5AdKWMsgcPOOIilyjymcYVZoCQF8fvFRezCNfblEXmi+PcM1eYHeOAgXCOQ==", + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.1", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/tslib": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", + "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" + }, + "node_modules/tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dependencies": { + "tslib": "^1.8.1" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" + } + }, + "node_modules/tsutils/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, + "node_modules/type": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz", + "integrity": "sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz", + "integrity": "sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==", + "dependencies": { + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "is-typed-array": "^1.1.9" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "dependencies": { + "is-typedarray": "^1.0.0" + } + }, + "node_modules/typescript": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/unbox-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", + "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "dependencies": { + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", + "which-boxed-primitive": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/utf-8-validate": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz", + "integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==", + "hasInstallScript": true, + "dependencies": { + "node-gyp-build": "^4.3.0" + }, + "engines": { + "node": ">=6.14.2" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/websocket": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/websocket/-/websocket-1.0.34.tgz", + "integrity": "sha512-PRDso2sGwF6kM75QykIesBijKSVceR6jL2G8NGYyq2XrItNC2P5/qL5XeR056GhA+Ly7JMFvJb9I312mJfmqnQ==", + "dependencies": { + "bufferutil": "^4.0.1", + "debug": "^2.2.0", + "es5-ext": "^0.10.50", + "typedarray-to-buffer": "^3.1.5", + "utf-8-validate": "^5.0.2", + "yaeti": "^0.0.6" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/websocket/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/websocket/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dependencies": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.1.tgz", + "integrity": "sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A==", + "dependencies": { + "is-map": "^2.0.1", + "is-set": "^2.0.1", + "is-weakmap": "^2.0.1", + "is-weakset": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.9.tgz", + "integrity": "sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA==", + "dependencies": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.0", + "is-typed-array": "^1.1.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true, + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yaeti": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/yaeti/-/yaeti-0.0.6.tgz", + "integrity": "sha512-MvQa//+KcZCUkBTIC9blM+CU9J2GzuTytsOUwf2lidtvkx/6gnEp1QvJv34t9vdjhFmha/mUiNDbN0D0mJWdug==", + "engines": { + "node": ">=0.10.32" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/yargs": { + "version": "17.7.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.1.tgz", + "integrity": "sha512-cwiTb08Xuv5fqF4AovYacTFNxk62th7LKJ6BL9IGUpTJrWoU7/7WdQGTP2SjKf1dUNBGzDd28p/Yfs/GI6JrLw==", + "dev": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/examples/todo-list/nextjs-todo-list/package.json b/examples/todo-list/nextjs-todo-list/package.json new file mode 100644 index 0000000000..32fac00379 --- /dev/null +++ b/examples/todo-list/nextjs-todo-list/package.json @@ -0,0 +1,32 @@ +{ + "name": "nextjs-todo-list", + "version": "2.0.0", + "private": true, + "scripts": { + "dev": "concurrently \"npm run dev:css\" \"next dev\"", + "dev:css": "tailwindcss -w -i ./styles/tailwind.css -o styles/app.css", + "build": "next build", + "build:css": "tailwindcss -m -i ./styles/tailwind.css -o styles/app.css", + "start": "next start", + "lint": "next lint" + }, + "dependencies": { + "@next/font": "13.1.6", + "@supabase/auth-helpers-react": "^0.3.1", + "@supabase/auth-ui-react": "^0.2.8", + "@supabase/supabase-js": "^2.8.0", + "@types/node": "18.14.0", + "@types/react": "18.0.28", + "@types/react-dom": "18.0.11", + "eslint": "8.34.0", + "eslint-config-next": "13.1.6", + "next": "13.1.6", + "react": "18.2.0", + "react-dom": "18.2.0", + "typescript": "4.9.5" + }, + "devDependencies": { + "concurrently": "^7.6.0", + "tailwindcss": "^3.2.7" + } +} diff --git a/examples/todo-list/nextjs-todo-list/pages/_app.tsx b/examples/todo-list/nextjs-todo-list/pages/_app.tsx new file mode 100644 index 0000000000..59d31d438b --- /dev/null +++ b/examples/todo-list/nextjs-todo-list/pages/_app.tsx @@ -0,0 +1,12 @@ +import { supabase } from '@/lib/initSupabase' +import '@/styles/app.css' +import { SessionContextProvider } from '@supabase/auth-helpers-react' +import type { AppProps } from 'next/app' + +export default function App({ Component, pageProps }: AppProps) { + return ( + + + + ) +} diff --git a/examples/todo-list/nextjs-todo-list/pages/_document.tsx b/examples/todo-list/nextjs-todo-list/pages/_document.tsx new file mode 100644 index 0000000000..54e8bf3e2a --- /dev/null +++ b/examples/todo-list/nextjs-todo-list/pages/_document.tsx @@ -0,0 +1,13 @@ +import { Html, Head, Main, NextScript } from 'next/document' + +export default function Document() { + return ( + + + +
    + + + + ) +} diff --git a/examples/todo-list/nextjs-todo-list/pages/api/hello.ts b/examples/todo-list/nextjs-todo-list/pages/api/hello.ts new file mode 100644 index 0000000000..f8bcc7e5ca --- /dev/null +++ b/examples/todo-list/nextjs-todo-list/pages/api/hello.ts @@ -0,0 +1,13 @@ +// Next.js API route support: https://nextjs.org/docs/api-routes/introduction +import type { NextApiRequest, NextApiResponse } from 'next' + +type Data = { + name: string +} + +export default function handler( + req: NextApiRequest, + res: NextApiResponse +) { + res.status(200).json({ name: 'John Doe' }) +} diff --git a/examples/todo-list/nextjs-todo-list/pages/index.tsx b/examples/todo-list/nextjs-todo-list/pages/index.tsx new file mode 100644 index 0000000000..d498b19419 --- /dev/null +++ b/examples/todo-list/nextjs-todo-list/pages/index.tsx @@ -0,0 +1,50 @@ +import Head from 'next/head' +import { useSession, useSupabaseClient } from '@supabase/auth-helpers-react' +import { Auth, ThemeSupa } from '@supabase/auth-ui-react' +import TodoList from '@/components/TodoList' + +export default function Home() { + const session = useSession() + const supabase = useSupabaseClient() + + return ( + <> + + Create Next App + + + + +
    + {!session ? ( +
    +
    +
    + + Login + + +
    +
    +
    + ) : ( +
    + + +
    + )} +
    + + ) +} diff --git a/examples/todo-list/nextjs-todo-list/public/favicon.ico b/examples/todo-list/nextjs-todo-list/public/favicon.ico new file mode 100644 index 0000000000..718d6fea48 Binary files /dev/null and b/examples/todo-list/nextjs-todo-list/public/favicon.ico differ diff --git a/examples/todo-list/nextjs-todo-list/public/next.svg b/examples/todo-list/nextjs-todo-list/public/next.svg new file mode 100644 index 0000000000..5174b28c56 --- /dev/null +++ b/examples/todo-list/nextjs-todo-list/public/next.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/todo-list/nextjs-todo-list/public/thirteen.svg b/examples/todo-list/nextjs-todo-list/public/thirteen.svg new file mode 100644 index 0000000000..8977c1bd12 --- /dev/null +++ b/examples/todo-list/nextjs-todo-list/public/thirteen.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/todo-list/nextjs-todo-list/public/vercel.svg b/examples/todo-list/nextjs-todo-list/public/vercel.svg new file mode 100644 index 0000000000..d2f8422273 --- /dev/null +++ b/examples/todo-list/nextjs-todo-list/public/vercel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/todo-list/nextjs-todo-list/styles/app.css b/examples/todo-list/nextjs-todo-list/styles/app.css new file mode 100644 index 0000000000..0530e31cac --- /dev/null +++ b/examples/todo-list/nextjs-todo-list/styles/app.css @@ -0,0 +1,844 @@ +/* +! tailwindcss v3.2.7 | MIT License | https://tailwindcss.com +*/ + +/* +1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4) +2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116) +*/ + +*, +::before, +::after { + box-sizing: border-box; + /* 1 */ + border-width: 0; + /* 2 */ + border-style: solid; + /* 2 */ + border-color: #e5e7eb; + /* 2 */ +} + +::before, +::after { + --tw-content: ''; +} + +/* +1. Use a consistent sensible line-height in all browsers. +2. Prevent adjustments of font size after orientation changes in iOS. +3. Use a more readable tab size. +4. Use the user's configured `sans` font-family by default. +5. Use the user's configured `sans` font-feature-settings by default. +*/ + +html { + line-height: 1.5; + /* 1 */ + -webkit-text-size-adjust: 100%; + /* 2 */ + -moz-tab-size: 4; + /* 3 */ + -o-tab-size: 4; + tab-size: 4; + /* 3 */ + font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + /* 4 */ + font-feature-settings: normal; + /* 5 */ +} + +/* +1. Remove the margin in all browsers. +2. Inherit line-height from `html` so users can set them as a class directly on the `html` element. +*/ + +body { + margin: 0; + /* 1 */ + line-height: inherit; + /* 2 */ +} + +/* +1. Add the correct height in Firefox. +2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655) +3. Ensure horizontal rules are visible by default. +*/ + +hr { + height: 0; + /* 1 */ + color: inherit; + /* 2 */ + border-top-width: 1px; + /* 3 */ +} + +/* +Add the correct text decoration in Chrome, Edge, and Safari. +*/ + +abbr:where([title]) { + -webkit-text-decoration: underline dotted; + text-decoration: underline dotted; +} + +/* +Remove the default font size and weight for headings. +*/ + +h1, +h2, +h3, +h4, +h5, +h6 { + font-size: inherit; + font-weight: inherit; +} + +/* +Reset links to optimize for opt-in styling instead of opt-out. +*/ + +a { + color: inherit; + text-decoration: inherit; +} + +/* +Add the correct font weight in Edge and Safari. +*/ + +b, +strong { + font-weight: bolder; +} + +/* +1. Use the user's configured `mono` font family by default. +2. Correct the odd `em` font sizing in all browsers. +*/ + +code, +kbd, +samp, +pre { + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + /* 1 */ + font-size: 1em; + /* 2 */ +} + +/* +Add the correct font size in all browsers. +*/ + +small { + font-size: 80%; +} + +/* +Prevent `sub` and `sup` elements from affecting the line height in all browsers. +*/ + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sub { + bottom: -0.25em; +} + +sup { + top: -0.5em; +} + +/* +1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297) +2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016) +3. Remove gaps between table borders by default. +*/ + +table { + text-indent: 0; + /* 1 */ + border-color: inherit; + /* 2 */ + border-collapse: collapse; + /* 3 */ +} + +/* +1. Change the font styles in all browsers. +2. Remove the margin in Firefox and Safari. +3. Remove default padding in all browsers. +*/ + +button, +input, +optgroup, +select, +textarea { + font-family: inherit; + /* 1 */ + font-size: 100%; + /* 1 */ + font-weight: inherit; + /* 1 */ + line-height: inherit; + /* 1 */ + color: inherit; + /* 1 */ + margin: 0; + /* 2 */ + padding: 0; + /* 3 */ +} + +/* +Remove the inheritance of text transform in Edge and Firefox. +*/ + +button, +select { + text-transform: none; +} + +/* +1. Correct the inability to style clickable types in iOS and Safari. +2. Remove default button styles. +*/ + +button, +[type='button'], +[type='reset'], +[type='submit'] { + -webkit-appearance: button; + /* 1 */ + background-color: transparent; + /* 2 */ + background-image: none; + /* 2 */ +} + +/* +Use the modern Firefox focus style for all focusable elements. +*/ + +:-moz-focusring { + outline: auto; +} + +/* +Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737) +*/ + +:-moz-ui-invalid { + box-shadow: none; +} + +/* +Add the correct vertical alignment in Chrome and Firefox. +*/ + +progress { + vertical-align: baseline; +} + +/* +Correct the cursor style of increment and decrement buttons in Safari. +*/ + +::-webkit-inner-spin-button, +::-webkit-outer-spin-button { + height: auto; +} + +/* +1. Correct the odd appearance in Chrome and Safari. +2. Correct the outline style in Safari. +*/ + +[type='search'] { + -webkit-appearance: textfield; + /* 1 */ + outline-offset: -2px; + /* 2 */ +} + +/* +Remove the inner padding in Chrome and Safari on macOS. +*/ + +::-webkit-search-decoration { + -webkit-appearance: none; +} + +/* +1. Correct the inability to style clickable types in iOS and Safari. +2. Change font properties to `inherit` in Safari. +*/ + +::-webkit-file-upload-button { + -webkit-appearance: button; + /* 1 */ + font: inherit; + /* 2 */ +} + +/* +Add the correct display in Chrome and Safari. +*/ + +summary { + display: list-item; +} + +/* +Removes the default spacing and border for appropriate elements. +*/ + +blockquote, +dl, +dd, +h1, +h2, +h3, +h4, +h5, +h6, +hr, +figure, +p, +pre { + margin: 0; +} + +fieldset { + margin: 0; + padding: 0; +} + +legend { + padding: 0; +} + +ol, +ul, +menu { + list-style: none; + margin: 0; + padding: 0; +} + +/* +Prevent resizing textareas horizontally by default. +*/ + +textarea { + resize: vertical; +} + +/* +1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300) +2. Set the default placeholder color to the user's configured gray 400 color. +*/ + +input::-moz-placeholder, textarea::-moz-placeholder { + opacity: 1; + /* 1 */ + color: #9ca3af; + /* 2 */ +} + +input::placeholder, +textarea::placeholder { + opacity: 1; + /* 1 */ + color: #9ca3af; + /* 2 */ +} + +/* +Set the default cursor for buttons. +*/ + +button, +[role="button"] { + cursor: pointer; +} + +/* +Make sure disabled buttons don't get the pointer cursor. +*/ + +:disabled { + cursor: default; +} + +/* +1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14) +2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210) + This can trigger a poorly considered lint error in some tools but is included by design. +*/ + +img, +svg, +video, +canvas, +audio, +iframe, +embed, +object { + display: block; + /* 1 */ + vertical-align: middle; + /* 2 */ +} + +/* +Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14) +*/ + +img, +video { + max-width: 100%; + height: auto; +} + +/* Make elements with the HTML hidden attribute stay hidden by default */ + +[hidden] { + display: none; +} + +*, ::before, ::after { + --tw-border-spacing-x: 0; + --tw-border-spacing-y: 0; + --tw-translate-x: 0; + --tw-translate-y: 0; + --tw-rotate: 0; + --tw-skew-x: 0; + --tw-skew-y: 0; + --tw-scale-x: 1; + --tw-scale-y: 1; + --tw-pan-x: ; + --tw-pan-y: ; + --tw-pinch-zoom: ; + --tw-scroll-snap-strictness: proximity; + --tw-ordinal: ; + --tw-slashed-zero: ; + --tw-numeric-figure: ; + --tw-numeric-spacing: ; + --tw-numeric-fraction: ; + --tw-ring-inset: ; + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-color: rgb(59 130 246 / 0.5); + --tw-ring-offset-shadow: 0 0 #0000; + --tw-ring-shadow: 0 0 #0000; + --tw-shadow: 0 0 #0000; + --tw-shadow-colored: 0 0 #0000; + --tw-blur: ; + --tw-brightness: ; + --tw-contrast: ; + --tw-grayscale: ; + --tw-hue-rotate: ; + --tw-invert: ; + --tw-saturate: ; + --tw-sepia: ; + --tw-drop-shadow: ; + --tw-backdrop-blur: ; + --tw-backdrop-brightness: ; + --tw-backdrop-contrast: ; + --tw-backdrop-grayscale: ; + --tw-backdrop-hue-rotate: ; + --tw-backdrop-invert: ; + --tw-backdrop-opacity: ; + --tw-backdrop-saturate: ; + --tw-backdrop-sepia: ; +} + +::backdrop { + --tw-border-spacing-x: 0; + --tw-border-spacing-y: 0; + --tw-translate-x: 0; + --tw-translate-y: 0; + --tw-rotate: 0; + --tw-skew-x: 0; + --tw-skew-y: 0; + --tw-scale-x: 1; + --tw-scale-y: 1; + --tw-pan-x: ; + --tw-pan-y: ; + --tw-pinch-zoom: ; + --tw-scroll-snap-strictness: proximity; + --tw-ordinal: ; + --tw-slashed-zero: ; + --tw-numeric-figure: ; + --tw-numeric-spacing: ; + --tw-numeric-fraction: ; + --tw-ring-inset: ; + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-color: rgb(59 130 246 / 0.5); + --tw-ring-offset-shadow: 0 0 #0000; + --tw-ring-shadow: 0 0 #0000; + --tw-shadow: 0 0 #0000; + --tw-shadow-colored: 0 0 #0000; + --tw-blur: ; + --tw-brightness: ; + --tw-contrast: ; + --tw-grayscale: ; + --tw-hue-rotate: ; + --tw-invert: ; + --tw-saturate: ; + --tw-sepia: ; + --tw-drop-shadow: ; + --tw-backdrop-blur: ; + --tw-backdrop-brightness: ; + --tw-backdrop-contrast: ; + --tw-backdrop-grayscale: ; + --tw-backdrop-hue-rotate: ; + --tw-backdrop-invert: ; + --tw-backdrop-opacity: ; + --tw-backdrop-saturate: ; + --tw-backdrop-sepia: ; +} + +.mx-4 { + margin-left: 1rem; + margin-right: 1rem; +} + +.my-2 { + margin-top: 0.5rem; + margin-bottom: 0.5rem; +} + +.my-3 { + margin-top: 0.75rem; + margin-bottom: 0.75rem; +} + +.mb-1 { + margin-bottom: 0.25rem; +} + +.mb-12 { + margin-bottom: 3rem; +} + +.ml-2 { + margin-left: 0.5rem; +} + +.mt-12 { + margin-top: 3rem; +} + +.block { + display: block; +} + +.flex { + display: flex; +} + +.h-4 { + height: 1rem; +} + +.h-full { + height: 100%; +} + +.min-h-screen { + min-height: 100vh; +} + +.w-4 { + width: 1rem; +} + +.w-full { + width: 100%; +} + +.min-w-0 { + min-width: 0px; +} + +.min-w-full { + min-width: 100%; +} + +.max-w-sm { + max-width: 24rem; +} + +.flex-1 { + flex: 1 1 0%; +} + +.cursor-pointer { + cursor: pointer; +} + +.flex-col { + flex-direction: column; +} + +.items-center { + align-items: center; +} + +.justify-center { + justify-content: center; +} + +.gap-2 { + gap: 0.5rem; +} + +.overflow-hidden { + overflow: hidden; +} + +.truncate { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.rounded { + border-radius: 0.25rem; +} + +.rounded-md { + border-radius: 0.375rem; +} + +.border-2 { + border-width: 2px; +} + +.border-b { + border-bottom-width: 1px; +} + +.bg-gray-200 { + --tw-bg-opacity: 1; + background-color: rgb(229 231 235 / var(--tw-bg-opacity)); +} + +.bg-red-100 { + --tw-bg-opacity: 1; + background-color: rgb(254 226 226 / var(--tw-bg-opacity)); +} + +.bg-white { + --tw-bg-opacity: 1; + background-color: rgb(255 255 255 / var(--tw-bg-opacity)); +} + +.p-2 { + padding: 0.5rem; +} + +.p-4 { + padding: 1rem; +} + +.p-5 { + padding: 1.25rem; +} + +.px-4 { + padding-left: 1rem; + padding-right: 1rem; +} + +.py-4 { + padding-top: 1rem; + padding-bottom: 1rem; +} + +.pb-2 { + padding-bottom: 0.5rem; +} + +.text-center { + text-align: center; +} + +.font-sans { + font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; +} + +.text-4xl { + font-size: 2.25rem; + line-height: 2.5rem; +} + +.text-base { + font-size: 1rem; + line-height: 1.5rem; +} + +.text-sm { + font-size: 0.875rem; + line-height: 1.25rem; +} + +.font-medium { + font-weight: 500; +} + +.leading-5 { + line-height: 1.25rem; +} + +.text-red-700 { + --tw-text-opacity: 1; + color: rgb(185 28 28 / var(--tw-text-opacity)); +} + +.shadow { + --tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); + --tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} + +.filter { + filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow); +} + +.transition { + transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-backdrop-filter; + transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter; + transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter, -webkit-backdrop-filter; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 150ms; +} + +.duration-150 { + transition-duration: 150ms; +} + +.ease-in-out { + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); +} + +/* Write your own custom component styles here */ + +.btn-black { + border-radius: 0.25rem; + border-width: 1px; + --tw-border-opacity: 1; + border-color: rgb(0 0 0 / var(--tw-border-opacity)); + --tw-bg-opacity: 1; + background-color: rgb(0 0 0 / var(--tw-bg-opacity)); + padding-top: 0.5rem; + padding-bottom: 0.5rem; + padding-left: 1rem; + padding-right: 1rem; + text-align: center; + font-weight: 700; + --tw-text-opacity: 1; + color: rgb(255 255 255 / var(--tw-text-opacity)); +} + +.btn-black:hover { + --tw-bg-opacity: 1; + background-color: rgb(255 255 255 / var(--tw-bg-opacity)); + --tw-text-opacity: 1; + color: rgb(0 0 0 / var(--tw-text-opacity)); + transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-backdrop-filter; + transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter; + transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter, -webkit-backdrop-filter; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 150ms; +} + +.btn-black-outline { + border-radius: 0.25rem; + border-width: 1px; + --tw-border-opacity: 1; + border-color: rgb(0 0 0 / var(--tw-border-opacity)); + padding-top: 0.5rem; + padding-bottom: 0.5rem; + padding-left: 1rem; + padding-right: 1rem; + text-align: center; + font-weight: 700; + --tw-text-opacity: 1; + color: rgb(0 0 0 / var(--tw-text-opacity)); +} + +.btn-black-outline:hover { + --tw-bg-opacity: 1; + background-color: rgb(0 0 0 / var(--tw-bg-opacity)); + --tw-text-opacity: 1; + color: rgb(255 255 255 / var(--tw-text-opacity)); + transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-backdrop-filter; + transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter; + transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter, -webkit-backdrop-filter; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 150ms; +} + +html, +body, +#__next { + height: 100vh; + min-height: 100vh; +} + +h1 { + font-size: 4rem; + font-weight: bold; + display: block; +} + +.hover\:border-black:hover { + --tw-border-opacity: 1; + border-color: rgb(0 0 0 / var(--tw-border-opacity)); +} + +.hover\:bg-gray-200:hover { + --tw-bg-opacity: 1; + background-color: rgb(229 231 235 / var(--tw-bg-opacity)); +} + +.focus\:bg-gray-200:focus { + --tw-bg-opacity: 1; + background-color: rgb(229 231 235 / var(--tw-bg-opacity)); +} + +.focus\:outline-none:focus { + outline: 2px solid transparent; + outline-offset: 2px; +} + +@media (min-width: 640px) { + .sm\:h-auto { + height: auto; + } + + .sm\:w-2\/5 { + width: 40%; + } + + .sm\:px-6 { + padding-left: 1.5rem; + padding-right: 1.5rem; + } +} diff --git a/examples/todo-list/nextjs-todo-list/styles/tailwind.css b/examples/todo-list/nextjs-todo-list/styles/tailwind.css new file mode 100644 index 0000000000..a183f14adb --- /dev/null +++ b/examples/todo-list/nextjs-todo-list/styles/tailwind.css @@ -0,0 +1,30 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +/* Write your own custom component styles here */ +.btn-black { + @apply border border-black bg-black text-white font-bold py-2 px-4 rounded text-center; +} +.btn-black:hover { + @apply transition duration-150 bg-white text-black; +} +.btn-black-outline { + @apply border border-black text-black py-2 px-4 rounded font-bold text-center; +} +.btn-black-outline:hover { + @apply transition duration-150 bg-black text-white; +} + +html, +body, +#__next { + height: 100vh; + min-height: 100vh; +} + +h1 { + font-size: 4rem; + font-weight: bold; + display: block; +} diff --git a/examples/todo-list/nextjs-todo-list/tailwind.config.js b/examples/todo-list/nextjs-todo-list/tailwind.config.js new file mode 100644 index 0000000000..81f91ba2f1 --- /dev/null +++ b/examples/todo-list/nextjs-todo-list/tailwind.config.js @@ -0,0 +1,13 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: ['./components/**/*.{js,ts,jsx,tsx}', './pages/**/*.{js,ts,jsx,tsx}'], + theme: { + extend: { + colors: { + 'accent-1': '#333', + }, + }, + }, + variants: {}, + plugins: [], +} diff --git a/examples/todo-list/nextjs-todo-list/tsconfig.json b/examples/todo-list/nextjs-todo-list/tsconfig.json new file mode 100644 index 0000000000..f4ab65fd2e --- /dev/null +++ b/examples/todo-list/nextjs-todo-list/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "baseUrl": ".", + "paths": { + "@/*": ["./*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], + "exclude": ["node_modules"] +} diff --git a/examples/todo-list/sveltejs-todo-list/src/assets/app.css b/examples/todo-list/sveltejs-todo-list/src/assets/app.css index e20cf35c6f..2bf19da9df 100644 --- a/examples/todo-list/sveltejs-todo-list/src/assets/app.css +++ b/examples/todo-list/sveltejs-todo-list/src/assets/app.css @@ -457,53 +457,6 @@ video { --tw-backdrop-sepia: ; } -::-webkit-backdrop { - --tw-border-spacing-x: 0; - --tw-border-spacing-y: 0; - --tw-translate-x: 0; - --tw-translate-y: 0; - --tw-rotate: 0; - --tw-skew-x: 0; - --tw-skew-y: 0; - --tw-scale-x: 1; - --tw-scale-y: 1; - --tw-pan-x: ; - --tw-pan-y: ; - --tw-pinch-zoom: ; - --tw-scroll-snap-strictness: proximity; - --tw-ordinal: ; - --tw-slashed-zero: ; - --tw-numeric-figure: ; - --tw-numeric-spacing: ; - --tw-numeric-fraction: ; - --tw-ring-inset: ; - --tw-ring-offset-width: 0px; - --tw-ring-offset-color: #fff; - --tw-ring-color: rgb(59 130 246 / 0.5); - --tw-ring-offset-shadow: 0 0 #0000; - --tw-ring-shadow: 0 0 #0000; - --tw-shadow: 0 0 #0000; - --tw-shadow-colored: 0 0 #0000; - --tw-blur: ; - --tw-brightness: ; - --tw-contrast: ; - --tw-grayscale: ; - --tw-hue-rotate: ; - --tw-invert: ; - --tw-saturate: ; - --tw-sepia: ; - --tw-drop-shadow: ; - --tw-backdrop-blur: ; - --tw-backdrop-brightness: ; - --tw-backdrop-contrast: ; - --tw-backdrop-grayscale: ; - --tw-backdrop-hue-rotate: ; - --tw-backdrop-invert: ; - --tw-backdrop-opacity: ; - --tw-backdrop-saturate: ; - --tw-backdrop-sepia: ; -} - ::backdrop { --tw-border-spacing-x: 0; --tw-border-spacing-y: 0; diff --git a/examples/user-management/sveltekit-user-management/src/app.html b/examples/user-management/sveltekit-user-management/src/app.html index b555f2469a..8ce87909fb 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 fd355b36fe..c24beabf92 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3143,6 +3143,37 @@ "node": ">=0.1.95" } }, + "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", @@ -3473,6 +3504,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", @@ -4472,6 +4563,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/@mdx-js/loader": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/@mdx-js/loader/-/loader-2.2.1.tgz", @@ -4834,6 +4949,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", @@ -7050,6 +7173,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", @@ -11216,6 +11562,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", @@ -13730,6 +14081,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", @@ -16462,7 +16831,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" } @@ -19354,6 +19722,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", @@ -19363,6 +19748,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", @@ -21023,6 +21442,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", @@ -23602,6 +24029,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", @@ -23970,6 +24405,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", @@ -24921,6 +25379,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", @@ -26963,6 +27437,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", @@ -30227,6 +30706,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", @@ -30468,6 +30958,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", @@ -34224,6 +34747,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", @@ -34371,6 +34900,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", @@ -35430,6 +35964,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", @@ -36441,6 +36980,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", @@ -36456,6 +37000,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", @@ -38134,6 +38684,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", @@ -38156,12 +38708,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", @@ -38423,6 +38979,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", @@ -40247,6 +40811,37 @@ "minimist": "^1.2.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", @@ -40513,6 +41108,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", @@ -41334,6 +41969,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" + } + }, "@mdx-js/loader": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/@mdx-js/loader/-/loader-2.2.1.tgz", @@ -41573,6 +42232,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", @@ -43285,6 +43949,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", @@ -45316,7 +46146,7 @@ "@sinclair/typebox": "^0.25.1", "pg": "^8.7.1", "pg-format": "^1.0.4", - "pgsql-parser": "^13.3.0", + "pgsql-parser": "13.4.0", "postgres-array": "^3.0.1", "prettier": "^2.6.0", "prettier-plugin-sql": "^0.12.1" @@ -46501,6 +47331,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", @@ -48425,6 +49260,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", @@ -51205,8 +52053,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==" }, "errno": { "version": "0.1.8", @@ -53323,6 +54170,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", @@ -53332,6 +54191,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", @@ -54526,6 +55405,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", @@ -56607,6 +57491,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", @@ -56906,6 +57798,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", @@ -57626,6 +58537,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", @@ -59057,6 +59974,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", @@ -61503,6 +62425,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", @@ -61678,6 +62608,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", @@ -64410,6 +65363,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", @@ -64467,12 +65422,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", @@ -64632,6 +65591,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", @@ -64836,6 +65800,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", @@ -65034,6 +66004,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", @@ -65797,6 +66772,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", @@ -66748,6 +67728,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", @@ -66762,6 +67747,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/index.tsx b/packages/ui/index.tsx index 772f434d34..3edb9ec62c 100644 --- a/packages/ui/index.tsx +++ b/packages/ui/index.tsx @@ -305,6 +305,7 @@ export * from './src/components/Icon/icons/IconSun' export * from './src/components/Icon/icons/IconSunrise' export * from './src/components/Icon/icons/IconSunset' export * from './src/components/Icon/icons/IconTablet' +export * from './src/components/Icon/icons/IconTable' export * from './src/components/Icon/icons/IconTag' export * from './src/components/Icon/icons/IconTarget' export * from './src/components/Icon/icons/IconTerminal' diff --git a/packages/ui/internals/icons.js b/packages/ui/internals/icons.js index 07a7f0e553..1cde50bfbd 100644 --- a/packages/ui/internals/icons.js +++ b/packages/ui/internals/icons.js @@ -234,6 +234,7 @@ export default { IconSunrise: 'src/components/Icon/icons/IconSunrise/index.tsx', IconSunset: 'src/components/Icon/icons/IconSunset/index.tsx', IconTablet: 'src/components/Icon/icons/IconTablet/index.tsx', + IconTable: 'src/components/Icon/icons/IconTable/index.tsx', IconTag: 'src/components/Icon/icons/IconTag/index.tsx', IconTarget: 'src/components/Icon/icons/IconTarget/index.tsx', IconTerminal: 'src/components/Icon/icons/IconTerminal/index.tsx', @@ -284,5 +285,5 @@ export default { IconZapOff: 'src/components/Icon/icons/IconZapOff/index.tsx', IconZap: 'src/components/Icon/icons/IconZap/index.tsx', IconZoomIn: 'src/components/Icon/icons/IconZoomIn/index.tsx', - IconZoomOut: 'src/components/Icon/icons/IconZoomOut/index.tsx' -} \ No newline at end of file + IconZoomOut: 'src/components/Icon/icons/IconZoomOut/index.tsx', +} diff --git a/packages/ui/internals/icons.json b/packages/ui/internals/icons.json index 992be561d2..a348ede4cf 100644 --- a/packages/ui/internals/icons.json +++ b/packages/ui/internals/icons.json @@ -939,6 +939,10 @@ "name": "Tablet", "path": "react-feather/dist/icons/tablet" }, + "Table": { + "name": "Table", + "path": "react-feather/dist/icons/table" + }, "Tag": { "name": "Tag", "path": "react-feather/dist/icons/tag" diff --git a/packages/ui/src/components/Accordion/Accordion.tsx b/packages/ui/src/components/Accordion/Accordion.tsx index 4a802c44ed..167a4414ff 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 + 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 || []) @@ -99,7 +98,7 @@ function Accordion({ className={containerClasses.join(' ')} children={ -
    {children}
    +
    {children}
    } > diff --git a/packages/ui/src/components/Icon/IconImportHandler.tsx b/packages/ui/src/components/Icon/IconImportHandler.tsx index 0d158b428c..3863d41970 100644 --- a/packages/ui/src/components/Icon/IconImportHandler.tsx +++ b/packages/ui/src/components/Icon/IconImportHandler.tsx @@ -241,6 +241,7 @@ export const Sun: Icon = require(`react-feather/dist/icons/sun`) export const Sunrise: Icon = require(`react-feather/dist/icons/sunrise`) export const Sunset: Icon = require(`react-feather/dist/icons/sunset`) export const Tablet: Icon = require(`react-feather/dist/icons/tablet`) +export const Table: Icon = require(`react-feather/dist/icons/table`) export const Tag: Icon = require(`react-feather/dist/icons/tag`) export const Target: Icon = require(`react-feather/dist/icons/target`) export const Terminal: Icon = require(`react-feather/dist/icons/terminal`) diff --git a/packages/ui/src/components/Icon/icons/IconTable/IconTable.tsx b/packages/ui/src/components/Icon/icons/IconTable/IconTable.tsx new file mode 100644 index 0000000000..c0e41b2909 --- /dev/null +++ b/packages/ui/src/components/Icon/icons/IconTable/IconTable.tsx @@ -0,0 +1,13 @@ +// DO NOT EDIT + // this file is generated by /internals/populate-icons script + import * as React from 'react' + // @ts-ignore + import { Table } from 'react-feather' + import IconBase from './../../IconBase' + + function IconTable(props: any) { + return + } + + export default IconTable + \ No newline at end of file diff --git a/packages/ui/src/components/Icon/icons/IconTable/index.tsx b/packages/ui/src/components/Icon/icons/IconTable/index.tsx new file mode 100644 index 0000000000..906c04de23 --- /dev/null +++ b/packages/ui/src/components/Icon/icons/IconTable/index.tsx @@ -0,0 +1 @@ +export { default as IconTable } from './IconTable' \ No newline at end of file diff --git a/packages/ui/src/components/Icon/icons/index.tsx b/packages/ui/src/components/Icon/icons/index.tsx index 9fc2134a5a..5975707ac7 100644 --- a/packages/ui/src/components/Icon/icons/index.tsx +++ b/packages/ui/src/components/Icon/icons/index.tsx @@ -234,6 +234,7 @@ export * as IconSun from './IconSun' export * as IconSunrise from './IconSunrise' export * as IconSunset from './IconSunset' export * as IconTablet from './IconTablet' +export * as IconTable from './IconTable' export * as IconTag from './IconTag' export * as IconTarget from './IconTarget' export * as IconTerminal from './IconTerminal' diff --git a/packages/ui/src/components/SidePanel/SidePanel.tsx b/packages/ui/src/components/SidePanel/SidePanel.tsx index d04b062b3c..b9595e7fec 100644 --- a/packages/ui/src/components/SidePanel/SidePanel.tsx +++ b/packages/ui/src/components/SidePanel/SidePanel.tsx @@ -22,7 +22,7 @@ interface CustomProps { children?: React.ReactNode header?: string | React.ReactNode visible: boolean - size?: 'medium' | 'large' | 'xlarge' + size?: 'medium' | 'large' | 'xlarge' | 'xxlarge' loading?: boolean align?: 'right' | 'left' hideFooter?: boolean diff --git a/packages/ui/src/lib/theme/defaultTheme.ts b/packages/ui/src/lib/theme/defaultTheme.ts index 28fe703fb7..d5bffd2718 100644 --- a/packages/ui/src/lib/theme/defaultTheme.ts +++ b/packages/ui/src/lib/theme/defaultTheme.ts @@ -187,7 +187,11 @@ export default { chevron: { base: ` text-scale-900 - rotate-0 group-state-open:rotate-180 + rotate-0 + group-state-open:rotate-180 + group-data-[state=open]:rotate-180 + ease-[cubic-bezier(0.87,_0,_0.13,_1)] + transition-transform duration-300 duration-200 `, align: { @@ -998,6 +1002,7 @@ export default { medium: `w-screen max-w-md h-full`, large: `w-screen max-w-2xl h-full`, xlarge: `w-screen max-w-3xl h-full`, + xxlarge: `w-screen max-w-4xl h-full`, }, align: { left: ` diff --git a/spec/cli_v1_commands--old.yaml b/spec/cli_v1_commands--old.yaml index d7af310d12..f3671ceb1a 100644 --- a/spec/cli_v1_commands--old.yaml +++ b/spec/cli_v1_commands--old.yaml @@ -380,12 +380,12 @@ commands: ``` - id: supabase-functions-serve title: supabase functions serve - summary: Serve a Function locally + summary: Serve Functions locally tags: [] links: [] usage: |- ```sh - supabase functions serve [flags] + supabase functions serve [flags] ``` subcommands: [] options: |- diff --git a/spec/cli_v1_commands.yaml b/spec/cli_v1_commands.yaml index c060b75634..daf1355200 100644 --- a/spec/cli_v1_commands.yaml +++ b/spec/cli_v1_commands.yaml @@ -761,10 +761,10 @@ commands: flags: [] - id: supabase-functions-serve title: supabase functions serve - summary: Serve a Function locally + summary: Serve all functions locally tags: [] links: [] - usage: supabase functions serve [flags] + usage: supabase functions serve [flags] subcommands: [] flags: - id: env-file diff --git a/spec/common-cli-sections.json b/spec/common-cli-sections.json index 8e5dff8991..127c10a531 100644 --- a/spec/common-cli-sections.json +++ b/spec/common-cli-sections.json @@ -239,7 +239,7 @@ }, { "id": "supabase-functions-serve", - "title": "Serve a function", + "title": "Serve functions locally", "slug": "supabase-functions-serve", "type": "cli-command" }, diff --git a/spec/supabase_js_v2.yml b/spec/supabase_js_v2.yml index 26c48fec45..e59459ce89 100644 --- a/spec/supabase_js_v2.yml +++ b/spec/supabase_js_v2.yml @@ -2258,6 +2258,9 @@ functions: ``` hideCodeBlock: true isSpotlight: true + description: | + When using [reserved words](https://www.postgresql.org/docs/current/sql-keywords-appendix.html) for column names you need + to add double quotes e.g. `.gt('"order"', 2)` - id: gte title: gte() $ref: '@supabase/postgrest-js.PostgrestFilterBuilder.gte' diff --git a/spec/supabase_py_v2.yml b/spec/supabase_py_v2.yml index a3af18a5ac..486e429f3e 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 d839ced0cf..1b7c0d8fbc 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}

    -
    - , - ] - : []), - ]} - > - - + , + ] + : []), + ]} + > + + + )}
    )} diff --git a/studio/components/grid/components/menu/ColumnMenu.tsx b/studio/components/grid/components/menu/ColumnMenu.tsx index d252aa5828..27d5f59b57 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/grid/types/grid.ts b/studio/components/grid/types/grid.ts index a036249a97..bae5c96556 100644 --- a/studio/components/grid/types/grid.ts +++ b/studio/components/grid/types/grid.ts @@ -64,6 +64,15 @@ export interface SupabaseGridProps { */ onEditRow?: (row: SupaRow) => void onError?: (error: any) => void + /** + * Toggle api preview panel open + */ + apiPreviewPanelOpen?: boolean + setApiPreviewPanelOpen?: () => void + /** + * Refresh the docs after a change is made to the table + */ + refreshDocs: () => void onExpandJSONEditor: (column: string, row: SupaRow) => void } diff --git a/studio/components/interfaces/Auth/EmailTemplates/EmailTemplates.tsx b/studio/components/interfaces/Auth/EmailTemplates/EmailTemplates.tsx index a982880a84..85462a7e4b 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 (
    -
    + { {inviteEnabled && }
    -
    +
    diff --git a/studio/components/interfaces/Billing/Billing.constants.tsx b/studio/components/interfaces/Billing/Billing.constants.tsx index 547f5c5460..f253e1bbf0 100644 --- a/studio/components/interfaces/Billing/Billing.constants.tsx +++ b/studio/components/interfaces/Billing/Billing.constants.tsx @@ -1,4 +1,4 @@ -import { Badge, IconArchive, IconCode, IconDatabase, IconKey } from 'ui' +import { IconArchive, IconCode, IconDatabase, IconKey, IconZap } from 'ui' export const CANCELLATION_REASONS = [ 'Pricing', @@ -154,4 +154,36 @@ export const USAGE_BASED_PRODUCTS = [ }, ], }, + { + title: 'Realtime', + icon: , + features: [ + { + key: 'realtime_message_count', + attribute: 'total_realtime_message_count', + title: 'Realtime Messages', + units: 'absolute', + costPerUnit: 0.0000025, + tooltip: ( + + Billing is based on the total amount of messages throughout your billing period. + + ), + }, + + { + key: 'realtime_peak_connection', + attribute: 'total_realtime_peak_connection', + title: 'Realtime Concurrent Peak Connections', + units: 'absolute', + costPerUnit: 0.01, + tooltip: ( + + Billing is based on the maximum amount of concurrent peak connections throughout your + billing period. + + ), + }, + ], + }, ] diff --git a/studio/components/interfaces/Database/Backups/BackupsList.tsx b/studio/components/interfaces/Database/Backups/BackupsList.tsx index 7f4f41434c..b444a8e913 100644 --- a/studio/components/interfaces/Database/Backups/BackupsList.tsx +++ b/studio/components/interfaces/Database/Backups/BackupsList.tsx @@ -35,7 +35,7 @@ const BackupsList: FC = ({}) => { 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 cd43b19d45..daab410640 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 - - - - - + + {!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 4b47575c1d..b634e1365c 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
    - + + + + + {!canTriggerPhysicalBackup && ( + + +
    + + You need additional permissions to trigger a PITR recovery + +
    +
    + )} +
    } > diff --git a/studio/components/interfaces/Database/Extensions/Extensions.constants.ts b/studio/components/interfaces/Database/Extensions/Extensions.constants.ts index 669b441d50..a725a810c9 100644 --- a/studio/components/interfaces/Database/Extensions/Extensions.constants.ts +++ b/studio/components/interfaces/Database/Extensions/Extensions.constants.ts @@ -14,4 +14,5 @@ export const HIDDEN_EXTENSIONS = [ 'supabase_vault', 'intagg', 'xml2', + 'pg_tle', ] diff --git a/studio/components/interfaces/Database/Functions/FunctionsList/FunctionsList.tsx b/studio/components/interfaces/Database/Functions/FunctionsList/FunctionsList.tsx index 46458d9a75..df69111db5 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 956fb1af75..c5059c26e0 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 = ({}) => {

    - + + + + + {!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 c5b668777f..e53ed44286 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 && (
    - + + + + + {!canUpdateColumns && ( + + +
    + + You need additional permissions to create columns + +
    +
    + )} +
    )}
    @@ -103,20 +129,59 @@ const ColumnList: FC = ({
    -
    diff --git a/studio/components/interfaces/Database/Tables/TableList.tsx b/studio/components/interfaces/Database/Tables/TableList.tsx index de85658293..b058f9ce6f 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 && (
    - + + + + + {!canUpdateTables && ( + + +
    + + You need additional permissions to create tables + +
    +
    + )} +
    )}
    @@ -183,7 +207,62 @@ const TableList: FC = ({ > {x.columns.length} columns -
    diff --git a/studio/components/interfaces/Database/Wrappers/CreateWrapper.tsx b/studio/components/interfaces/Database/Wrappers/CreateWrapper.tsx index dbe8fe10c8..242345a01f 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 2286ec04c7..fdcec1aa4b 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 35b7efbed3..896cd11d45 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 ? ( + + +
    ) diff --git a/studio/components/interfaces/Database/Wrappers/Wrappers.tsx b/studio/components/interfaces/Database/Wrappers/Wrappers.tsx index 461d9776b1..529d9cd0c1 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 b74c9a9fff..76a5ce5b6b 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 ( + + + + + + +
    + + You need additional permissions to add wrappers + +
    +
    +
    + ) + } return ( { + setIsGeneratingTypes(true) + const res = await get(`${API_ADMIN_URL}/projects/${ref}/types/typescript`) + + if (!res.error) { + let element = document.createElement('a') + element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(res.types)) + element.setAttribute('download', 'supabase.ts') + element.style.display = 'none' + document.body.appendChild(element) + element.click() + document.body.removeChild(element) + ui.setNotification({ + category: 'success', + message: `Successfully generated types! File is being downloaded`, + }) + } else { + ui.setNotification({ + category: 'error', + message: `Failed to generate types: ${res.error.message}`, + error: res.error, + }) + } + + setIsGeneratingTypes(false) + } + return ( + <> +

    + Generating types + + + + + +

    +
    +
    +

    + Supabase APIs are generated from your database, which means that we can use database + introspection to generate type-safe API definitions. +

    +

    + You can generate types from your database either through the{' '} + + Supabase CLI + + , or by downloading the types file via the button on the right and importing it in your + application within src/index.ts. +

    +
    +
    +
    +

    + {selectedLang === 'js' && ( + + )} +

    +

    + Remember to re-generate and download this file as you make changes to your tables. +

    +
    + + +
    +
    + + ) +} + +const localSnippets = { + cliLogin: () => ({ + title: 'Login via the CLI with your Personal Access Token', + bash: { + code: ` +npx supabase login +`, + }, + }), + generateTypes: (ref: string) => ({ + title: 'Generate types', + bash: { + code: ` +npx supabase gen types typescript --project-id "${ref}" --schema public > types/supabase.ts +`, + }, + }), +} diff --git a/studio/components/interfaces/Docs/LangSelector.tsx b/studio/components/interfaces/Docs/LangSelector.tsx new file mode 100644 index 0000000000..a0e0527287 --- /dev/null +++ b/studio/components/interfaces/Docs/LangSelector.tsx @@ -0,0 +1,110 @@ +import { FC } from 'react' +import { Button, Dropdown, IconKey } from 'ui' +import { checkPermissions } from 'hooks' +import { PermissionAction } from '@supabase/shared-types/out/constants' + +import { showApiKey } from 'components/interfaces/Docs/Docs.types' + +const DEFAULT_KEY = { name: 'hide', key: 'SUPABASE_KEY' } + +interface Props { + selectedLang: string + setSelectedLang: (selectedLang: string) => void + showApiKey: showApiKey + setShowApiKey: (showApiKey: showApiKey) => void + apiKey: string | undefined + autoApiService: any +} + +const LangSelector: FC = ({ + selectedLang, + setSelectedLang, + showApiKey, + setShowApiKey, + apiKey, + autoApiService, +}) => { + const canReadServiceKey = checkPermissions( + PermissionAction.READ, + 'service_api_keys.service_role_key' + ) + + return ( +
    +
    + + + {selectedLang == 'bash' && ( +
    +
    + + Project API key : +
    + + setShowApiKey(DEFAULT_KEY)}> + hide + + {apiKey && ( + + setShowApiKey({ + key: apiKey, + name: 'anon (public)', + }) + } + > + anon (public) + + )} + {canReadServiceKey && ( + + setShowApiKey({ + key: autoApiService.serviceApiKey, + name: 'service_role (secret)', + }) + } + > + service_role (secret) + + )} + + } + > + + +
    + )} +
    +
    + ) +} + +export default LangSelector diff --git a/studio/components/interfaces/Docs/ResourceContent.tsx b/studio/components/interfaces/Docs/ResourceContent.tsx index a37ed88408..21e6a3dc5f 100644 --- a/studio/components/interfaces/Docs/ResourceContent.tsx +++ b/studio/components/interfaces/Docs/ResourceContent.tsx @@ -2,6 +2,7 @@ import Snippets from 'components/to-be-cleaned/Docs/Snippets' import CodeSnippet from 'components/to-be-cleaned/Docs/CodeSnippet' import Param from 'components/to-be-cleaned/Docs/Param' import Description from 'components/to-be-cleaned/Docs/Description' +import { IconTable } from 'ui' const ResourceContent = ({ autoApiService, @@ -29,12 +30,18 @@ const ResourceContent = ({ return ( <> -

    - {resourceId} +

    + + + + {resourceId}

    -
    +
    + {properties.map((x) => (
    -
    +
    -
    -
    +
    +
    -
    +
    ))} @@ -87,8 +94,8 @@ const ResourceContent = ({ To read rows in {resourceId}, use the select method.

    - - Learn more. + + Learn more

    @@ -120,8 +127,11 @@ const ResourceContent = ({

    Filtering

    Supabase provides a wide range of filters.

    - - Learn more. + + Learn more

    @@ -147,8 +157,8 @@ const ResourceContent = ({ insert will also return the replaced values for UPSERT.

    - - Learn more. + + Learn more

    @@ -183,8 +193,8 @@ const ResourceContent = ({ update will also return the replaced values for UPDATE.

    - - Learn more. + + Learn more

    @@ -207,8 +217,8 @@ const ResourceContent = ({ default, so remember to specify your filters!

    - - Learn more. + + Learn more

    @@ -230,8 +240,8 @@ const ResourceContent = ({ users depending on Row Level Security (RLS) policies.

    - - Learn more. + + Learn more

    @@ -264,21 +274,6 @@ const ResourceContent = ({ - <> -

    Much more

    -
    -
    -

    - These docs are a work in progress! See our{' '} - - docs - {' '} - for the additional functionality Supabase has to offer. -

    -
    -
    -
    - ) } diff --git a/studio/components/interfaces/Functions/EdgeFunctionDetails.tsx b/studio/components/interfaces/Functions/EdgeFunctionDetails.tsx new file mode 100644 index 0000000000..10c2fffa64 --- /dev/null +++ b/studio/components/interfaces/Functions/EdgeFunctionDetails.tsx @@ -0,0 +1,332 @@ +import dayjs from 'dayjs' +import { useRouter } from 'next/router' +import { FC, useState, useEffect } from 'react' +import * as Tooltip from '@radix-ui/react-tooltip' +import { PermissionAction } from '@supabase/shared-types/out/constants' +import { + Alert, + IconGlobe, + IconTerminal, + IconMinimize2, + IconMaximize2, + IconCheck, + IconClipboard, + Button, + Modal, +} from 'ui' + +import { useStore, useParams, checkPermissions } from 'hooks' +import Panel from 'components/ui/Panel' +import CommandRender from './CommandRender' +import { useProjectApiQuery } from 'data/config/project-api-query' +import { useEdgeFunctionDeleteMutation } from 'data/edge-functions/edge-functions-delete-mutation' + +interface Props {} + +// [Joshen] Next - additional configs: Verify jwt + import maps + +const EdgeFunctionDetails: FC = () => { + 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 + : '[YOUR ANON KEY]' + + 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}

    + +
    + +
    +

    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 +

    + + + + + {!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 0000000000..0d9ce1f727 --- /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`) + }} + > + +
    +

    {item.name}

    +
    +
    + +
    +

    {functionUrl}

    + +
    +
    + +

    {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 f2b9045d41..0000000000 --- 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 dfe6bd5ab1..af24e28d29 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 f8f30b523a..a5770812ef 100644 --- a/studio/components/interfaces/Functions/TerminalInstructions.tsx +++ b/studio/components/interfaces/Functions/TerminalInstructions.tsx @@ -10,23 +10,20 @@ 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 - : undefined + : '[YOUR ANON KEY]' const endpoint = settings?.autoApiService.app_config.endpoint ?? '' const endpointSections = endpoint.split('.') @@ -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 b096ff5e60..003d5feaca 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 0000000000..c1aa2b81a9 --- /dev/null +++ b/studio/components/interfaces/GraphQL/GraphiQL.tsx @@ -0,0 +1,590 @@ +/* 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' + accessToken?: string +} + +const GraphiQL = ({ fetcher, theme = 'dark', accessToken }: 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} +
    + +
    +
    {PluginContent ? : 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 keyFunction
    + {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 GraphiQL uses sublime + . +

    +
    +
    +
    + { + setShowDialog(null) + setClearStorageStatus(null) + }} + > +
    +
    Settings
    + { + setShowDialog(null) + setClearStorageStatus(null) + }} + /> +
    + + {storageContext ? ( +
    +
    +
    Clear storage
    +
    + Remove all locally stored data and start fresh. +
    +
    +
    + +
    +
    + ) : null} +
    +
    + ) +} + +// Configure the UI by providing this Component as a child of GraphiQL. +function GraphiQLLogo(props: PropsWithChildren) { + return ( +
    + {props.children || ( + + Graph + i + QL + + )} +
    + ) +} + +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 0000000000..dc574beed6 --- /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/Home/Home.constants.ts b/studio/components/interfaces/Home/Home.constants.ts index 0bbcef3fcd..319aa46eb5 100644 --- a/studio/components/interfaces/Home/Home.constants.ts +++ b/studio/components/interfaces/Home/Home.constants.ts @@ -75,7 +75,7 @@ export const EXAMPLE_PROJECTS = [ framework: 'NextJS', title: 'Next.js todo list app', description: 'NextJS todo list example', - url: 'https://github.com/supabase/examples/tree/main/supabase-js-v1/todo-list/nextjs-todo-list', + url: 'https://github.com/supabase/supabase/tree/master/examples/todo-list/nextjs-todo-list', }, { framework: 'React', diff --git a/studio/components/interfaces/Organization/TeamSettings/MemberActions.tsx b/studio/components/interfaces/Organization/TeamSettings/MemberActions.tsx index a920d38809..9bd55624df 100644 --- a/studio/components/interfaces/Organization/TeamSettings/MemberActions.tsx +++ b/studio/components/interfaces/Organization/TeamSettings/MemberActions.tsx @@ -15,12 +15,11 @@ import { isInviteExpired } from '../Organization.utils' import { getRolesManagementPermissions } from './TeamSettings.utils' interface Props { - members: Member[] member: Member roles: Role[] } -const MemberActions: FC = ({ members, member, roles }) => { +const MemberActions: FC = ({ member, roles }) => { const { ui } = useStore() const { slug } = useParams() const { rolesRemovable } = getRolesManagementPermissions(roles) diff --git a/studio/components/interfaces/Organization/TeamSettings/MembersView.tsx b/studio/components/interfaces/Organization/TeamSettings/MembersView.tsx index c54088028e..925ddf81cc 100644 --- a/studio/components/interfaces/Organization/TeamSettings/MembersView.tsx +++ b/studio/components/interfaces/Organization/TeamSettings/MembersView.tsx @@ -191,6 +191,7 @@ const MembersView: FC = ({ searchString, roles, members }) => { value={r.id} label={r.name} disabled={disableRoleEdit} + className="w-36" > {r.name} @@ -232,9 +233,7 @@ const MembersView: FC = ({ searchString, roles, members }) => { )} - {!memberIsUser && ( - - )} + {!memberIsUser && } diff --git a/studio/components/interfaces/Reports/Reports.constants.ts b/studio/components/interfaces/Reports/Reports.constants.ts index 3713345e5f..17183a514b 100644 --- a/studio/components/interfaces/Reports/Reports.constants.ts +++ b/studio/components/interfaces/Reports/Reports.constants.ts @@ -242,6 +242,90 @@ order by }, }, }, + [Presets.QUERY_PERFORMANCE]: { + title: 'Query performance', + queries: { + mostFrequentlyInvoked: { + queryType: 'db', + sql: (_params) => ` +-- Most frequently called queries +-- A limit of 100 has been added below +select + auth.rolname, + statements.query, + statements.calls, + -- -- Postgres 13, 14, 15 + statements.total_exec_time + statements.total_plan_time as total_time, + statements.min_exec_time + statements.min_plan_time as min_time, + statements.max_exec_time + statements.max_plan_time as max_time, + statements.mean_exec_time + statements.mean_plan_time as mean_time, + -- -- Postgres <= 12 + -- total_time, + -- min_time, + -- max_time, + -- mean_time, + statements.rows / statements.calls as avg_rows + from pg_stat_statements as statements + inner join pg_authid as auth on statements.userid = auth.oid + order by + statements.calls desc + limit 10;`, + }, + mostTimeConsuming: { + queryType: 'db', + sql: (_params) => `-- A limit of 100 has been added below +select + auth.rolname, + statements.query, + statements.calls, + statements.total_exec_time + statements.total_plan_time as total_time, + to_char(((statements.total_exec_time + statements.total_plan_time)/sum(statements.total_exec_time + statements.total_plan_time) OVER()) * 100, 'FM90D0') || '%' AS prop_total_time + from pg_stat_statements as statements + inner join pg_authid as auth on statements.userid = auth.oid + order by + total_time desc + limit 10;`, + }, + slowestExecutionTime: { + queryType: 'db', + sql: (_params) => `-- Slowest queries by max execution time +-- A limit of 100 has been added below +select + auth.rolname, + statements.query, + statements.calls, + -- -- Postgres 13, 14, 15 + statements.total_exec_time + statements.total_plan_time as total_time, + statements.min_exec_time + statements.min_plan_time as min_time, + statements.max_exec_time + statements.max_plan_time as max_time, + statements.mean_exec_time + statements.mean_plan_time as mean_time, + -- -- Postgres <= 12 + -- total_time, + -- min_time, + -- max_time, + -- mean_time, + statements.rows / statements.calls as avg_rows + from pg_stat_statements as statements + inner join pg_authid as auth on statements.userid = auth.oid + order by + max_time desc + limit 10`, + }, + queryHitRate: { + queryType: 'db', + sql: (_params) => `-- Cache and index hit rate +select + 'index hit rate' as name, + (sum(idx_blks_hit)) / nullif(sum(idx_blks_hit + idx_blks_read),0) as ratio + from pg_statio_user_indexes + union all + select + 'table hit rate' as name, + sum(heap_blks_hit) / nullif(sum(heap_blks_hit) + sum(heap_blks_read),0) as ratio + from pg_statio_user_tables;`, + }, + }, + }, } export const DATETIME_FORMAT = 'MMM D, ha' diff --git a/studio/components/interfaces/Reports/Reports.types.ts b/studio/components/interfaces/Reports/Reports.types.ts index 0cc81dd470..af6ca322b0 100644 --- a/studio/components/interfaces/Reports/Reports.types.ts +++ b/studio/components/interfaces/Reports/Reports.types.ts @@ -4,6 +4,7 @@ import { DEFAULT_QUERY_PARAMS } from './Reports.constants' export enum Presets { API = 'api', AUTH = 'auth', + QUERY_PERFORMANCE = 'query_performance', } export interface QueryDataBase { diff --git a/studio/components/interfaces/Reports/Reports.utils.tsx b/studio/components/interfaces/Reports/Reports.utils.tsx index 006ee0e17d..507176c6cc 100644 --- a/studio/components/interfaces/Reports/Reports.utils.tsx +++ b/studio/components/interfaces/Reports/Reports.utils.tsx @@ -64,16 +64,24 @@ export const usePresetReport = (hooks: PresetHookResult[]) => { } const isLoading = data.map((datum) => datum.isLoading).some((v) => v) - const Layout: React.FC<{ title: string }> = ({ title, children }) => ( + const Layout: React.FC<{ title: string; showDatePickers?: boolean }> = ({ + title, + showDatePickers = true, + children, + }) => (

    {title}

    - +
    + {showDatePickers && ( + + )} +
    + + {disabled && ( + + +
    + + You need additional permissions to update network restrictions + +
    +
    + )} + +) + +const DisallowAllAccessButton: FC<{ disabled: boolean; onClick: (value: boolean) => void }> = ({ + disabled, + onClick, +}) => ( + + + + + {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 = ({}) => { - + + + + + {!canUpdateNetworkRestrictions && ( + + +
    + + You need additional permissions to update network restrictions + +
    +
    + )} +
    {isLoading ? ( @@ -75,12 +159,14 @@ const NetworkRestrictions = ({}) => {

    - - + +
    @@ -101,9 +187,10 @@ const NetworkRestrictions = ({}) => {
    - +
    ) : isDisallowedAll ? ( @@ -125,9 +212,10 @@ const NetworkRestrictions = ({}) => {
    - +
    ) : ( @@ -146,12 +234,14 @@ const NetworkRestrictions = ({}) => {

    - - + +
    {restrictedIps.map((ip) => { diff --git a/studio/components/interfaces/Settings/Database/SSLConfiguration.tsx b/studio/components/interfaces/Settings/Database/SSLConfiguration.tsx index 192c613789..9f1f859690 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 = @@ -118,7 +122,7 @@ const SSLConfiguration = () => { )} diff --git a/studio/components/interfaces/Settings/Logs/Logs.constants.ts b/studio/components/interfaces/Settings/Logs/Logs.constants.ts index 1ad8909622..2445383509 100644 --- a/studio/components/interfaces/Settings/Logs/Logs.constants.ts +++ b/studio/components/interfaces/Settings/Logs/Logs.constants.ts @@ -200,6 +200,28 @@ limit 100 `, for: ['database'], }, + { + label: 'Storage Object Requests', + description: 'Number of requests done on Storage Objects', + mode: 'custom', + searchString: `select + r.method as http_verb, + r.path as filepath, + count(*) as num_requests + from edge_logs + cross join unnest(metadata) as m + cross join unnest(m.request) AS r + cross join unnest(r.headers) AS h + where + path like '%rest/v1/object%' + group by + r.path, r.method + order by + num_requests desc + limit 100 +`, + for: ['api'], + }, ] export const LOG_TYPE_LABEL_MAPPING: { [k: string]: string } = { diff --git a/studio/components/interfaces/Settings/Logs/LogsErrorRenderers/ResourcesExceededErrorRenderer.tsx b/studio/components/interfaces/Settings/Logs/LogsErrorRenderers/ResourcesExceededErrorRenderer.tsx index 2473f64aa1..c8b76284ad 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 a7d3134706..c24fbe5a68 100644 --- a/studio/components/interfaces/Settings/ProjectUsageBars/ProjectUsageBars.tsx +++ b/studio/components/interfaces/Settings/ProjectUsageBars/ProjectUsageBars.tsx @@ -1,16 +1,29 @@ +import Link from 'next/link' import { FC, useEffect } from 'react' -import { Badge, Button, IconAlertCircle, IconInfo, Loading, IconExternalLink } from 'ui' +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 { useProjectReadOnlyQuery } from 'data/config/project-read-only-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,18 @@ const ProjectUsage: FC = ({ projectRef }) => { const { data: usage, error, isLoading } = useProjectUsageQuery({ projectRef }) const router = useRouter() - const subscriptionTier = ui.selectedProject?.subscription_tier + const { project } = useProjectContext() + const { data: isReadOnlyMode } = useProjectReadOnlyQuery({ + projectRef: project?.ref, + connectionString: project?.connectionString, + }) + 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 || @@ -62,13 +85,12 @@ const ProjectUsage: FC = ({ projectRef }) => { const isPaidTier = subscriptionTier !== PRICING_TIER_PRODUCT_IDS.FREE - const featureFootnotes: Record = { - db_size: ( + const featureFootnotes: Record = { + db_size: isPaidTier ? (
    {usage?.disk_volume_size_gb && Disk Size: {usage.disk_volume_size_gb} GB} - - {isPaidTier && Auto-Scaling} + Auto-Scaling
    - ), + ) : null, } return ( @@ -127,7 +149,6 @@ const ProjectUsage: FC = ({ projectRef }) => { - {/* Line items */} {usage === undefined ? (
    @@ -148,14 +169,16 @@ const ProjectUsage: FC = ({ projectRef }) => { usageElement = (
    Not included in {planName} tier - + {canUpdateSubscription && ( + + )}
    ) } else if (showUsageExceedMessage) { @@ -253,6 +276,121 @@ const ProjectUsage: FC = ({ projectRef }) => { )} + + {isReadOnlyMode && product.title === 'Database' && ( +
    + +

    + {isPaidTier ? ( + <> + Your disk has reached 95% capacity and has entered{' '} + + read-only mode + + . + + ) : ( + <> + You have exceeded the 500mb Database size limit and your project is now + in{' '} + + read-only mode + + . + + )} +

    + {isPaidTier ? ( + <> +

    + For Pro and Enterprise projects,{' '} + + Disk Size expands automatically + {' '} + when it reaches 90% capacity, but can only occur once every six hours. + If the disk size has already expanded and then reaches 95% capacity + within 6 hours, then{' '} + + your disk will enter read-only mode + {' '} + until it can resize again after 6 hours. +

    +

    + If you require help or need your disk changed to read/write mode so you + can delete data,{' '} + + + you can contact the support team + + + . +

    + + ) : ( +

    + You can either{' '} + + reduce your disk usage below 500mb + {' '} + or{' '} + + + upgrade your project + + + . +

    + )} + + {!isPaidTier ? ( + + ) : ( + + )} +
    +
    + )}
    ) })} diff --git a/studio/components/interfaces/Settings/Vault/Keys/EncryptionKeysManagement.tsx b/studio/components/interfaces/Settings/Vault/Keys/EncryptionKeysManagement.tsx index 876805da8a..63cac0d982 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 = ({}) => { - + + + + + {!canManageKeys && ( + + +
    + + You need additional permissions to add keys + +
    +
    + )} +
    @@ -178,12 +204,32 @@ const EncryptionKeysManagement: FC = ({}) => {

    Added on {dayjs(key.created).format('MMM D, YYYY')}

    - - + + + + + {!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 198e9a2a96..9996fc8848 100644 --- a/studio/components/interfaces/TableGridEditor/GridHeaderActions.tsx +++ b/studio/components/interfaces/TableGridEditor/GridHeaderActions.tsx @@ -1,21 +1,78 @@ import { FC } from 'react' import Link from 'next/link' -import { Button, IconAlertCircle, IconLock } from 'ui' +import { Button, IconAlertCircle, IconCode, IconLock } from 'ui' +import * as Tooltip from '@radix-ui/react-tooltip' 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 + apiPreviewPanelOpen: boolean + setApiPreviewPanelOpen: (apiPreviewPanelOpen: boolean) => void + refreshDocs: () => void } -const GridHeaderActions: FC = ({ table }) => { +const GridHeaderActions: FC = ({ + table, + apiPreviewPanelOpen, + setApiPreviewPanelOpen, + refreshDocs, +}) => { const { ui, meta } = useStore() 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') + + function handlePreviewToggle() { + setApiPreviewPanelOpen(!apiPreviewPanelOpen) + refreshDocs() + } + + const RenderAPIPreviewToggle = () => { + return ( + + ) + } + return (
    + {isReadOnly && ( + + +
    + Viewing as read-only +
    +
    + + +
    + + You need additional permissions to manage your project's data + +
    +
    +
    + )} +
    + +
    + + + + + +
    - - {name && ( - <> - - - - - -
    {name}
    - - )} {item && } @@ -140,6 +161,24 @@ const FunctionsLayout: FC = ({ title, children }) => { )} + + setShowTerminalInstructions(false)} + header={

    Deploying an edge function to your project

    } + customFooter={ +
    + +
    + } + > +
    + +
    +
    ) } diff --git a/studio/components/layouts/ProjectLayout/ConnectingState.tsx b/studio/components/layouts/ProjectLayout/ConnectingState.tsx index a412447425..4378343983 100644 --- a/studio/components/layouts/ProjectLayout/ConnectingState.tsx +++ b/studio/components/layouts/ProjectLayout/ConnectingState.tsx @@ -37,9 +37,7 @@ const ConnectingState: FC = ({ project }) => { }, [project]) const testProjectConnection = async () => { - const result = await pingPostgrest(project.restUrl!, project.ref, { - kpsVersion: project.kpsVersion, - }) + const result = await pingPostgrest(project.ref, { kpsVersion: project.kpsVersion }) if (result) { clearInterval(checkProjectConnectionIntervalRef.current) app.onProjectPostgrestStatusUpdated(project.id, 'ONLINE') diff --git a/studio/components/layouts/ProjectLayout/LayoutHeader/LayoutHeader.tsx b/studio/components/layouts/ProjectLayout/LayoutHeader/LayoutHeader.tsx index cf8789a80e..90700cd6c3 100644 --- a/studio/components/layouts/ProjectLayout/LayoutHeader/LayoutHeader.tsx +++ b/studio/components/layouts/ProjectLayout/LayoutHeader/LayoutHeader.tsx @@ -11,19 +11,30 @@ 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 { useProjectReadOnlyQuery } from 'data/config/project-read-only-query' +import { Badge } from 'ui' +import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' const LayoutHeader = ({ customHeaderComponents, breadcrumbs = [], headerBorder = true }: any) => { const { ui } = useStore() const { selectedOrganization, selectedProject } = ui const { ref: projectRef } = useParams() + const { project } = useProjectContext() + + const { data: isReadOnlyMode } = useProjectReadOnlyQuery({ + projectRef: project?.ref, + connectionString: project?.connectionString, + }) 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 +74,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/ReportsLayout/ReportsMenu.utils.ts b/studio/components/layouts/ReportsLayout/ReportsMenu.utils.ts index 8e6142aaef..a2da5b72d1 100644 --- a/studio/components/layouts/ReportsLayout/ReportsMenu.utils.ts +++ b/studio/components/layouts/ReportsLayout/ReportsMenu.utils.ts @@ -42,6 +42,13 @@ export const generateReportsMenu = (project?: Project): ProductMenuGroup[] => { url: `/project/${ref}/reports/database`, items: [], }, + { + name: 'Query Performance', + key: 'query-performance', + url: `/project/${ref}/reports/query-performance`, + items: [], + label: 'NEW', + }, ], }, ] diff --git a/studio/components/layouts/SignInLayout/SignInLayout.tsx b/studio/components/layouts/SignInLayout/SignInLayout.tsx index a9a88f4834..4732fc9617 100644 --- a/studio/components/layouts/SignInLayout/SignInLayout.tsx +++ b/studio/components/layouts/SignInLayout/SignInLayout.tsx @@ -118,7 +118,7 @@ const SignInLayout = ({
    -
    +

    {heading}

    {subheading}

    diff --git a/studio/components/layouts/StorageLayout/BucketRow.tsx b/studio/components/layouts/StorageLayout/BucketRow.tsx index f96a5d96b4..79e9cf91ac 100644 --- a/studio/components/layouts/StorageLayout/BucketRow.tsx +++ b/studio/components/layouts/StorageLayout/BucketRow.tsx @@ -1,6 +1,8 @@ import { FC } from 'react' import { Badge, Dropdown, IconLoader, IconMoreVertical, IconTrash } from 'ui' +import { PermissionAction } from '@supabase/shared-types/out/constants' +import { checkPermissions } from 'hooks' import ProductMenuItem from 'components/ui/ProductMenu/ProductMenuItem' import { STORAGE_ROW_STATUS } from 'components/to-be-cleaned/Storage/Storage.constants' @@ -19,6 +21,8 @@ const BucketRow: FC = ({ onSelectDeleteBucket = () => {}, onSelectToggleBucketPublic = () => {}, }) => { + const canUpdateBuckets = checkPermissions(PermissionAction.STORAGE_ADMIN_WRITE, '*') + return ( = ({ action={ bucket.status === STORAGE_ROW_STATUS.LOADING ? ( - ) : bucket.status === STORAGE_ROW_STATUS.READY ? ( + ) : canUpdateBuckets && bucket.status === STORAGE_ROW_STATUS.READY ? ( = ({ title, children }) => { const { data: settings, isLoading } = useProjectApiQuery({ projectRef }) const apiService = settings?.autoApiService - const serviceKey = find(apiService?.service_api_keys ?? [], (key) => key.tags === 'service_role') - const canAccessStorage = !isLoading && apiService && serviceKey useEffect(() => { if (!isLoading && apiService) initializeStorageStore(apiService) @@ -51,15 +49,13 @@ const StorageLayout: FC = ({ title, children }) => { const initializeStorageStore = async (apiService: AutoApiService) => { if (apiService.endpoint) { - if (serviceKey) { - storageExplorerStore.initStore( - projectRef, - apiService.endpoint, - apiService.serviceApiKey, - apiService.protocol - ) - await storageExplorerStore.fetchBuckets() - } + storageExplorerStore.initStore( + projectRef, + apiService.endpoint, + apiService.serviceApiKey, + apiService.protocol + ) + await storageExplorerStore.fetchBuckets() } else { ui.setNotification({ category: 'error', @@ -99,15 +95,15 @@ const StorageLayout: FC = ({ title, children }) => { } } - if (!isLoading && !canAccessStorage) { - return ( - -
    - -
    -
    - ) - } + // if (!isLoading && !canAccessStorage) { + // return ( + // + //
    + // + //
    + //
    + // ) + // } return ( }> diff --git a/studio/components/layouts/StorageLayout/StorageMenu.tsx b/studio/components/layouts/StorageLayout/StorageMenu.tsx index 441e07c40a..1d2a88de2f 100644 --- a/studio/components/layouts/StorageLayout/StorageMenu.tsx +++ b/studio/components/layouts/StorageLayout/StorageMenu.tsx @@ -2,9 +2,11 @@ import { FC } from 'react' import Link from 'next/link' 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 { PermissionAction } from '@supabase/shared-types/out/constants' +import { checkPermissions, useParams } from 'hooks' import BucketRow from './BucketRow' import { useStorageStore } from 'localStores/storageExplorer/StorageExplorerStore' @@ -13,6 +15,7 @@ interface Props {} const StorageMenu: FC = () => { const router = useRouter() const { ref, bucketId } = useParams() + const canCreateBuckets = checkPermissions(PermissionAction.STORAGE_ADMIN_WRITE, '*') const page = router.pathname.split('/')[4] as | undefined @@ -33,19 +36,39 @@ const StorageMenu: FC = () => { return (
    -
    - } - style={{ justifyContent: 'start' }} - onClick={openCreateBucketModal} - > - New bucket - + + +
    + } + disabled={!canCreateBuckets} + style={{ justifyContent: 'start' }} + onClick={openCreateBucketModal} + > + New bucket + + + {!canCreateBuckets && ( + + +
    + + You need additional permissions to create buckets + +
    +
    + )} +
    diff --git a/studio/components/to-be-cleaned/Docs/CodeSnippet.js b/studio/components/to-be-cleaned/Docs/CodeSnippet.js index 02bd93d5df..47acdeff5d 100644 --- a/studio/components/to-be-cleaned/Docs/CodeSnippet.js +++ b/studio/components/to-be-cleaned/Docs/CodeSnippet.js @@ -3,12 +3,12 @@ import SimpleCodeBlock from 'components/to-be-cleaned/SimpleCodeBlock' const CodeSnippet = ({ selectedLang, snippet }) => { if (!snippet[selectedLang]) return null return ( - <> +

    {snippet.title}

    {snippet[selectedLang].code} - +
    ) } export default CodeSnippet diff --git a/studio/components/to-be-cleaned/Docs/Pages/Authentication.js b/studio/components/to-be-cleaned/Docs/Pages/Authentication.js index 4ad66d322f..651fe8142e 100644 --- a/studio/components/to-be-cleaned/Docs/Pages/Authentication.js +++ b/studio/components/to-be-cleaned/Docs/Pages/Authentication.js @@ -5,9 +5,13 @@ import CodeSnippet from '../CodeSnippet' export default function Authentication({ autoApiService, selectedLang, showApiKey }) { // [Joshen] ShowApiKey should really be a boolean, its confusing const defaultApiKey = - showApiKey !== 'SUPABASE_KEY' ? autoApiService.defaultApiKey : 'SUPABASE_CLIENT_API_KEY' + showApiKey !== 'SUPABASE_KEY' + ? autoApiService?.defaultApiKey ?? 'SUPABASE_CLIENT_API_KEY' + : 'SUPABASE_CLIENT_API_KEY' const serviceApiKey = - showApiKey !== 'SUPABASE_KEY' ? autoApiService.serviceApiKey : 'SUPABASE_SERVICE_KEY' + showApiKey !== 'SUPABASE_KEY' + ? autoApiService?.serviceApiKey ?? 'SUPABASE_SERVICE_KEY' + : 'SUPABASE_SERVICE_KEY' return ( <> diff --git a/studio/components/to-be-cleaned/Docs/Pages/Introduction.js b/studio/components/to-be-cleaned/Docs/Pages/Introduction.js deleted file mode 100644 index 30e2430103..0000000000 --- a/studio/components/to-be-cleaned/Docs/Pages/Introduction.js +++ /dev/null @@ -1,59 +0,0 @@ -import Snippets from '../Snippets' -import CodeSnippet from '../CodeSnippet' - -export default function Introduction({ autoApiService, selectedLang }) { - return ( - <> -

    Introduction

    -
    -
    -

    - This API provides an easy way to integrate with your Postgres database. The API - documentation below is specifically generated for your database. -

    -

    - This is an auto-generating API, so as you make changes to your database, this - documentation will change too. -

    -

    - Please note: if you make changes to a field (column) name or type, the API - interface for those fields will change correspondingly. Therefore, please make sure to - update your API implementation accordingly whenever you make changes to your Supabase - schema from the graphical interface. -

    -
    -
    - -

    API URL

    -
    -
    -

    The API URL for your project.

    -
    -
    - -
    -
    - -

    Client Libraries

    -
    -
    -

    Your API consists of both a RESTful interface and a Realtime interface.

    -

    - For interacting with the Realtime streams, we provide client libraries that handle the - websockets. -

    -
    -
    - - -
    -
    - - ) -} diff --git a/studio/components/to-be-cleaned/Docs/Pages/Introduction.tsx b/studio/components/to-be-cleaned/Docs/Pages/Introduction.tsx new file mode 100644 index 0000000000..61f132b1cf --- /dev/null +++ b/studio/components/to-be-cleaned/Docs/Pages/Introduction.tsx @@ -0,0 +1,112 @@ +import Snippets from '../Snippets' +import CodeSnippet from '../CodeSnippet' +import Image from 'next/image' +import { useStore } from 'hooks' +import { AutoApiService } from 'data/config/project-api-query' + +const libs = [ + { + name: 'Javascript', + url: 'https://supabase.com/docs/reference/javascript/introduction', + icon: 'javascript', + }, + { name: 'Flutter', url: 'https://supabase.com/docs/reference/dart/introduction', icon: 'dart' }, + { + name: 'Python', + url: 'https://supabase.com/docs/reference/python/introduction', + icon: 'python', + }, + { name: 'C#', url: 'https://supabase.com/docs/reference/csharp/introduction', icon: 'csharp' }, +] + +interface Props { + autoApiService: AutoApiService + selectedLang: string +} + +export default function Introduction({ autoApiService, selectedLang }: Props) { + const { ui } = useStore() + const { isDarkTheme } = ui + + return ( + <> +

    Introduction

    +
    +
    +

    + This API provides an easy way to integrate with your Postgres database. The API + documentation below is specifically generated for your database. +

    +

    + This is an auto-generating API, so as you make changes to your database, this + documentation will change too. +

    +

    + Note: if you make changes to a field (column) name or type, the API interface for + those fields will change correspondingly. Therefore, please make sure to update your API + implementation accordingly whenever you make changes to your Supabase schema from the + graphical interface. +

    + +
    +

    Read the reference documentation:

    +
    + {libs.map((lib) => ( + + {lib.name} + {lib.name} + + ))} +
    +
    +
    +
    + +

    API URL

    +
    +
    +

    The API URL for your project.

    +
    +
    + +
    +
    + +

    Client Libraries

    +
    +
    +

    Your API consists of both a RESTful interface and a Realtime interface.

    +

    + For interacting with the Realtime streams, we provide client libraries that handle the + websockets. +

    +
    +
    + + +
    +
    + + ) +} diff --git a/studio/components/to-be-cleaned/Docs/Pages/Tables/Introduction.tsx b/studio/components/to-be-cleaned/Docs/Pages/Tables/Introduction.tsx index 8ef26409da..ba2441ad01 100644 --- a/studio/components/to-be-cleaned/Docs/Pages/Tables/Introduction.tsx +++ b/studio/components/to-be-cleaned/Docs/Pages/Tables/Introduction.tsx @@ -1,10 +1,8 @@ -import { FC, useState } from 'react' +import { FC } from 'react' import Link from 'next/link' -import { Button, IconDownload, IconExternalLink } from 'ui' -import { useParams, useStore } from 'hooks' -import { API_ADMIN_URL } from 'lib/constants' -import { get } from 'lib/common/fetch' +import { useParams } from 'hooks' import CodeSnippet from '../../CodeSnippet' +import GeneratingTypes from 'components/interfaces/Docs/GeneratingTypes' interface Props { selectedLang: string @@ -12,35 +10,6 @@ interface Props { const Introduction: FC = ({ selectedLang }) => { const { ref } = useParams() - const { ui } = useStore() - const [isGeneratingTypes, setIsGeneratingTypes] = useState(false) - - const generateTypes = async () => { - setIsGeneratingTypes(true) - const res = await get(`${API_ADMIN_URL}/projects/${ref}/types/typescript`) - - if (!res.error) { - let element = document.createElement('a') - element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(res.types)) - element.setAttribute('download', 'supabase.ts') - element.style.display = 'none' - document.body.appendChild(element) - element.click() - document.body.removeChild(element) - ui.setNotification({ - category: 'success', - message: `Successfully generated types! File is being downloaded`, - }) - } else { - ui.setNotification({ - category: 'error', - message: `Failed to generate types: ${res.error.message}`, - error: res.error, - }) - } - - setIsGeneratingTypes(false) - } return ( <> @@ -65,48 +34,7 @@ const Introduction: FC = ({ selectedLang }) => {
    -

    - Generating types - - - - - -

    -
    -
    -

    - Supabase APIs are generated from your database, which means that we can use database - introspection to generate type-safe API definitions. -

    -

    - You can generate types from your database either through the{' '} - Supabase CLI, - or by downloading the types file via the button on the right and importing it in your - application within src/index.ts. -

    -
    -
    - {selectedLang === 'js' && ( - - )} - - -
    -
    +

    GraphQL vs Supabase @@ -141,22 +69,6 @@ const Introduction: FC = ({ selectedLang }) => { } const localSnippets = { - cliLogin: () => ({ - title: 'Login via the CLI with your Personal Access Token', - bash: { - code: ` -npx supabase login -`, - }, - }), - generateTypes: (ref: string) => ({ - title: 'Generate types', - bash: { - code: ` -npx supabase gen types typescript --project-id "${ref}" --schema public > types/supabase.ts -`, - }, - }), withApollo: () => ({ title: 'With Apollo GraphQL', bash: { diff --git a/studio/components/to-be-cleaned/Docs/Param.js b/studio/components/to-be-cleaned/Docs/Param.js deleted file mode 100644 index 9abb36712f..0000000000 --- a/studio/components/to-be-cleaned/Docs/Param.js +++ /dev/null @@ -1,35 +0,0 @@ -import { Badge } from 'ui' -import Description from './Description' - -const Param = ({ - name, - type, - format, - required, - description, - metadata = {}, - onDesciptionUpdated = () => {}, -}) => { - return ( - <> -
    - {name} - {required ? 'Required' : 'Optional'} -
    - {format && format != type && ( -
    - - {format} - {type} -
    - )} - {description !== false && ( - <> - - - - )} - - ) -} -export default Param diff --git a/studio/components/to-be-cleaned/Docs/Param.tsx b/studio/components/to-be-cleaned/Docs/Param.tsx new file mode 100644 index 0000000000..611a61e707 --- /dev/null +++ b/studio/components/to-be-cleaned/Docs/Param.tsx @@ -0,0 +1,97 @@ +import { FC } from 'react' +import { Badge, IconCode, IconDatabase } from 'ui' +import Description from './Description' + +function getColumnType(type: string, format: string) { + // json and jsonb both have type=undefined, so check format instead + if (type === undefined && (format === 'jsonb' || format === 'json')) return 'json' + + switch (type) { + case 'string': + return 'string' + case 'integer': + return 'number' + case 'json': + return 'json' + case 'number': + return 'number' + case 'boolean': + return 'boolean' + default: + return '' + } +} + +interface Props { + name: string + type: string + format: string + required: boolean + description: boolean + metadata?: any + onDesciptionUpdated?: () => void +} +const Param: FC = ({ + name, + type, + format, + required, + description, + metadata = {}, + onDesciptionUpdated = () => {}, +}) => { + return ( + <> +
    +
    +
    + + +
    + {name} +
    +
    +
    + + {required ? 'Required' : 'Optional'} +
    + {format && ( +
    +
    + +
    + + + + {getColumnType(type, format)} + + +
    +
    +
    + +
    + + + + {format} + + +
    +
    +
    + )} + {description !== false && ( +
    + + +
    + )} + + ) +} +export default Param diff --git a/studio/components/to-be-cleaned/Storage/StorageExplorer/ColumnContextMenu.js b/studio/components/to-be-cleaned/Storage/StorageExplorer/ColumnContextMenu.js deleted file mode 100644 index 45d5cb5ba0..0000000000 --- a/studio/components/to-be-cleaned/Storage/StorageExplorer/ColumnContextMenu.js +++ /dev/null @@ -1,49 +0,0 @@ -import { Menu, Item, Separator, Submenu } from 'react-contexify' -import 'react-contexify/dist/ReactContexify.css' - -import { STORAGE_VIEWS, STORAGE_SORT_BY, STORAGE_SORT_BY_ORDER } from '../Storage.constants' - -const ColumnContextMenu = ({ - id = '', - onCreateNewFolder = () => {}, - onSelectAllItems = () => {}, - onSelectView = () => {}, - onSelectSort = () => {}, - onSelectSortByOrder = () => {}, -}) => { - return ( - - { - onCreateNewFolder(props.index) - }} - > - New folder - - - { - onSelectAllItems(props.index) - }} - > - Select all items - - - onSelectView(STORAGE_VIEWS.COLUMNS)}>As columns - onSelectView(STORAGE_VIEWS.LIST)}>As list - - - onSelectSort(STORAGE_SORT_BY.NAME)}>Name - onSelectSort(STORAGE_SORT_BY.CREATED_AT)}>Last created - onSelectSort(STORAGE_SORT_BY.UPDATED_AT)}>Last modified - onSelectSort(STORAGE_SORT_BY.LAST_ACCESSED_AT)}>Last accessed - - - onSelectSortByOrder(STORAGE_SORT_BY_ORDER.ASC)}>Ascending - onSelectSortByOrder(STORAGE_SORT_BY_ORDER.DESC)}>Descending - - - ) -} - -export default ColumnContextMenu diff --git a/studio/components/to-be-cleaned/Storage/StorageExplorer/ColumnContextMenu.tsx b/studio/components/to-be-cleaned/Storage/StorageExplorer/ColumnContextMenu.tsx new file mode 100644 index 0000000000..4f13edbf96 --- /dev/null +++ b/studio/components/to-be-cleaned/Storage/StorageExplorer/ColumnContextMenu.tsx @@ -0,0 +1,121 @@ +import { FC } from 'react' +import { compact, uniqBy } from 'lodash' +import { Menu, Item, Separator, Submenu } from 'react-contexify' +import 'react-contexify/dist/ReactContexify.css' + +import { + STORAGE_VIEWS, + STORAGE_SORT_BY, + STORAGE_SORT_BY_ORDER, + STORAGE_ROW_TYPES, +} from '../Storage.constants' +import { useStorageStore } from 'localStores/storageExplorer/StorageExplorerStore' +import { IconFolderPlus } from 'ui' + +interface Props { + id: string +} + +const ColumnContextMenu: FC = ({ id = '' }) => { + const storageExplorerStore = useStorageStore() + const { + columns, + selectedItems, + setSelectedItems, + setView, + setSortBy, + setSortByOrder, + addNewFolderPlaceholder, + } = storageExplorerStore + + const onSelectCreateFolder = (columnIndex = -1) => { + addNewFolderPlaceholder(columnIndex) + } + + const onSelectAllItemsInColumn = (columnIndex: number) => { + const columnFiles = columns[columnIndex].items + .filter((item: any) => item.type === STORAGE_ROW_TYPES.FILE) + .map((item: any) => { + return { ...item, columnIndex } + }) + const columnFilesId = compact(columnFiles.map((item: any) => item.id)) + const selectedItemsFromColumn = selectedItems.filter((item: any) => + columnFilesId.includes(item.id) + ) + + if (selectedItemsFromColumn.length === columnFiles.length) { + // Deselect all items from column + const updatedSelectedItems = selectedItems.filter( + (item: any) => !columnFilesId.includes(item.id) + ) + setSelectedItems(updatedSelectedItems) + } else { + // Select all items from column + const updatedSelectedItems = uniqBy(selectedItems.concat(columnFiles), 'id') + setSelectedItems(updatedSelectedItems) + } + } + + return ( + + onSelectCreateFolder(props.index)}> + + New folder + + + onSelectAllItemsInColumn(props.index)}> + Select all items + + + View +

    + } + > + setView(STORAGE_VIEWS.COLUMNS)}> + As columns + + setView(STORAGE_VIEWS.LIST)}> + As list + + + + Sort by +
    + } + > + setSortBy(STORAGE_SORT_BY.NAME)}> + Name + + setSortBy(STORAGE_SORT_BY.CREATED_AT)}> + Last created + + setSortBy(STORAGE_SORT_BY.UPDATED_AT)}> + Last modified + + setSortBy(STORAGE_SORT_BY.LAST_ACCESSED_AT)}> + Last accessed + + + + Sort by order +
    + } + > + setSortByOrder(STORAGE_SORT_BY_ORDER.ASC)}> + Ascending + + setSortByOrder(STORAGE_SORT_BY_ORDER.DESC)}> + Descending + + + + ) +} + +export default ColumnContextMenu diff --git a/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorer.js b/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorer.js index 69e9a354f0..2b41ffda51 100644 --- a/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorer.js +++ b/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorer.js @@ -1,4 +1,4 @@ -import React, { useEffect, useRef } from 'react' +import { useEffect, useRef } from 'react' import { observer } from 'mobx-react-lite' import { STORAGE_VIEWS, CONTEXT_MENU_KEYS } from '../Storage.constants' @@ -6,7 +6,6 @@ import ItemContextMenu from './ItemContextMenu' import FolderContextMenu from './FolderContextMenu' import ColumnContextMenu from './ColumnContextMenu' import FileExplorerColumn from './FileExplorerColumn' -import { useStorageStore } from 'localStores/storageExplorer/StorageExplorerStore' const FileExplorer = ({ view = STORAGE_VIEWS.COLUMNS, @@ -17,16 +16,9 @@ const FileExplorer = ({ onFilesUpload = () => {}, onSelectAllItemsInColumn = () => {}, onSelectColumnEmptySpace = () => {}, - onSelectCreateFolder = () => {}, - onChangeView = () => {}, - onChangeSortBy = () => {}, - onChangeSortByOrder = () => {}, onColumnLoadMore = () => {}, }) => { const fileExplorerRef = useRef(null) - const storageExplorerStore = useStorageStore() - - const { setSelectedItemToRename, setSelectedItemsToDelete } = storageExplorerStore useEffect(() => { if (fileExplorerRef) { @@ -42,20 +34,9 @@ const FileExplorer = ({ ref={fileExplorerRef} className="file-explorer flex flex-grow overflow-x-auto justify-between h-full w-full" > - + - setSelectedItemToRename(folder)} - onDeleteFolder={(folder) => setSelectedItemsToDelete([folder])} - /> + {view === STORAGE_VIEWS.COLUMNS ? (
    {columns.map((column, index) => ( diff --git a/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorerHeader.tsx b/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorerHeader.tsx index e4ab9f3e40..d3b10ab510 100644 --- a/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorerHeader.tsx +++ b/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorerHeader.tsx @@ -18,6 +18,10 @@ import { IconChevronsUp, IconList, } from 'ui' +import * as Tooltip from '@radix-ui/react-tooltip' +import { PermissionAction } from '@supabase/shared-types/out/constants' + +import { checkPermissions } from 'hooks' import { useStorageStore } from 'localStores/storageExplorer/StorageExplorerStore' import { STORAGE_VIEWS, STORAGE_SORT_BY, STORAGE_SORT_BY_ORDER } from '../Storage.constants' @@ -137,6 +141,7 @@ const FileExplorerHeader: FC = ({ const breadcrumbs = columns.map((column: any) => column.name) const backDisabled = columns.length <= 1 + const canUpdateStorage = checkPermissions(PermissionAction.STORAGE_ADMIN_WRITE, '*') useEffect(() => { if (itemSearchString) setSearchString(itemSearchString) @@ -392,22 +397,60 @@ const FileExplorerHeader: FC = ({ {/* @ts-ignore */}
    - - + + + + + {!canUpdateStorage && ( + + +
    + + You need additional permissions to upload files + +
    +
    + )} +
    + + + + + {!canUpdateStorage && ( + + +
    + + You need additional permissions to create folders + +
    +
    + )} +
    {/* Search: Disabled for now */} diff --git a/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorerRow.tsx b/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorerRow.tsx index b5bd5982c2..7eaf41c543 100644 --- a/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorerRow.tsx +++ b/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorerRow.tsx @@ -20,6 +20,9 @@ import { import SVG from 'react-inlinesvg' import * as Tooltip from '@radix-ui/react-tooltip' import { useContextMenu } from 'react-contexify' +import { PermissionAction } from '@supabase/shared-types/out/constants' + +import { checkPermissions } from 'hooks' import { STORAGE_VIEWS, STORAGE_ROW_TYPES, @@ -68,6 +71,7 @@ const RowIcon = ({ view, status, fileType, mimeType }: any) => { } interface Props { + index: number item: any view: string columnIndex: number @@ -77,6 +81,7 @@ interface Props { } const FileExplorerRow: FC = ({ + index: itemIndex, item = {}, view = STORAGE_VIEWS.COLUMNS, columnIndex = 0, @@ -95,6 +100,7 @@ const FileExplorerRow: FC = ({ addNewFolder, renameFolder, renameFile, + selectedBucket, setSelectedItems, setSelectedItemsToDelete, setSelectedItemToRename, @@ -104,13 +110,16 @@ const FileExplorerRow: FC = ({ downloadFile, downloadFolder, copyFileURLToClipboard, + selectRangeItems, } = storageExplorerStore + const isPublic = selectedBucket.public const itemWithColumnIndex = { ...item, columnIndex } const isSelected = find(selectedItems, item) !== undefined const isOpened = openedFolders.length > columnIndex ? isEqual(openedFolders[columnIndex], item) : false const isPreviewed = !isEmpty(selectedFilePreview) && isEqual(selectedFilePreview.id, item.id) + const canUpdateFiles = checkPermissions(PermissionAction.STORAGE_ADMIN_WRITE, '*') const onSelectFile = async (columnIndex: number, file: any) => { popColumnAtIndex(columnIndex) @@ -127,11 +136,18 @@ const FileExplorerRow: FC = ({ await fetchFolderContents(folder.id, folder.name, columnIndex) } - const onCheckItem = (item: any) => { - if (find(selectedItems, item) === undefined) { - setSelectedItems(selectedItems.concat([item])) + const onCheckItem = (isShiftKeyHeld: boolean) => { + // Select a range if shift is held down + if (isShiftKeyHeld && selectedItems.length !== 0) { + selectRangeItems(columnIndex, itemIndex) + return + } + if (find(selectedItems, (item: any) => itemWithColumnIndex.id === item.id) !== undefined) { + setSelectedItems( + selectedItems.filter((selectedItem: any) => itemWithColumnIndex.id !== selectedItem.id) + ) } else { - setSelectedItems(selectedItems.filter((selectedItem: any) => item.id !== selectedItem.id)) + setSelectedItems([...selectedItems, itemWithColumnIndex]) } closeFilePreview() } @@ -187,77 +203,109 @@ const FileExplorerRow: FC = ({ const rowOptions = item.type === STORAGE_ROW_TYPES.FOLDER ? [ - { - name: 'Rename', - icon: , - onClick: () => setSelectedItemToRename(itemWithColumnIndex), - }, - { - name: 'Download', - icon: , - onClick: () => downloadFolder(itemWithColumnIndex), - }, - { name: 'Separator', icon: undefined, onClick: undefined }, - { - name: 'Delete', - icon: , - onClick: () => setSelectedItemsToDelete([itemWithColumnIndex]), - }, - ] - : [ - ...(!item.isCorrupted + ...(canUpdateFiles ? [ - { - name: 'Get URL', - icon: , - children: [ - { - name: 'Expire in 1 week', - onClick: async () => - await copyFileURLToClipboard(itemWithColumnIndex, URL_EXPIRY_DURATION.WEEK), - }, - { - name: 'Expire in 1 month', - onClick: async () => - await copyFileURLToClipboard( - itemWithColumnIndex, - URL_EXPIRY_DURATION.MONTH - ), - }, - { - name: 'Expire in 1 year', - onClick: async () => - await copyFileURLToClipboard(itemWithColumnIndex, URL_EXPIRY_DURATION.YEAR), - }, - { - name: 'Custom expiry', - onClick: async () => setSelectedFileCustomExpiry(itemWithColumnIndex), - }, - ], - }, { name: 'Rename', icon: , onClick: () => setSelectedItemToRename(itemWithColumnIndex), }, - { - name: 'Move', - icon: , - onClick: () => setSelectedItemsToMove([itemWithColumnIndex]), - }, - { - name: 'Download', - icon: , - onClick: async () => await downloadFile(itemWithColumnIndex), - }, - { name: 'Separator', icon: undefined, onClick: undefined }, ] : []), { - name: 'Delete', - icon: , - onClick: () => setSelectedItemsToDelete([itemWithColumnIndex]), + name: 'Download', + icon: , + onClick: () => downloadFolder(itemWithColumnIndex), }, + ...(canUpdateFiles + ? [ + { name: 'Separator', icon: undefined, onClick: undefined }, + { + name: 'Delete', + icon: , + onClick: () => setSelectedItemsToDelete([itemWithColumnIndex]), + }, + ] + : []), + ] + : [ + ...(!item.isCorrupted + ? [ + ...(isPublic + ? [ + { + name: 'Get URL', + icon: , + onClick: async () => await copyFileURLToClipboard(itemWithColumnIndex), + }, + ] + : [ + { + name: 'Get URL', + icon: , + children: [ + { + name: 'Expire in 1 week', + onClick: async () => + await copyFileURLToClipboard( + itemWithColumnIndex, + URL_EXPIRY_DURATION.WEEK + ), + }, + { + name: 'Expire in 1 month', + onClick: async () => + await copyFileURLToClipboard( + itemWithColumnIndex, + URL_EXPIRY_DURATION.MONTH + ), + }, + { + name: 'Expire in 1 year', + onClick: async () => + await copyFileURLToClipboard( + itemWithColumnIndex, + URL_EXPIRY_DURATION.YEAR + ), + }, + { + name: 'Custom expiry', + onClick: async () => setSelectedFileCustomExpiry(itemWithColumnIndex), + }, + ], + }, + ]), + ...(canUpdateFiles + ? [ + { + name: 'Rename', + icon: , + onClick: () => setSelectedItemToRename(itemWithColumnIndex), + }, + { + name: 'Move', + icon: , + onClick: () => setSelectedItemsToMove([itemWithColumnIndex]), + }, + { + name: 'Download', + icon: , + onClick: async () => await downloadFile(itemWithColumnIndex), + }, + { name: 'Separator', icon: undefined, onClick: undefined }, + ] + : []), + ] + : []), + ...(canUpdateFiles + ? [ + { + name: 'Delete', + icon: , + onClick: () => setSelectedItemsToDelete([itemWithColumnIndex]), + }, + ] + : []), ] const size = item.metadata ? formatBytes(item.metadata.size) : '-' @@ -343,7 +391,7 @@ const FileExplorerRow: FC = ({ checked={isSelected} onChange={(event) => { event.stopPropagation() - onCheckItem(itemWithColumnIndex) + onCheckItem((event.nativeEvent as KeyboardEvent).shiftKey) }} /> diff --git a/studio/components/to-be-cleaned/Storage/StorageExplorer/FolderContextMenu.js b/studio/components/to-be-cleaned/Storage/StorageExplorer/FolderContextMenu.js deleted file mode 100644 index 15ecfd3e31..0000000000 --- a/studio/components/to-be-cleaned/Storage/StorageExplorer/FolderContextMenu.js +++ /dev/null @@ -1,29 +0,0 @@ -import { useStorageStore } from 'localStores/storageExplorer/StorageExplorerStore' -import { Menu, Item, Separator } from 'react-contexify' -import 'react-contexify/dist/ReactContexify.css' -import { IconEdit, IconDownload, IconTrash2 } from 'ui' - -const FolderContextMenu = ({ id = '', onRenameFolder = () => {}, onDeleteFolder = () => {} }) => { - const storageExplorerStore = useStorageStore() - const { downloadFolder } = storageExplorerStore - - return ( - - onRenameFolder(props.item)}> - - Rename - - downloadFolder(props.item)}> - - Download - - - onDeleteFolder(props.item)}> - - Delete - - - ) -} - -export default FolderContextMenu diff --git a/studio/components/to-be-cleaned/Storage/StorageExplorer/FolderContextMenu.tsx b/studio/components/to-be-cleaned/Storage/StorageExplorer/FolderContextMenu.tsx new file mode 100644 index 0000000000..aedfa8e5c5 --- /dev/null +++ b/studio/components/to-be-cleaned/Storage/StorageExplorer/FolderContextMenu.tsx @@ -0,0 +1,44 @@ +import { FC } from 'react' +import { Menu, Item, Separator } from 'react-contexify' +import 'react-contexify/dist/ReactContexify.css' +import { IconEdit, IconDownload, IconTrash2 } from 'ui' +import { PermissionAction } from '@supabase/shared-types/out/constants' + +import { checkPermissions } from 'hooks' +import { useStorageStore } from 'localStores/storageExplorer/StorageExplorerStore' + +interface Props { + id: string +} + +const FolderContextMenu: FC = ({ id = '' }) => { + const storageExplorerStore = useStorageStore() + const { downloadFolder, setSelectedItemToRename, setSelectedItemsToDelete } = storageExplorerStore + const canUpdateFiles = checkPermissions(PermissionAction.STORAGE_ADMIN_WRITE, '*') + + return ( + + {canUpdateFiles && ( + setSelectedItemToRename(props.item)}> + + Rename + + )} + downloadFolder(props.item)}> + + Download + + {canUpdateFiles && ( + <> + + setSelectedItemsToDelete([props.item])}> + + Delete + + + )} + + ) +} + +export default FolderContextMenu diff --git a/studio/components/to-be-cleaned/Storage/StorageExplorer/ItemContextMenu.js b/studio/components/to-be-cleaned/Storage/StorageExplorer/ItemContextMenu.tsx similarity index 66% rename from studio/components/to-be-cleaned/Storage/StorageExplorer/ItemContextMenu.js rename to studio/components/to-be-cleaned/Storage/StorageExplorer/ItemContextMenu.tsx index eb48047e27..e42be48fe5 100644 --- a/studio/components/to-be-cleaned/Storage/StorageExplorer/ItemContextMenu.js +++ b/studio/components/to-be-cleaned/Storage/StorageExplorer/ItemContextMenu.tsx @@ -1,12 +1,19 @@ +import { FC } from 'react' import { observer } from 'mobx-react-lite' import { Menu, Item, Separator, Submenu } from 'react-contexify' import 'react-contexify/dist/ReactContexify.css' +import { PermissionAction } from '@supabase/shared-types/out/constants' import { IconClipboard, IconEdit, IconMove, IconDownload, IconTrash2, IconChevronRight } from 'ui' +import { checkPermissions } from 'hooks' import { URL_EXPIRY_DURATION } from '../Storage.constants' import { useStorageStore } from 'localStores/storageExplorer/StorageExplorerStore' -const ItemContextMenu = ({ id = '' }) => { +interface Props { + id: string +} + +const ItemContextMenu: FC = ({ id = '' }) => { const storageExplorerStore = useStorageStore() const { downloadFile, @@ -18,12 +25,13 @@ const ItemContextMenu = ({ id = '' }) => { copyFileURLToClipboard, } = storageExplorerStore const isPublic = selectedBucket.public + const canUpdateFiles = checkPermissions(PermissionAction.STORAGE_ADMIN_WRITE, '*') - const onHandleClick = async (event, item, expiresIn) => { + const onHandleClick = async (event: any, item: any, expiresIn?: number) => { if (item.isCorrupted) return switch (event) { case 'copy': - if (expiresIn < 0) return setSelectedFileCustomExpiry(item) + if (expiresIn !== undefined && expiresIn < 0) return setSelectedFileCustomExpiry(item) else return await copyFileURLToClipboard(item, expiresIn) case 'rename': return setSelectedItemToRename(item) @@ -73,23 +81,27 @@ const ItemContextMenu = ({ id = '' }) => { )} - onHandleClick('rename', props.item)}> - - Rename - - onHandleClick('move', props.item)}> - - Move - - onHandleClick('download', props.item)}> - - Download - - - setSelectedItemsToDelete([props.item])}> - - Delete - + {canUpdateFiles && ( + <> + onHandleClick('rename', props.item)}> + + Rename + + onHandleClick('move', props.item)}> + + Move + + onHandleClick('download', props.item)}> + + Download + + + setSelectedItemsToDelete([props.item])}> + + Delete + + + )} ) } diff --git a/studio/components/to-be-cleaned/Storage/StorageExplorer/MoveItemsModal.js b/studio/components/to-be-cleaned/Storage/StorageExplorer/MoveItemsModal.tsx similarity index 83% rename from studio/components/to-be-cleaned/Storage/StorageExplorer/MoveItemsModal.js rename to studio/components/to-be-cleaned/Storage/StorageExplorer/MoveItemsModal.tsx index 0f85cf1fae..709b4c5bca 100644 --- a/studio/components/to-be-cleaned/Storage/StorageExplorer/MoveItemsModal.js +++ b/studio/components/to-be-cleaned/Storage/StorageExplorer/MoveItemsModal.tsx @@ -1,7 +1,15 @@ -import { useEffect, useState } from 'react' -import { Modal, Button, Input, Space } from 'ui' +import { FC, useEffect, useState } from 'react' +import { Modal, Button, Input } from 'ui' -const MoveItemsModal = ({ +interface Props { + bucketName: string + visible: boolean + selectedItemsToMove: any[] + onSelectCancel: () => void + onSelectMove: (path: string) => void +} + +const MoveItemsModal: FC = ({ bucketName = '', visible = false, selectedItemsToMove = [], @@ -21,14 +29,14 @@ const MoveItemsModal = ({ const title = multipleFiles ? `Moving ${selectedItemsToMove.length} items within ${bucketName}` : selectedItemsToMove.length === 1 - ? `Moving ${selectedItemsToMove[0].name} within ${bucketName}` + ? `Moving ${selectedItemsToMove[0]?.name} within ${bucketName}` : `` const description = `Enter the path to where you'd like to move the file${ multipleFiles ? 's' : '' } to.` - const onConfirmMove = (event) => { + const onConfirmMove = (event: any) => { if (event) { event.preventDefault() } diff --git a/studio/components/to-be-cleaned/Storage/StorageExplorer/PreviewPane.tsx b/studio/components/to-be-cleaned/Storage/StorageExplorer/PreviewPane.tsx index 205e6fbbba..2ba600a6f8 100644 --- a/studio/components/to-be-cleaned/Storage/StorageExplorer/PreviewPane.tsx +++ b/studio/components/to-be-cleaned/Storage/StorageExplorer/PreviewPane.tsx @@ -1,4 +1,3 @@ -import Link from 'next/link' import { isEmpty } from 'lodash' import { Button, @@ -14,6 +13,10 @@ import { import SVG from 'react-inlinesvg' import { formatBytes } from 'lib/helpers' import { Transition } from '@headlessui/react' +import * as Tooltip from '@radix-ui/react-tooltip' +import { PermissionAction } from '@supabase/shared-types/out/constants' + +import { checkPermissions } from 'hooks' import { URL_EXPIRY_DURATION } from '../Storage.constants' import { useStorageStore } from 'localStores/storageExplorer/StorageExplorerStore' @@ -93,6 +96,7 @@ const PreviewFile = ({ mimeType, previewUrl }: { mimeType: string; previewUrl: s const PreviewPane = () => { const storageExplorerStore = useStorageStore() const { + downloadFile, selectedBucket, selectedFilePreview: file, copyFileURLToClipboard, @@ -107,6 +111,7 @@ const PreviewPane = () => { const mimeType = file.metadata ? file.metadata.mimetype : null const createdAt = file.created_at ? new Date(file.created_at).toLocaleString() : 'Unknown' const updatedAt = file.updated_at ? new Date(file.updated_at).toLocaleString() : 'Unknown' + const canUpdateFiles = checkPermissions(PermissionAction.STORAGE_ADMIN_WRITE, '*') return ( <> @@ -176,17 +181,14 @@ const PreviewPane = () => { {/* Actions */}
    - - - - - + {selectedBucket.public ? (
    - + + + + + {!canUpdateFiles && ( + + +
    + + You need additional permissions to delete this file + +
    +
    + )} +
    diff --git a/studio/components/to-be-cleaned/Storage/StorageExplorer/StorageExplorer.js b/studio/components/to-be-cleaned/Storage/StorageExplorer/StorageExplorer.js index b19374ff0b..b31a2d8f79 100644 --- a/studio/components/to-be-cleaned/Storage/StorageExplorer/StorageExplorer.js +++ b/studio/components/to-be-cleaned/Storage/StorageExplorer/StorageExplorer.js @@ -30,14 +30,10 @@ const StorageExplorer = observer(({ bucket }) => { selectedItemsToMove, clearSelectedItemsToMove, view, - setView, - setSortBy, - setSortByOrder, currentBucketName, openBucket, loadExplorerPreferences, - addNewFolderPlaceholder, fetchFolderContents, fetchMoreFolderContents, deleteFolder, @@ -116,10 +112,6 @@ const StorageExplorer = observer(({ bucket }) => { /** File manipulation methods */ - const onSelectCreateFolder = (columnIndex = -1) => { - addNewFolderPlaceholder(columnIndex) - } - const onFilesUpload = async (event, columnIndex = -1) => { event.persist() const items = event.target.files || event.dataTransfer.items @@ -158,12 +150,6 @@ const StorageExplorer = observer(({ bucket }) => { clearSelectedItems() } - const onChangeView = (view) => setView(view) - - const onChangeSortBy = (sortBy) => setSortBy(sortBy) - - const onChangeSortByOrder = (sortByOrder) => setSortByOrder(sortByOrder) - return (
    { onFilesUpload={onFilesUpload} onSelectAllItemsInColumn={onSelectAllItemsInColumn} onSelectColumnEmptySpace={onSelectColumnEmptySpace} - onSelectCreateFolder={onSelectCreateFolder} - onChangeView={onChangeView} - onChangeSortBy={onChangeSortBy} - onChangeSortByOrder={onChangeSortByOrder} onColumnLoadMore={(index, column) => fetchMoreFolderContents(index, column, itemSearchString) } diff --git a/studio/components/to-be-cleaned/Storage/StorageSettings/StorageSettings.tsx b/studio/components/to-be-cleaned/Storage/StorageSettings/StorageSettings.tsx index 4e05953129..a604849edf 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 +

    + ) : ( +
    + )}
    diff --git a/studio/components/to-be-cleaned/forms/SchemaForm.js b/studio/components/to-be-cleaned/forms/SchemaForm.js index bc5e6ac20a..fa65c7b156 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 || ''}
    -
    - - -
    + + +
    + )} {message}
    { setHasChanged(true) if (onChangeModel) onChangeModel(model) diff --git a/studio/components/ui/Flag/FlagProvider.tsx b/studio/components/ui/Flag/FlagProvider.tsx index e521fec1a6..40d1ac7dae 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/ProductMenu/ProductMenuItem.tsx b/studio/components/ui/ProductMenu/ProductMenuItem.tsx index ca2fdcf51a..2b8dedb6ec 100644 --- a/studio/components/ui/ProductMenu/ProductMenuItem.tsx +++ b/studio/components/ui/ProductMenu/ProductMenuItem.tsx @@ -16,6 +16,32 @@ interface Props { label?: string } +const Label = ({ label }: { label: string }) => { + const color = + label.toLowerCase() === 'new' ? 'text-brand-900 bg-brand-500' : 'text-amber-900 bg-amber-500' + + return ( + + {label} + + ) +} + const ProductMenuItem: FC = ({ name = '', isActive, @@ -34,7 +60,7 @@ const ProductMenuItem: FC = ({
    {name}{' '} {label !== undefined && ( diff --git a/studio/components/ui/UpgradeToPro.tsx b/studio/components/ui/UpgradeToPro.tsx index ca7b1b549b..ccee10b129 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}

    - - - - - + + {!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/auth/keys.ts b/studio/data/auth/keys.ts new file mode 100644 index 0000000000..052713f52f --- /dev/null +++ b/studio/data/auth/keys.ts @@ -0,0 +1,3 @@ +export const authKeys = { + accessToken: () => ['access-token'] as const, +} diff --git a/studio/data/auth/session-access-token-query.ts b/studio/data/auth/session-access-token-query.ts new file mode 100644 index 0000000000..46068987ff --- /dev/null +++ b/studio/data/auth/session-access-token-query.ts @@ -0,0 +1,35 @@ +import { useCallback } from 'react' +import { useQuery, useQueryClient, UseQueryOptions } from '@tanstack/react-query' + +import { authKeys } from './keys' +import { auth } from 'lib/gotrue' + +export async function getSessionAccessToken() { + // ignore if server-side + if (typeof window === 'undefined') return '' + const { + data: { session }, + } = await auth.getSession() + return session?.access_token +} + +export type SessionAccessTokenData = Awaited> +export type SessionAccessTokenError = unknown + +export const useSessionAccessTokenQuery = ({ + enabled = true, + ...options +}: UseQueryOptions = {}) => + useQuery( + authKeys.accessToken(), + () => getSessionAccessToken(), + options + ) + +export const useSessionAccessTokenPrefetch = () => { + const client = useQueryClient() + return useCallback( + () => client.prefetchQuery(authKeys.accessToken(), () => getSessionAccessToken()), + [] + ) +} diff --git a/studio/data/config/project-read-only-query.ts b/studio/data/config/project-read-only-query.ts new file mode 100644 index 0000000000..23312f2f67 --- /dev/null +++ b/studio/data/config/project-read-only-query.ts @@ -0,0 +1,56 @@ +import { UseQueryOptions } from '@tanstack/react-query' +import { ExecuteSqlData, useExecuteSqlPrefetch, useExecuteSqlQuery } from '../sql/execute-sql-query' + +// TODO: temporary solution to check if project is in read only mode +// until we get an api endpoint for this + +export const getProjectReadOnlySql = () => { + const sql = /* SQL */ ` + show default_transaction_read_only; + ` + + return sql +} + +export type ProjectReadOnlyVariables = { + projectRef?: string + connectionString?: string +} + +export type ProjectReadOnlyData = boolean +export type ProjectReadOnlyError = unknown + +export const useProjectReadOnlyQuery = ( + { projectRef, connectionString }: ProjectReadOnlyVariables, + options: Omit< + UseQueryOptions, + 'select' + > = {} +) => + useExecuteSqlQuery( + { + projectRef, + connectionString, + sql: getProjectReadOnlySql(), + queryKey: ['project-read-only'], + }, + { + select(data) { + return data.result[0]?.default_transaction_read_only === 'on' + }, + enabled: typeof projectRef !== 'undefined' && typeof connectionString !== 'undefined', + ...options, + } + ) + +export const useProjectReadOnlyPrefetch = ({ + projectRef, + connectionString, +}: ProjectReadOnlyVariables) => { + return useExecuteSqlPrefetch({ + projectRef, + connectionString, + sql: getProjectReadOnlySql(), + queryKey: ['project-read-only'], + }) +} diff --git a/studio/data/docs/keys.ts b/studio/data/docs/keys.ts index b0f6636294..266c151231 100644 --- a/studio/data/docs/keys.ts +++ b/studio/data/docs/keys.ts @@ -1,7 +1,3 @@ export const docsKeys = { - jsonSchema: ( - projectRef: string | undefined, - swaggerUrl: string | undefined, - apiKey: string | undefined - ) => ['projects', projectRef, 'docs', { swaggerUrl, apiKey }] as const, + jsonSchema: (projectRef: string | undefined) => ['projects', projectRef, 'docs'] as const, } diff --git a/studio/data/docs/project-json-schema-query.ts b/studio/data/docs/project-json-schema-query.ts index f37f6dd55c..1155f7c14a 100644 --- a/studio/data/docs/project-json-schema-query.ts +++ b/studio/data/docs/project-json-schema-query.ts @@ -1,41 +1,26 @@ import { useQuery, useQueryClient, UseQueryOptions } from '@tanstack/react-query' import { get } from 'lib/common/fetch' +import { API_URL } from 'lib/constants' import { useCallback } from 'react' import { docsKeys } from './keys' export type ProjectJsonSchemaVariables = { projectRef?: string - swaggerUrl?: string - apiKey?: string } export type ProjectJsonSchemaResponse = any export async function getProjectJsonSchema( - { projectRef, swaggerUrl, apiKey }: ProjectJsonSchemaVariables, + { projectRef }: ProjectJsonSchemaVariables, signal?: AbortSignal ) { if (!projectRef) { throw new Error('projectRef is required') } - if (!swaggerUrl) { - throw new Error('id is required') - } - - let headers: { - [key: string]: string | undefined - } = { apikey: apiKey } - if ((apiKey?.length ?? 0) > 40) headers['Authorization'] = `Bearer ${apiKey}` - - const response = await get(swaggerUrl, { - signal, - headers, - credentials: 'omit', - }) - if (response.error) { - throw response.error - } + const url = `${API_URL}/projects/${projectRef}/api/rest` + const response = await get(url, { signal }) + if (response.error) throw response.error return response as ProjectJsonSchemaResponse } @@ -43,33 +28,29 @@ export type ProjectJsonSchemaData = Awaited( - { projectRef, swaggerUrl, apiKey }: ProjectJsonSchemaVariables, + { projectRef }: ProjectJsonSchemaVariables, { enabled = true, ...options }: UseQueryOptions = {} ) => useQuery( - docsKeys.jsonSchema(projectRef, swaggerUrl, apiKey), - ({ signal }) => getProjectJsonSchema({ projectRef, swaggerUrl, apiKey }, signal), + docsKeys.jsonSchema(projectRef), + ({ signal }) => getProjectJsonSchema({ projectRef }, signal), { - enabled: enabled && typeof projectRef !== 'undefined' && typeof swaggerUrl !== 'undefined', + enabled: enabled && typeof projectRef !== 'undefined', ...options, } ) -export const useProjectJsonSchemaPrefetch = ({ - projectRef, - swaggerUrl, - apiKey, -}: ProjectJsonSchemaVariables) => { +export const useProjectJsonSchemaPrefetch = ({ projectRef }: ProjectJsonSchemaVariables) => { const client = useQueryClient() return useCallback(() => { - if (projectRef && swaggerUrl) { - client.prefetchQuery(docsKeys.jsonSchema(projectRef, swaggerUrl, apiKey), ({ signal }) => - getProjectJsonSchema({ projectRef, swaggerUrl, apiKey }, signal) + if (projectRef) { + client.prefetchQuery(docsKeys.jsonSchema(projectRef), ({ signal }) => + getProjectJsonSchema({ projectRef }, signal) ) } - }, [projectRef, swaggerUrl]) + }, [projectRef]) } 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 0000000000..d642758cd9 --- /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 0000000000..28bc3f8c64 --- /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 0000000000..dacd8d4a8d --- /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/data/permissions/permissions-query.ts b/studio/data/permissions/permissions-query.ts index 45d73e0d0a..0dff675037 100644 --- a/studio/data/permissions/permissions-query.ts +++ b/studio/data/permissions/permissions-query.ts @@ -21,10 +21,9 @@ export async function getPermissions(signal?: AbortSignal) { export type PermissionsData = Awaited> export type PermissionsError = unknown -export const usePermissionsQuery = ({ - enabled = true, - ...options -}: UseQueryOptions = {}) => +export const usePermissionsQuery = ( + options: UseQueryOptions = {} +) => useQuery( permissionKeys.list(), ({ signal }) => getPermissions(signal), diff --git a/studio/hooks/misc/withAuth.tsx b/studio/hooks/misc/withAuth.tsx index b41734cb1c..d2cdc5d94f 100644 --- a/studio/hooks/misc/withAuth.tsx +++ b/studio/hooks/misc/withAuth.tsx @@ -50,6 +50,7 @@ export function withAuth( }) usePermissionsQuery({ + enabled: IS_PLATFORM, onSuccess(permissions) { ui.setPermissions(permissions) }, diff --git a/studio/lib/common/fetch/base.ts b/studio/lib/common/fetch/base.ts index 45597dc358..4116526bf6 100644 --- a/studio/lib/common/fetch/base.ts +++ b/studio/lib/common/fetch/base.ts @@ -12,6 +12,9 @@ export async function handleResponse( response: Response, requestId: string ): Promise> { + const contentType = response.headers.get('Content-Type') + if (contentType === 'application/octet-stream') return response as any + try { const resTxt = await response.text() try { diff --git a/studio/lib/common/fetch/delete.ts b/studio/lib/common/fetch/delete.ts index ff5a2f3f4e..4bedf25cfa 100644 --- a/studio/lib/common/fetch/delete.ts +++ b/studio/lib/common/fetch/delete.ts @@ -14,7 +14,6 @@ export async function delete_( const response = await fetch(url, { method: 'DELETE', body: JSON.stringify(data), - credentials: 'include', referrerPolicy: 'no-referrer-when-downgrade', headers, ...otherOptions, diff --git a/studio/lib/common/fetch/get.ts b/studio/lib/common/fetch/get.ts index dc4f56a081..58b3f8946d 100644 --- a/studio/lib/common/fetch/get.ts +++ b/studio/lib/common/fetch/get.ts @@ -12,7 +12,6 @@ export async function get( const headers = await constructHeaders(requestId, optionHeaders) const response = await fetch(url, { method: 'GET', - credentials: 'include', referrerPolicy: 'no-referrer-when-downgrade', headers, ...otherOptions, @@ -37,7 +36,6 @@ export async function getWithTimeout( const headers = await constructHeaders(requestId, optionHeaders) const response = await fetch(url, { method: 'GET', - credentials: 'include', referrerPolicy: 'no-referrer-when-downgrade', headers, ...otherOptions, diff --git a/studio/lib/common/fetch/head.ts b/studio/lib/common/fetch/head.ts index 6d7e7f42d6..2bcba4895f 100644 --- a/studio/lib/common/fetch/head.ts +++ b/studio/lib/common/fetch/head.ts @@ -13,7 +13,6 @@ export async function head( const headers = await constructHeaders(requestId, optionHeaders) const response = await fetch(url, { method: 'HEAD', - credentials: 'include', referrerPolicy: 'no-referrer-when-downgrade', headers, ...otherOptions, @@ -39,7 +38,6 @@ export async function headWithTimeout( const headers = await constructHeaders(requestId, optionHeaders) const response = await fetch(url, { method: 'HEAD', - credentials: 'include', referrerPolicy: 'no-referrer-when-downgrade', headers, ...otherOptions, diff --git a/studio/lib/common/fetch/patch.ts b/studio/lib/common/fetch/patch.ts index 9ca15e0c98..7fcf4aad2c 100644 --- a/studio/lib/common/fetch/patch.ts +++ b/studio/lib/common/fetch/patch.ts @@ -14,7 +14,6 @@ export async function patch( const response = await fetch(url, { method: 'PATCH', body: JSON.stringify(data), - credentials: 'include', referrerPolicy: 'no-referrer-when-downgrade', headers, ...otherOptions, diff --git a/studio/lib/common/fetch/post.ts b/studio/lib/common/fetch/post.ts index e4757dc34a..ce29bada3c 100644 --- a/studio/lib/common/fetch/post.ts +++ b/studio/lib/common/fetch/post.ts @@ -9,15 +9,15 @@ export async function post( ): Promise> { const requestId = uuidv4() try { - const { headers: optionHeaders, ...otherOptions } = options ?? {} + const { headers: optionHeaders, abortSignal, ...otherOptions } = options ?? {} const headers = await constructHeaders(requestId, optionHeaders) const response = await fetch(url, { method: 'POST', body: JSON.stringify(data), - credentials: 'include', referrerPolicy: 'no-referrer-when-downgrade', headers, ...otherOptions, + signal: abortSignal, }) if (!response.ok) return handleResponseError(response, requestId) return handleResponse(response, requestId) diff --git a/studio/lib/pingPostgrest.ts b/studio/lib/pingPostgrest.ts index 623775327d..42919651b3 100644 --- a/studio/lib/pingPostgrest.ts +++ b/studio/lib/pingPostgrest.ts @@ -2,6 +2,8 @@ import semver from 'semver' import { headWithTimeout, getWithTimeout } from './common/fetch' import { API_URL } from './constants' +const DEFAULT_TIMEOUT_MILLISECONDS = 2000 + /** * Ping Postgrest for health check. Default timeout in 2s. * @@ -15,7 +17,6 @@ import { API_URL } from './constants' * @return true if ping is successful else false */ async function pingPostgrest( - restUrl: string, projectRef: string, options?: { kpsVersion?: string @@ -23,8 +24,6 @@ async function pingPostgrest( } ) { if (projectRef === undefined) return false - const apiKey = await getApiKey(projectRef, options?.timeout) - if (apiKey === undefined) return false const { kpsVersion, timeout } = options ?? {} const healthCheckApiEnable = semver.gte( @@ -33,35 +32,21 @@ async function pingPostgrest( semver.coerce('kps-v3.8.6') ) if (healthCheckApiEnable) { - const healthCheckUrl = `${restUrl.replace('/rest/', '/rest-admin/')}live` - return pingHealthCheckApi(healthCheckUrl, apiKey, timeout) + return pingHealthCheckApi(projectRef, timeout) + } else { + return pingOpenApi(projectRef, timeout) } - - return pingOpenApi(restUrl, apiKey, timeout) } + export default pingPostgrest -const getApiKey = async ( - projectRef: string, - timeout = DEFAULT_TIMEOUT_MILLISECONDS -): Promise => { - const response = await getWithTimeout(`${API_URL}/props/project/${projectRef}/api`, { timeout }) - if (response.error || response.autoApiService.service_api_keys.length === 0) return undefined - return response.autoApiService.defaultApiKey -} - -const DEFAULT_TIMEOUT_MILLISECONDS = 2000 - /** * Send a HEAD request to postgrest OpenAPI. * * @return true if there's no error else false */ -async function pingOpenApi(url: string, apikey: string, timeout?: number) { - const headers = { apikey, Authorization: `Bearer ${apikey}` } - const { error } = await headWithTimeout(url, [], { - headers, - credentials: 'omit', +async function pingOpenApi(ref: string, timeout?: number) { + const { error } = await headWithTimeout(`${API_URL}/projects/${ref}/api/rest`, [], { timeout: timeout ?? DEFAULT_TIMEOUT_MILLISECONDS, }) return error === undefined @@ -72,11 +57,8 @@ async function pingOpenApi(url: string, apikey: string, timeout?: number) { * * @return true if there's no error else false */ -async function pingHealthCheckApi(url: string, apikey: string, timeout?: number) { - const headers = { apikey } - const { error } = await getWithTimeout(url, { - headers, - credentials: 'omit', +async function pingHealthCheckApi(ref: string, timeout?: number) { + const { error } = await getWithTimeout(`${API_URL}/projects/${ref}/live`, { timeout: timeout ?? DEFAULT_TIMEOUT_MILLISECONDS, }) return error === undefined diff --git a/studio/localStores/storageExplorer/StorageExplorerStore.js b/studio/localStores/storageExplorer/StorageExplorerStore.js index 77d26ceda7..57533c66f9 100644 --- a/studio/localStores/storageExplorer/StorageExplorerStore.js +++ b/studio/localStores/storageExplorer/StorageExplorerStore.js @@ -1,13 +1,14 @@ import toast from 'react-hot-toast' import { createContext, useContext } from 'react' import { makeAutoObservable } from 'mobx' -import { find, compact, isEqual, isNil, has, some, chunk, get, uniq } from 'lodash' +import { find, compact, isEqual, has, some, chunk, uniq, uniqBy, findIndex } from 'lodash' import { BlobReader, BlobWriter, ZipWriter } from '@zip.js/zip.js' import { createClient } from '@supabase/supabase-js' import { useStore } from 'hooks' import { copyToClipboard } from 'lib/helpers' -import { IS_PLATFORM } from 'lib/constants' +import { API_URL, IS_PLATFORM } from 'lib/constants' +import { get, patch, post, delete_ } from 'lib/common/fetch' import { PROJECT_ENDPOINT_PROTOCOL } from 'pages/api/constants' import { STORAGE_VIEWS, @@ -71,6 +72,8 @@ class StorageExplorerStore { /* Supabase client */ supabaseClient = null + /* [Joshen] Move towards using API */ + endpoint = '' /* FE to toggle page level modals */ showCreateBucketModal = false @@ -99,7 +102,8 @@ class StorageExplorerStore { initStore(projectRef, url, serviceKey, protocol = PROJECT_ENDPOINT_PROTOCOL) { this.projectRef = projectRef - this.initializeSupabaseClient(serviceKey, url, protocol) + this.endpoint = `${API_URL}/storage/${projectRef}` + if (serviceKey !== undefined) this.initializeSupabaseClient(serviceKey, url, protocol) } /* Methods which are commonly used + For better readability */ @@ -353,9 +357,9 @@ class StorageExplorerStore { .upload(formattedPathToEmptyPlaceholderFile, new File([], EMPTY_FOLDER_PLACEHOLDER_FILE_NAME)) if (pathToFolder.length > 0) { - await this.supabaseClient.storage - .from(this.selectedBucket.name) - .remove([`${pathToFolder}/${EMPTY_FOLDER_PLACEHOLDER_FILE_NAME}`]) + await delete_(`${this.endpoint}/buckets/${this.selectedBucket.id}/objects`, { + paths: [`${pathToFolder}/${EMPTY_FOLDER_PLACEHOLDER_FILE_NAME}`], + }) } } @@ -373,8 +377,8 @@ class StorageExplorerStore { this.selectedFilePreview = { ...file, previewUrl: 'loading' } const cachedPreview = find(this.filePreviewCache, { id: file.id }) - const fetchedAt = get(cachedPreview, ['fetchedAt'], null) - const expiresIn = get(cachedPreview, ['expiresIn'], null) + const fetchedAt = cachedPreview?.fetchedAt ?? null + const expiresIn = cachedPreview?.expiresIn ?? null const existsInCache = fetchedAt !== null && expiresIn !== null const isExpired = existsInCache ? fetchedAt + expiresIn * 1000 < Date.now() : true @@ -454,28 +458,16 @@ class StorageExplorerStore { /* Bucket CRUD */ createBucket = async (bucketName, isPublic = false) => { - if (isNil(this.supabaseClient)) { - return this.ui.setNotification({ - message: 'Failed to initialize supabase client, try refreshing your browser.', - category: 'error', - }) - } - - const { data, error } = await this.supabaseClient.storage.createBucket(bucketName, { - public: isPublic, - }) - if (error) { - this.ui.setNotification({ - message: error.message, - category: 'error', - }) + const res = await post(`${this.endpoint}/buckets`, { id: bucketName, public: isPublic }) + if (res.error) { + this.ui.setNotification({ category: 'error', message: res.error.message }) this.closeCreateBucketModal() return undefined + } else { + await this.fetchBuckets() + this.closeCreateBucketModal() + return res } - - await this.fetchBuckets() - this.closeCreateBucketModal() - return data } openBucket = async (bucket) => { @@ -488,10 +480,10 @@ class StorageExplorerStore { } fetchBuckets = async () => { - const { data: buckets, error } = await this.supabaseClient.storage.listBuckets() - if (error) return this.ui.setNotification({ message: error.message, category: 'error' }) + const res = await get(`${this.endpoint}/buckets`) + if (res.error) return this.ui.setNotification({ category: 'error', message: res.error.message }) - const formattedBuckets = buckets.map((bucket) => { + const formattedBuckets = res.map((bucket) => { return { ...bucket, type: STORAGE_ROW_TYPES.BUCKET, status: STORAGE_ROW_STATUS.READY } }) this.buckets = formattedBuckets @@ -503,15 +495,15 @@ class StorageExplorerStore { // hence delete bucket and empty bucket are coupled tightly here const { id, name: bucketName } = bucket - const { error: emptyBucketError } = await this.supabaseClient.storage.emptyBucket(id) - if (emptyBucketError) { - this.ui.setNotification({ message: emptyBucketError.message, category: 'error' }) + const emptyBucketRes = await post(`${this.endpoint}/buckets/${id}/empty`, {}) + if (emptyBucketRes.error) { + this.ui.setNotification({ category: 'error', message: emptyBucketRes.error.message }) return false } - const { error: deleteBucketError } = await this.supabaseClient.storage.deleteBucket(id) - if (deleteBucketError) { - this.ui.setNotification({ message: deleteBucketError.message, category: 'error' }) + const deleteBucketRes = await delete_(`${this.endpoint}/buckets/${id}`) + if (deleteBucketRes.error) { + this.ui.setNotification({ category: 'error', message: deleteBucketRes.error.message }) return false } @@ -527,13 +519,9 @@ class StorageExplorerStore { } toggleBucketPublic = async (bucket) => { - const { name: bucketName } = bucket - - const { data, error } = await this.supabaseClient.storage.updateBucket(bucketName, { - public: !bucket.public, - }) - if (error) { - this.ui.setNotification({ message: error.message, category: 'error' }) + const res = await patch(`${this.endpoint}/buckets/${bucket.id}`, { public: !bucket.public }) + if (res.error) { + this.ui.setNotification({ category: 'error', message: res.error.message }) return this.closeToggleBucketPublicModal() } @@ -619,7 +607,7 @@ class StorageExplorerStore { // If we're uploading a folder which name already exists in the same folder that we're uploading to // We sanitize the folder name and let all file uploads through. (This is only via drag drop) - const topLevelFolders = get(this.columns, [derivedColumnIndex, 'items'], []) + const topLevelFolders = (this.columns?.[derivedColumnIndex]?.items ?? []) .filter((item) => !item.id) .map((item) => item.name) const formattedFilesToUpload = filesToUpload.map((file) => { @@ -666,7 +654,7 @@ class StorageExplorerStore { const fileOptions = { cacheControl: '3600' } const metadata = { mimetype: file.type, size: file.size } - const isWithinFolder = get(file, ['path'], '').split('/').length > 1 + const isWithinFolder = (file?.path ?? '').split('/').length > 1 const fileName = !isWithinFolder ? this.sanitizeNameForDuplicateInColumn(file.name, autofix) : file.name @@ -740,9 +728,9 @@ class StorageExplorerStore { }, Promise.resolve()) if (numberOfFilesUploadedSuccess > 0) { - await this.supabaseClient.storage - .from(this.selectedBucket.name) - .remove([`${pathToFile}/${EMPTY_FOLDER_PLACEHOLDER_FILE_NAME}`]) + await delete_(`${this.endpoint}/buckets/${this.selectedBucket.id}/objects`, { + paths: [`${pathToFile}/${EMPTY_FOLDER_PLACEHOLDER_FILE_NAME}`], + }) } await this.refetchAllOpenedFolders() @@ -810,16 +798,13 @@ class StorageExplorerStore { const toPath = newPathToFile.length > 0 ? `${formattedNewPathToFile}/${item.name}` : item.name - const { error } = await this.supabaseClient.storage - .from(this.selectedBucket.name) - .move(fromPath, toPath) - - if (error) { + const res = await post(`${this.endpoint}/buckets/${this.selectedBucket.id}/objects/move`, { + from: fromPath, + to: toPath, + }) + if (res.error) { numberOfFilesMovedFail += 1 - this.ui.setNotification({ - message: error.message, - category: 'error', - }) + this.ui.setNotification({ category: 'error', message: res.error.message }) } }) ) @@ -857,23 +842,20 @@ class StorageExplorerStore { const formattedPathToFile = pathToFile.length > 0 ? `${pathToFile}/${fileName}` : fileName if (this.selectedBucket.public) { - const { data, error } = await this.supabaseClient.storage - .from(this.selectedBucket.name) - .getPublicUrl(formattedPathToFile) - - if (!error) { - return data.publicUrl - } + const res = await post( + `${this.endpoint}/buckets/${this.selectedBucket.id}/objects/public-url`, + { path: formattedPathToFile } + ) + if (!res.error) return res.publicUrl + else console.error('Failed to fetch public file preview', res.error.message) + } else { + const res = await post(`${this.endpoint}/buckets/${this.selectedBucket.id}/objects/sign`, { + path: formattedPathToFile, + expiresIn: expiresIn || DEFAULT_EXPIRY, + }) + if (!res.error) return res.signedUrl + else console.error('Failed to fetch signed url preview', res.error.message) } - - const { data, error } = await this.supabaseClient.storage - .from(this.selectedBucket.name) - .createSignedUrl(formattedPathToFile, expiresIn || DEFAULT_EXPIRY) - - if (!error) { - return data.signedUrl - } - return null } @@ -907,7 +889,9 @@ class StorageExplorerStore { // batch BATCH_SIZE prefixes per request const batches = chunk(prefixes, BATCH_SIZE).map((batch) => () => { progress = progress + batch.length / prefixes.length - return this.supabaseClient.storage.from(this.selectedBucket.name).remove(batch) + return delete_(`${this.endpoint}/buckets/${this.selectedBucket.name}/objects`, { + paths: batch, + }) }) // make BATCH_SIZE requests at the same time @@ -973,17 +957,18 @@ class StorageExplorerStore { const fileMimeType = file.metadata?.mimetype ?? null return () => { return new Promise(async (resolve) => { - const { data, error } = await this.supabaseClient.storage - .from(this.selectedBucket.name) - .download(`${file.prefix}/${file.name}`) - + const res = await post( + `${this.endpoint}/buckets/${this.selectedBucket.id}/objects/download`, + { path: `${file.prefix}/${file.name}` } + ) progress = progress + 1 / files.length - if (!error) { + if (!res.error) { + const blob = await res.blob() resolve({ name: file.name, prefix: file.prefix, - blob: new Blob([data], { type: fileMimeType }), + blob: new Blob([blob], { type: fileMimeType }), }) } else { resolve(false) @@ -1095,7 +1080,7 @@ class StorageExplorerStore { downloadFile = async (file, showToast = true, returnBlob = false) => { const fileName = file.name - const fileMimeType = get(file, ['metadata', 'mimetype'], null) + const fileMimeType = file?.metadata?.mimetype ?? null const toastId = showToast ? this.ui.setNotification({ category: 'loading', message: `Retrieving ${fileName}...` }) @@ -1106,13 +1091,12 @@ class StorageExplorerStore { .map((folder) => folder.name) .join('/') const formattedPathToFile = pathToFile.length > 0 ? `${pathToFile}/${fileName}` : fileName + const res = await post(`${this.endpoint}/buckets/${this.selectedBucket.id}/objects/download`, { + path: formattedPathToFile, + }) - const { data, error } = await this.supabaseClient.storage - .from(this.selectedBucket.name) - .download(formattedPathToFile) - - if (!error) { - const blob = data + if (!res.error) { + const blob = await res.blob() const newBlob = new Blob([blob], { type: fileMimeType }) if (returnBlob) return { name: fileName, blob: newBlob } @@ -1136,7 +1120,7 @@ class StorageExplorerStore { } else { if (toastId) { this.ui.setNotification({ - error, + error: error, id: toastId, category: 'error', message: `Failed to download ${fileName}`, @@ -1157,15 +1141,14 @@ class StorageExplorerStore { const fromPath = pathToFile.length > 0 ? `${pathToFile}/${originalName}` : originalName const toPath = pathToFile.length > 0 ? `${pathToFile}/${newName}` : newName - const { error } = await this.supabaseClient.storage - .from(this.selectedBucket.name) - .move(fromPath, toPath) - if (error) { - this.ui.setNotification({ - message: error.message, - type: 'error', - }) + const res = await post(`${this.endpoint}/buckets/${this.selectedBucket.id}/objects/move`, { + from: fromPath, + to: toPath, + }) + + if (res.error) { + this.ui.setNotification({ category: 'error', message: res.error.message }) } await this.refetchAllOpenedFolders() @@ -1195,16 +1178,16 @@ class StorageExplorerStore { search: searchString, sortBy: { column: this.sortBy, order: this.sortByOrder }, } - const parameters = { signal: this.abortController.signal } - - const { data: items, error } = await this.supabaseClient.storage - .from(this.selectedBucket.name) - .list(prefix, options, parameters) + const res = await post( + `${this.endpoint}/buckets/${this.selectedBucket.id}/objects/list`, + { path: prefix, options }, + { abortSignal: this.abortController.signal } + ) this.updateRowStatus(folderName, STORAGE_ROW_STATUS.READY, index) - if (!error) { - const formattedItems = this.formatFolderItems(items) + if (!res.error) { + const formattedItems = this.formatFolderItems(res) this.pushColumnAtIndex( { id: folderId || folderName, @@ -1228,22 +1211,23 @@ class StorageExplorerStore { search: searchString, sortBy: { column: this.sortBy, order: this.sortByOrder }, } - const parameters = { signal: this.abortController.signal } - const { data: items, error } = await this.supabaseClient.storage - .from(this.selectedBucket.name) - .list(prefix, options, parameters) + const res = await post( + `${this.endpoint}/buckets/${this.selectedBucket.id}/objects/list`, + { path: prefix, options }, + { abortSignal: this.abortController.signal } + ) - if (!error) { + if (!res.error) { // Add items to column - const formattedItems = this.formatFolderItems(items) + const formattedItems = this.formatFolderItems(res) this.columns = this.columns.map((col, idx) => { if (idx === index) { return { ...col, items: col.items.concat(formattedItems), isLoadingMoreItems: false, - hasMoreItems: items.length === LIMIT, + hasMoreItems: res.length === LIMIT, } } return col @@ -1268,15 +1252,12 @@ class StorageExplorerStore { sortBy: { column: this.sortBy, order: this.sortByOrder }, } - const { data: items, error } = await this.supabaseClient.storage - .from(this.selectedBucket.name) - .list(prefix, options) - - if (error) { - console.error('Error at fetchFoldersByPath:', error) - } - - return items + const res = await post(`${this.endpoint}/buckets/${this.selectedBucket.id}/objects/list`, { + path: prefix, + options, + }) + if (res.error) console.error('Error at fetchFoldersByPath:', error) + return res }) ) @@ -1314,10 +1295,11 @@ class StorageExplorerStore { // Check parent folder if its empty, if yes, reinstate .emptyFolderPlaceholder // Used when deleting folder or deleting files validateParentFolderEmpty = async (parentFolderPrefix) => { - const { data: items, error } = await this.supabaseClient.storage - .from(this.selectedBucket.name) - .list(parentFolderPrefix, this.DEFAULT_OPTIONS) - if (!error && items.length === 0) { + const res = await post(`${this.endpoint}/buckets/${this.selectedBucket.id}/objects/list`, { + path: parentFolderPrefix, + options: this.DEFAULT_OPTIONS, + }) + if (!res.error && res.length === 0) { const prefixToPlaceholder = `${parentFolderPrefix}/${EMPTY_FOLDER_PLACEHOLDER_FILE_NAME}` await this.supabaseClient.storage .from(this.selectedBucket.name) @@ -1396,10 +1378,14 @@ class StorageExplorerStore { return () => { return new Promise(async (resolve) => { progress = progress + 1 / files.length - const { error } = await this.supabaseClient.storage - .from(this.selectedBucket.name) - .move(fromPath, toPath) - if (error) { + const res = await post( + `${this.endpoint}/buckets/${this.selectedBucket.name}/objects/move`, + { + from: fromPath, + to: toPath, + } + ) + if (res.error) { hasErrors = true this.ui.setNotification({ category: 'error', @@ -1487,12 +1473,13 @@ class StorageExplorerStore { let folderContents = [] for (;;) { - const { data } = await this.supabaseClient.storage - .from(this.selectedBucket.name) - .list(formattedPathToFolder, options) - folderContents = folderContents.concat(data) + const res = await post(`${this.endpoint}/buckets/${this.selectedBucket.name}/objects/list`, { + path: formattedPathToFolder, + options, + }) + folderContents = folderContents.concat(res) options.offset += options.limit - if ((data || []).length < options.limit) { + if ((res || []).length < options.limit) { break } } @@ -1690,6 +1677,40 @@ class StorageExplorerStore { this.sortByOrder = sortByOrder } } + + selectRangeItems = (columnIndex, toItemIndex) => { + const columnItems = this.columns[columnIndex].items + const toItem = columnItems[toItemIndex] + const selectedItemIds = this.selectedItems.map((item) => item.id) + const lastSelectedItemId = selectedItemIds[selectedItemIds.length - 1] + const lastSelectedItemIndex = findIndex(columnItems, { id: lastSelectedItemId }) + + // Get the start and end index of the range to select + const start = Math.min(toItemIndex, lastSelectedItemIndex) + const end = Math.max(toItemIndex, lastSelectedItemIndex) + + // Get the range to select and reverse the order if necessary + const rangeToSelect = columnItems + .slice(start, end + 1) + // we need `columnIndex` in all item of `selectedItems` + .map((item) => ({ ...item, columnIndex })) + if (toItemIndex < lastSelectedItemIndex) { + rangeToSelect.reverse() + } + + if (selectedItemIds.includes(toItem.id)) { + const rangeToDeselectIds = rangeToSelect.map((item) => item.id) + // Deselect all items within the selection range + this.setSelectedItems( + this.selectedItems.filter( + (item) => item.id === toItem.id || !rangeToDeselectIds.includes(item.id) + ) + ) + } else { + // Select items within the range + this.setSelectedItems(uniqBy(this.selectedItems.concat(rangeToSelect), 'id')) + } + } } export default StorageExplorerStore diff --git a/studio/package.json b/studio/package.json index aafdafc3aa..0e601d9820 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 d97675d69e..02dd7f1361 100644 --- a/studio/pages/_app.tsx +++ b/studio/pages/_app.tsx @@ -10,12 +10,12 @@ import 'styles/contextMenu.scss' import 'styles/react-data-grid-logs.scss' import 'styles/date-picker.scss' import 'styles/grid.scss' -import 'styles/users-table.scss' 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 +44,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 0000000000..6fe1b70347 --- /dev/null +++ b/studio/pages/project/[ref]/api/graphiql.tsx @@ -0,0 +1,63 @@ +import { useEffect, useMemo } from 'react' +import { observer } from 'mobx-react-lite' +import { createGraphiQLFetcher } from '@graphiql/toolkit' + +import { NextPageWithLayout } from 'types' +import { useParams, useStore } from 'hooks' +import { API_URL } from 'lib/constants' +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 { useSessionAccessTokenQuery } from 'data/auth/session-access-token-query' + +const GraphiQLPage: NextPageWithLayout = () => { + const { ref: projectRef } = useParams() + const { ui, meta } = useStore() + + const isExtensionsLoading = meta.extensions.isLoading + const pgGraphqlExtension = meta.extensions.byId('pg_graphql') + const { data: accessToken } = useSessionAccessTokenQuery() + + useEffect(() => { + if (ui.selectedProject?.ref) { + // Schemas may be needed when enabling the GraphQL extension + meta.schemas.load() + meta.extensions.load() + } + }, [ui.selectedProject?.ref]) + + const graphqlUrl = `${API_URL}/projects/${projectRef}/api/graphql` + + const fetcher = useMemo(() => { + return createGraphiQLFetcher({ url: graphqlUrl, fetch }) + }, [graphqlUrl]) + + if (!accessToken || (isExtensionsLoading && !pgGraphqlExtension)) { + return + } + + 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]/api/index.tsx b/studio/pages/project/[ref]/api/index.tsx index 82bed5db77..3ceb55c338 100644 --- a/studio/pages/project/[ref]/api/index.tsx +++ b/studio/pages/project/[ref]/api/index.tsx @@ -1,5 +1,4 @@ import { useRouter } from 'next/router' -import { Button, Dropdown, IconKey } from 'ui' import { FC, createContext, useContext, useEffect, useState } from 'react' import { observer, useLocalObservable } from 'mobx-react-lite' @@ -11,6 +10,7 @@ import { DocsLayout } from 'components/layouts' import { GeneralContent, ResourceContent, RpcContent } from 'components/interfaces/Docs' import { PermissionAction } from '@supabase/shared-types/out/constants' import { useProjectJsonSchemaQuery } from 'data/docs/project-json-schema-query' +import LangSelector from 'components/interfaces/Docs/LangSelector' const PageContext = createContext(null) @@ -77,22 +77,12 @@ const DocView: FC = observer(({}) => { const anonKey = apiService?.service_api_keys.find((x) => x.name === 'anon key') ? apiService.defaultApiKey : undefined - const swaggerUrl = data?.autoApiService?.restUrl - - const canReadServiceKey = checkPermissions( - PermissionAction.READ, - 'service_api_keys.service_role_key' - ) const { data: jsonSchema, error: jsonSchemaError, refetch, - } = useProjectJsonSchemaQuery({ - projectRef, - swaggerUrl, - apiKey: anonKey, - }) + } = useProjectJsonSchemaQuery({ projectRef }) useEffect(() => { PageState.setJsonSchema(jsonSchema) @@ -104,7 +94,7 @@ const DocView: FC = observer(({}) => { if (error || jsonSchemaError) return ( -
    +

    Error connecting to API

    {`${error || jsonSchemaError}`}

    @@ -113,7 +103,7 @@ const DocView: FC = observer(({}) => { ) if (!data || !jsonSchema || !PageState.jsonSchema) return ( -
    +

    Building docs ...

    ) @@ -129,81 +119,17 @@ const DocView: FC = observer(({}) => { const PAGE_KEY: any = resource || rpc || page || 'index' return ( -
    +
    -
    -
    -
    - - - {selectedLang == 'bash' && ( -
    -
    - - Project API key : -
    - - setShowApiKey(DEFAULT_KEY)}> - hide - - - setShowApiKey({ - key: anonKey, - name: 'anon (public)', - }) - } - > - anon (public) - - {canReadServiceKey && ( - - setShowApiKey({ - key: autoApiService.serviceApiKey, - name: 'service_role (secret)', - }) - } - > - service_role (secret) - - )} - - } - > - - -
    - )} -
    -
    +
    +
    {resource ? ( diff --git a/studio/pages/project/[ref]/database/backups/pitr.tsx b/studio/pages/project/[ref]/database/backups/pitr.tsx index 5f3eb4aada..694e62f97c 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 a5abeb5df6..bf19705695 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 ead5fba1e2..d8df5feae3 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 4db0245131..225b405112 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 613833e4f3..debcd5ebe9 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 0532afd7a8..e78b41e099 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 20ba14361d..51ff8dfb94 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} +
    - - -
    + )}
    diff --git a/studio/pages/project/[ref]/reports/query-performance.tsx b/studio/pages/project/[ref]/reports/query-performance.tsx new file mode 100644 index 0000000000..ea6acc972d --- /dev/null +++ b/studio/pages/project/[ref]/reports/query-performance.tsx @@ -0,0 +1,384 @@ +import { PRESET_CONFIG } from 'components/interfaces/Reports/Reports.constants' +import { Presets } from 'components/interfaces/Reports/Reports.types' +import { hooksFactory, usePresetReport } from 'components/interfaces/Reports/Reports.utils' +import { ReportsLayout } from 'components/layouts' +import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' +import Table from 'components/to-be-cleaned/Table' +import CopyButton from 'components/ui/CopyButton' +import ConfirmModal from 'components/ui/Dialogs/ConfirmDialog' +import { executeSql } from 'data/sql/execute-sql-query' +import { useFlag } from 'hooks' +import { observer } from 'mobx-react-lite' +import { useRouter } from 'next/router' +import { useState } from 'react' +import ReactMarkdown from 'react-markdown' +import { NextPageWithLayout } from 'types' +import { Accordion, Button, IconAlertCircle, IconCheckCircle, Tabs } from 'ui' + +const QueryPerformanceReport: NextPageWithLayout = () => { + const { project } = useProjectContext() + const router = useRouter() + const { ref } = router.query + + const [showResetgPgStatStatements, setShowResetgPgStatStatements] = useState(false) + const tableIndexEfficiencyEnabled = useFlag('tableIndexEfficiency') + + const config = PRESET_CONFIG[Presets.QUERY_PERFORMANCE] + const hooks = hooksFactory(ref as string, config) + const mostFrequentlyInvoked = hooks.mostFrequentlyInvoked() + const mostTimeConsuming = hooks.mostTimeConsuming() + const slowestExecutionTime = hooks.slowestExecutionTime() + const queryHitRate = hooks.queryHitRate() + + const { isLoading, Layout, handleRefresh } = usePresetReport([ + mostFrequentlyInvoked, + mostTimeConsuming, + slowestExecutionTime, + queryHitRate, + ]) + + const checkAlert = ( +
    + +
    + ) + const warnAlert = ( +
    + +
    + ) + const dangerAlert = ( +
    + +
    + ) + + const indexHitRate = queryHitRate[0]?.data?.[0]?.ratio + const tableHitRate = queryHitRate[0]?.data?.[1]?.ratio + const showIndexWarning = + indexHitRate && tableHitRate && (indexHitRate <= 0.99 || tableHitRate <= 0.99) + + const HeaderText = `Identify the queries that consume the most time and database resources. + + It relies on the \`pg_stat_statements\` table. Read more about [examining query performance]((https://supabase.com/docs/guides/platform/performance#examining-query-performance)). + + Consider resetting the analysis after optimizing any queries. + +` + + const TimeConsumingHelperText = `This table lists queries ordered by their cumulative total execution time. + + It displays the total time a query has spent running and the proportion of total execution time the query has consumed. +` + + const MostFrequentHelperText = `This table lists queries in order of their execution count, providing insights into the most frequently executed queries. + + Pay attention to queries with high max_time or mean_time values that are called frequently, as they may benefit from optimization. +` + + const SlowestExecutionHelperText = `This table lists queries ordered by their maximum execution time. It shows outliers with high execution times. Queries with long execution times may benefit from optimization. + + Look for queries with high or mean execution times. These are often good candidates for optimization. +` + + const panelClassNames = 'text-sm max-w-none flex flex-col gap-8 py-4' + const helperTextClassNames = 'prose text-sm max-w-2xl text-scale-1000' + + return ( + + {tableIndexEfficiencyEnabled && ( + + + Index Efficiency + {showIndexWarning ? warnAlert : checkAlert} +
    + } + id="1" + className="flex flex-row gap-8" + > + {!isLoading && queryHitRate && ( +
    +
    +
    + Index Hit Rate +
    + {indexHitRate >= 0.99 + ? checkAlert + : indexHitRate >= 0.95 + ? warnAlert + : dangerAlert} +
    + + {(queryHitRate[0]?.data![0]?.ratio * 100).toFixed(2)} + + % +
    +
    +
    + +
    + {queryHitRate[0]?.data![1]?.name == 'table hit rate' && 'Table Hit Rate'} +
    + {tableHitRate >= 0.99 + ? checkAlert + : tableHitRate >= 0.95 + ? warnAlert + : dangerAlert} +
    + + {(queryHitRate[0]?.data![1]?.ratio * 100).toFixed(2)} + + % +
    +
    +
    +
    +
    +

    + For best performance, ensure that the cache hit rate ratios above 99%.
    {' '} + Consider upgrading to an instance with more memory if the ratios dip below 95%. +

    +
    +
    + )} + + + )} + + + +
    + +
    + + setShowResetgPgStatStatements(false)} + onSelectConfirm={async () => { + try { + await executeSql({ + projectRef: project?.ref, + connectionString: project?.connectionString, + sql: `SELECT pg_stat_statements_reset();`, + }) + setShowResetgPgStatStatements(false) + handleRefresh() + } catch (error) { + console.error(error) + } + }} + /> + +
    + + +
    + +
    +
    + Role + Time Consumed + Calls + Total Time + Query + + } + body={ + !isLoading && mostTimeConsuming ? ( + mostTimeConsuming[0].data!.map((item, i) => { + return ( + + + {item.rolname} + + + {item.prop_total_time} + + + {item.calls} + + + {item.total_time.toFixed(2)}ms + + +

    {item.query}

    + +
    +
    + ) + }) + ) : ( + <> + ) + } + /> + + + + +
    + +
    +
    + {/* source */} + Role + Avg. Roles + Calls + Max Time + Mean Time + Min Time + Total Time + Query + + } + body={ + !isLoading && mostFrequentlyInvoked ? ( + mostFrequentlyInvoked[0].data!.map((item, i) => { + return ( + + + {item.rolname} + + + {item.avg_rows} + + + {item.calls} + + + {item.max_time.toFixed(2)}ms + + + {item.mean_time.toFixed(2)}ms + + + {item.min_time.toFixed(2)}ms + + + {item.total_time.toFixed(2)}ms + + +

    {item.query}

    + +
    +
    + ) + }) + ) : ( + <> + ) + } + /> + + + + +
    + +
    +
    + Role + Avg Rows + Calls + Max Time + Mean Time + Min Time + Total Time + Query + + } + body={ + !isLoading && slowestExecutionTime ? ( + slowestExecutionTime[0].data!.map((item, i) => { + return ( + + + {item.rolname} + + + {item.avg_rows} + + + {item.calls} + + + {item.max_time.toFixed(2)}ms + + + {item.mean_time.toFixed(2)}ms + + + {item.min_time.toFixed(2)}ms + + + {item.total_time.toFixed(2)}ms + + +

    {item.query}

    + +
    +
    + ) + }) + ) : ( + <> + ) + } + /> + + + + + + + ) +} + +const QueryActions = ({ sql, className }: { sql: string; className: string }) => { + if (sql.includes('insufficient privilege')) return null + + return ( +
    + +
    + ) +} + +QueryPerformanceReport.getLayout = (page) => ( + {page} +) + +export default observer(QueryPerformanceReport) diff --git a/studio/pages/project/[ref]/settings/billing/subscription.tsx b/studio/pages/project/[ref]/settings/billing/subscription.tsx index 750af45273..a47a62ad90 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 80ca79f9f0..396a914ba2 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 0000000000..8060db470e --- /dev/null +++ b/studio/public/img/graphql.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/studio/public/img/icons/reference-auth-light.svg b/studio/public/img/icons/reference-auth-light.svg new file mode 100644 index 0000000000..3995c79084 --- /dev/null +++ b/studio/public/img/icons/reference-auth-light.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/studio/public/img/icons/reference-csharp-light.svg b/studio/public/img/icons/reference-csharp-light.svg new file mode 100644 index 0000000000..782f87bcc4 --- /dev/null +++ b/studio/public/img/icons/reference-csharp-light.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/studio/public/img/icons/reference-csharp.svg b/studio/public/img/icons/reference-csharp.svg new file mode 100644 index 0000000000..40d0375079 --- /dev/null +++ b/studio/public/img/icons/reference-csharp.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/studio/public/img/icons/reference-dart-light.svg b/studio/public/img/icons/reference-dart-light.svg new file mode 100644 index 0000000000..178f51a5b8 --- /dev/null +++ b/studio/public/img/icons/reference-dart-light.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/studio/public/img/icons/reference-dart.svg b/studio/public/img/icons/reference-dart.svg new file mode 100644 index 0000000000..33f9ad24a2 --- /dev/null +++ b/studio/public/img/icons/reference-dart.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/studio/public/img/icons/reference-javascript-light.svg b/studio/public/img/icons/reference-javascript-light.svg new file mode 100644 index 0000000000..a11845dff9 --- /dev/null +++ b/studio/public/img/icons/reference-javascript-light.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/studio/public/img/icons/reference-javascript.svg b/studio/public/img/icons/reference-javascript.svg new file mode 100644 index 0000000000..f8ce1de040 --- /dev/null +++ b/studio/public/img/icons/reference-javascript.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/studio/public/img/icons/reference-python-light.svg b/studio/public/img/icons/reference-python-light.svg new file mode 100644 index 0000000000..1ac57aae57 --- /dev/null +++ b/studio/public/img/icons/reference-python-light.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/studio/public/img/icons/reference-python.svg b/studio/public/img/icons/reference-python.svg new file mode 100644 index 0000000000..4ca064b0b7 --- /dev/null +++ b/studio/public/img/icons/reference-python.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/studio/sentry.client.config.js b/studio/sentry.client.config.js index 456f62c88a..23e597a09c 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), }) diff --git a/studio/stores/app/ProjectStore.ts b/studio/stores/app/ProjectStore.ts index d6f13efb06..d43814c29a 100644 --- a/studio/stores/app/ProjectStore.ts +++ b/studio/stores/app/ProjectStore.ts @@ -68,9 +68,7 @@ export default class ProjectStore extends PostgresMetaInterface { async pingPostgrest(project: Project): Promise<'ONLINE' | 'OFFLINE' | undefined> { if (project.status === PROJECT_STATUS.ACTIVE_HEALTHY && project.restUrl) { - const success = await pingPostgrest(project.restUrl, project.ref, { - kpsVersion: project.kpsVersion, - }) + const success = await pingPostgrest(project.ref, { kpsVersion: project.kpsVersion }) return success ? 'ONLINE' : 'OFFLINE' } return undefined diff --git a/studio/stores/authConfig/schema/AuthProviders/AuthProvidersFormValidation.tsx b/studio/stores/authConfig/schema/AuthProviders/AuthProvidersFormValidation.tsx index aa06d1771c..c5ffa57f38 100644 --- a/studio/stores/authConfig/schema/AuthProviders/AuthProvidersFormValidation.tsx +++ b/studio/stores/authConfig/schema/AuthProviders/AuthProvidersFormValidation.tsx @@ -191,7 +191,7 @@ const PROVIDER_PHONE = { SMS_TEMPLATE: { title: 'SMS Message', type: 'string', - description: 'To format the OPT code use `{{ .Code }}`', + description: 'To format the OTP code use `{{ .Code }}`', }, }, validationSchema: object().shape({ diff --git a/studio/stores/authConfig/schema/AuthProviders/AuthTemplatesValidation.tsx b/studio/stores/authConfig/schema/AuthProviders/AuthTemplatesValidation.tsx index 526e91022a..101d126e95 100644 --- a/studio/stores/authConfig/schema/AuthProviders/AuthTemplatesValidation.tsx +++ b/studio/stores/authConfig/schema/AuthProviders/AuthTemplatesValidation.tsx @@ -18,9 +18,11 @@ export const CONFIRMATION: FormSchema = { descriptionOptional: 'HTML body of your email', type: 'code', description: ` -- \`{{ .ConfirmationURL }}\` : URL to confirm the message +- \`{{ .ConfirmationURL }}\` : URL to confirm the e-mail address for the new account - \`{{ .Token }}\` : The 6-digit numeric email OTP - \`{{ .TokenHash }}\` : The hashed token used in the URL +- \`{{ .SiteURL }}\` : The URL of the site +- \`{{ .Email }}\` : The users email address `, }, }, @@ -49,9 +51,11 @@ export const INVITE: FormSchema = { descriptionOptional: 'HTML body of your email', type: 'code', description: ` -- \`{{ .ConfirmationURL }}\` : URL to confirm the message +- \`{{ .ConfirmationURL }}\` : URL to accept the invitation to create an account - \`{{ .Token }}\` : The 6-digit numeric email OTP - \`{{ .TokenHash }}\` : The hashed token used in the URL +- \`{{ .SiteURL }}\` : The URL of the site +- \`{{ .Email }}\` : The users email address `, }, }, @@ -80,9 +84,11 @@ export const MAGIC_LINK: FormSchema = { descriptionOptional: 'HTML body of your email', type: 'code', description: ` -- \`{{ .ConfirmationURL }}\` : URL to confirm the message +- \`{{ .ConfirmationURL }}\` : URL for a one-time login to the user's account - \`{{ .Token }}\` : The 6-digit numeric email OTP - \`{{ .TokenHash }}\` : The hashed token used in the URL +- \`{{ .SiteURL }}\` : The URL of the site +- \`{{ .Email }}\` : The users email address `, }, }, @@ -114,6 +120,9 @@ export const EMAIL_CHANGE: FormSchema = { - \`{{ .ConfirmationURL }}\` : URL to confirm the email change - \`{{ .Token }}\` : The 6-digit numeric email OTP - \`{{ .TokenHash }}\` : The hashed token used in the URL +- \`{{ .SiteURL }}\` : The URL of the site +- \`{{ .Email }}\` : The original users email address +- \`{{ .NewEmail }\` : The users new email address `, }, }, @@ -145,6 +154,8 @@ export const RECOVERY: FormSchema = { - \`{{ .ConfirmationURL }}\` : URL to confirm the password reset - \`{{ .Token }}\` : The 6-digit numeric email OTP - \`{{ .TokenHash }}\` : The hashed token used in the URL +- \`{{ .SiteURL }}\` : The URL of the site +- \`{{ .Email }}\` : The users email address `, }, }, diff --git a/studio/stores/common/PostgresMetaInterface.ts b/studio/stores/common/PostgresMetaInterface.ts index 09f605b5ac..bd51fd319d 100644 --- a/studio/stores/common/PostgresMetaInterface.ts +++ b/studio/stores/common/PostgresMetaInterface.ts @@ -117,6 +117,7 @@ export default class PostgresMetaInterface implements IPostgresMetaInterface< } } + // [Joshen] Only used for tables and views for now async loadBySchema(schema: string) { let { LOADING, ERROR, LOADED } = this.STATES try { @@ -132,7 +133,14 @@ export default class PostgresMetaInterface implements IPostgresMetaInterface< const data = response as T[] const formattedData = keyBy(data, this.identifier) - this.data = { ...this.data, ...formattedData } + // Purge existing data that belongs to given schema, otherwise + // stale data will persist + const purgedData = Object.keys(this.data) + .map((identifier: any) => this.data[identifier]) + .filter((item: any) => item.schema !== schema) + const formattedPurgedData = keyBy(purgedData, this.identifier) + + this.data = { ...formattedPurgedData, ...formattedData } this.setState(LOADED) return data diff --git a/studio/stores/pgmeta/MetaStore.ts b/studio/stores/pgmeta/MetaStore.ts index 3d0a73e40d..a24c5f365f 100644 --- a/studio/stores/pgmeta/MetaStore.ts +++ b/studio/stores/pgmeta/MetaStore.ts @@ -183,7 +183,7 @@ export default class MetaStore implements IMetaStore { this.openApi = new OpenApiStore( this.rootStore, - `${API_URL}/props/project/${this.projectRef}/api` + `${API_URL}/projects/${this.projectRef}/api/rest` ) this.tables = new TableStore(this.rootStore, `${this.baseUrl}/tables`, this.headers) this.columns = new ColumnStore(this.rootStore, `${this.baseUrl}/columns`, this.headers) @@ -895,7 +895,7 @@ export default class MetaStore implements IMetaStore { this.headers['x-connection-encrypted'] = connectionString } - this.openApi.setUrl(`${API_URL}/props/project/${this.projectRef}/api`) + this.openApi.setUrl(`${API_URL}/projects/${this.projectRef}/api/rest`) this.openApi.setHeaders(this.headers) this.tables.setUrl(`${this.baseUrl}/tables`) diff --git a/studio/stores/pgmeta/OpenApiStore.ts b/studio/stores/pgmeta/OpenApiStore.ts index 0b8f35150d..c66a209598 100644 --- a/studio/stores/pgmeta/OpenApiStore.ts +++ b/studio/stores/pgmeta/OpenApiStore.ts @@ -52,17 +52,7 @@ export default class OpenApiStore implements IOpenApiStore { } async fetchData() { - const headers = { 'Content-Type': 'application/json', ...this.headers } - const projectConfig = await get(this.url, { headers }) - if (projectConfig.error) throw projectConfig.error - - const apiKey = projectConfig.autoApiService?.defaultApiKey - const restApiUrl = projectConfig.autoApiService?.restUrl - - const response = await get(`${restApiUrl}?apikey=${apiKey}`, { - headers: { ...headers, apiKey: apiKey, Authorization: `Bearer ${apiKey}` }, - credentials: 'omit', - }) + const response = await get(this.url) if (response.error) throw response.error const tables = response.definitions diff --git a/studio/styles/ui.scss b/studio/styles/ui.scss index e927eb4878..891b2a0bb7 100644 --- a/studio/styles/ui.scss +++ b/studio/styles/ui.scss @@ -224,10 +224,40 @@ @apply border-b flex border-scale-500; } + &.Docs--table-editor .doc-section--introduction { + .text { + width: 85%; + } + } + + &.Docs--table-editor .doc-section--introduction { + .text { + width: 85%; + } + } + + &.Docs--table-editor .doc-section--client-libraries { + flex-direction: column; + margin-bottom: 40px; + + .text, + .code { + width: 100%; + max-width: none; + } + .code { + margin-bottom: 20px; + } + } + .doc-section article:first-child { @apply prose prose-docs prose-sm max-w-none; } + &.Docs--table-editor .doc-section__table-name { + @apply mt-12; + } + .text { @apply p-4 px-6 w-1/2; max-width: 50%; @@ -239,7 +269,7 @@ a { @apply transition; } - + a:hover { @apply text-brand-1000; } @@ -270,6 +300,9 @@ @apply text-brand-900; } } + .codeblock-container + .codeblock-container { + @apply mt-12; + } } ::-moz-selection { @@ -339,4 +372,44 @@ label { @apply mt-1; } -} \ No newline at end of file +} + +.thin-scrollbars { + scrollbar-width: thin; + scrollbar-color: var(--colors-slate9) var(--colors-slate1); + + .dark & { + scrollbar-color: var(--colors-slate10) var(--colors-slate3o); + } + + ::-webkit-scrollbar { + background: var(--colors-slate2); + height: 3px; + width: 0px; + border-radius: 0; + + .dark & { + background: var(--colors-slate6); + } + } + + ::-webkit-scrollbar-track { + -webkit-box-shadow: inset 0 0 6px var(--colors-slate6); + -webkit-border-radius: 5px; + border-radius: 5px; + + .dark & { + -webkit-box-shadow: inset 0 0 6px var(--colors-slate8); + } + } + + ::-webkit-scrollbar-thumb { + -webkit-border-radius: 5px; + border-radius: 5px; + background: var(--colors-slate9); + + .dark & { + -webkit-box-shadow: inset 0 0 6px var(--colors-slate12); + } + } +} diff --git a/studio/styles/users-table.scss b/studio/styles/users-table.scss index 5bd24e6d6e..8b13789179 100644 --- a/studio/styles/users-table.scss +++ b/studio/styles/users-table.scss @@ -1,39 +1 @@ -.users-table-container { - scrollbar-width: thin; - scrollbar-color: var(--colors-slate9) var(--colors-slate1); - .dark & { - scrollbar-color: var(--colors-slate10) var(--colors-slate3o); - } - - ::-webkit-scrollbar { - background: var(--colors-slate2); - height: 3px; - width: 0px; - border-radius: 0; - - .dark & { - background: var(--colors-slate6); - } - } - - ::-webkit-scrollbar-track { - -webkit-box-shadow: inset 0 0 6px var(--colors-slate6); - -webkit-border-radius: 5px; - border-radius: 5px; - - .dark & { - -webkit-box-shadow: inset 0 0 6px var(--colors-slate8); - } - } - - ::-webkit-scrollbar-thumb { - -webkit-border-radius: 5px; - border-radius: 5px; - background: var(--colors-slate9); - - .dark & { - -webkit-box-shadow: inset 0 0 6px var(--colors-slate12); - } - } -} diff --git a/studio/tailwind.config.js b/studio/tailwind.config.js index 21148b6013..42d8f6c855 100644 --- a/studio/tailwind.config.js +++ b/studio/tailwind.config.js @@ -73,6 +73,125 @@ module.exports = ui({ 'code::after': { content: '""', }, + '--tw-prose-body': theme('colors.scale[1100]'), + '--tw-prose-headings': theme('colors.scale[1200]'), + '--tw-prose-lead': theme('colors.scale[1100]'), + '--tw-prose-links': theme('colors.scale[1100]'), + '--tw-prose-bold': theme('colors.scale[1100]'), + '--tw-prose-counters': theme('colors.scale[1100]'), + '--tw-prose-bullets': theme('colors.scale[900]'), + '--tw-prose-hr': theme('colors.scale[500]'), + '--tw-prose-quotes': theme('colors.scale[1100]'), + '--tw-prose-quote-borders': theme('colors.scale[500]'), + '--tw-prose-captions': theme('colors.scale[700]'), + '--tw-prose-code': theme('colors.scale[1200]'), + '--tw-prose-pre-code': theme('colors.scale[900]'), + '--tw-prose-pre-bg': theme('colors.scale[400]'), + '--tw-prose-th-borders': theme('colors.scale[500]'), + '--tw-prose-td-borders': theme('colors.scale[200]'), + '--tw-prose-invert-body': theme('colors.scale[200]'), + '--tw-prose-invert-headings': theme('colors.white'), + '--tw-prose-invert-lead': theme('colors.scale[500]'), + '--tw-prose-invert-links': theme('colors.white'), + '--tw-prose-invert-bold': theme('colors.white'), + '--tw-prose-invert-counters': theme('colors.scale[400]'), + '--tw-prose-invert-bullets': theme('colors.scale[600]'), + '--tw-prose-invert-hr': theme('colors.scale[700]'), + '--tw-prose-invert-quotes': theme('colors.scale[100]'), + '--tw-prose-invert-quote-borders': theme('colors.scale[700]'), + '--tw-prose-invert-captions': theme('colors.scale[400]'), + // the following are typography overrides + // examples can be seen here —> https://github.com/tailwindlabs/tailwindcss-typography/blob/master/src/styles.js + // reset all header font weights + 'h1, h2, h3, h4, h5': { + fontWeight: '400', + }, + h2: { + fontWeight: '400', + }, + p: { + fontWeight: '400', + }, + pre: { + background: 'none', + padding: 0, + marginBottom: '32px', + }, + ul: { + listStyleType: 'none', + paddingLeft: '1rem', + }, + 'ul li': { + position: 'relative', + }, + 'ul li::before': { + position: 'absolute', + top: '0.75rem', + left: '-1rem', + height: '0.125rem', + width: '0.5rem', + borderRadius: '0.25rem', + backgroundColor: 'var(--colors-scale7)', + content: '""', + }, + ol: { + paddingLeft: '1rem', + counterReset: 'item', + listStyleType: 'none', + }, + 'ol li': { display: 'block', position: 'relative', paddingLeft: '1rem' }, + 'ol li::before': { + position: 'absolute', + top: '0.25rem', + left: '-1rem', + height: '1.2rem', + width: '1.2rem', + borderRadius: '0.25rem', + backgroundColor: 'var(--colors-scale3)', + border: '1px solid var(--colors-scale5)', + content: 'counter(item) " "', + counterIncrement: 'item', + fontSize: '12px', + color: 'var(--colors-scale9)', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + }, + + 'p img': { + border: '1px solid var(--colors-scale4)', + borderRadius: '4px', + overflow: 'hidden', + }, + iframe: { + border: '1px solid ' + theme('borderColor.DEFAULT'), + borderRadius: theme('borderRadius.lg'), + }, + td: { + borderBottom: '1px solid ' + theme('colors.scale[400]'), + }, + code: { + fontWeight: '400', + padding: '0.2rem 0.4rem', + backgroundColor: theme('colors.scale[400]'), + border: '1px solid ' + theme('colors.scale[500]'), + borderRadius: theme('borderRadius.lg'), + wordBreak: 'break-all', + }, + a: { + position: 'relative', + transition: 'color 0.3s ease-in-out', + paddingBottom: '2px', + fontWeight: '400', + color: 'var(--colors-scale12)', + textDecorationLine: 'underline', + textDecorationColor: 'var(--colors-brand7)', + textDecorationThickness: '1px', + textUnderlineOffset: '4px', + }, + 'a:hover': { + textDecorationColor: 'var(--colors-brand9)', + }, }, }, docs: { diff --git a/supabase/functions/og-images/component/Docs.tsx b/supabase/functions/og-images/component/Docs.tsx index 5293a1b4bc..4ccd920950 100644 --- a/supabase/functions/og-images/component/Docs.tsx +++ b/supabase/functions/og-images/component/Docs.tsx @@ -36,13 +36,13 @@ const Docs = (props: Props) => {

    - {type} + {type.substring(0, 1).toUpperCase() + title.substring(1)} )}
    -

    {!title ? 'Supabase' : title.substring(0, 1).toUpperCase() + title.substring(1)}

    +

    {!title ? 'Supabase' : title}

    {description}