No files matched your search
@@ -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
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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: 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 (
|
||||
<>
|
||||
<Head>
|
||||
<title>{props.meta?.title} | Supabase Docs</title>
|
||||
<title>
|
||||
{asPath === '/' ? 'Supabase Docs' : `${props.meta?.title} | Supabase Docadsdsds`}
|
||||
</title>
|
||||
<meta name="description" content={props.meta?.description} />
|
||||
<meta property="og:image" content={`https://supabase.com/docs/img/supabase-og-image.png`} />
|
||||
<meta
|
||||
name="twitter:image"
|
||||
content={`https://supabase.com/docs/img/supabase-og-image.png`}
|
||||
/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="icon" href="/docs/favicon.ico" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:site_name" content={props.meta?.title} />
|
||||
<meta property="og:description" content={props.meta?.description} />
|
||||
<meta property="og:title" content={props.meta?.title} />
|
||||
</Head>
|
||||
|
||||
<NextSeo
|
||||
canonical={props.meta?.canonical ?? `https://supabase.com/docs${asPath}`}
|
||||
openGraph={{
|
||||
url: `https://supabase.com/docs${asPath}`,
|
||||
type: 'article',
|
||||
videos: props.meta?.video && [
|
||||
{
|
||||
// youtube based video meta
|
||||
url: props.meta?.video,
|
||||
width: 640,
|
||||
height: 385,
|
||||
type: 'application/x-shockwave-flash',
|
||||
},
|
||||
],
|
||||
article: {
|
||||
publishedTime: new Date().toISOString(),
|
||||
modifiedTime: new Date().toISOString(),
|
||||
authors: ['Supabase'],
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<div className={['relative transition-all ease-out', 'duration-150 '].join(' ')}>
|
||||
{/* <p className="text-brand-900 tracking-wider">Tutorials</p> */}
|
||||
<article className="prose dark:prose-dar max-w-none">
|
||||
|
||||
@@ -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> = (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 (
|
||||
<>
|
||||
<Head>
|
||||
<title>{props.meta?.title} | Supabase Docs</title>
|
||||
<meta name="description" content={props.meta?.description} />
|
||||
<meta property="og:image" content={ogImageUrl} />
|
||||
<meta name="twitter:image" content={ogImageUrl} />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
</Head>
|
||||
<NextSeo
|
||||
title={`${props.meta?.title} | Supabase Docs`}
|
||||
description={props.meta?.description ? props.meta?.description : props.meta?.title}
|
||||
canonical={props.meta?.canonical ?? `https://supabase.com/docs${asPath}`}
|
||||
openGraph={{
|
||||
title: props.meta?.title,
|
||||
description: props.meta?.description,
|
||||
url: `https://supabase.com/docs${asPath}`,
|
||||
type: 'article',
|
||||
videos: props.meta?.video && [
|
||||
@@ -90,15 +101,6 @@ const Layout: FC<Props> = (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
|
||||
)}`,
|
||||
},
|
||||
],
|
||||
}}
|
||||
/>
|
||||
<div className={['grid grid-cols-12 relative gap-4'].join(' ')}>
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
<Favicons />
|
||||
<DefaultSeo
|
||||
title={SITE_TITLE}
|
||||
description={SITE_DESCRIPTION}
|
||||
openGraph={{
|
||||
type: 'website',
|
||||
url: 'https://supabase.com/docs',
|
||||
site_name: SITE_TITLE,
|
||||
images: [
|
||||
{
|
||||
url: `https://supabase.com${basePath}/img/supabase-og-image.png`,
|
||||
width: 800,
|
||||
height: 600,
|
||||
alt: 'Supabase Og Image',
|
||||
},
|
||||
],
|
||||
}}
|
||||
twitter={{
|
||||
handle: '@supabase',
|
||||
site: '@supabase',
|
||||
cardType: 'summary_large_image',
|
||||
}}
|
||||
/>
|
||||
{IS_PLATFORM ? (
|
||||
<SessionContextProvider supabaseClient={supabase}>
|
||||
<ThemeProvider>
|
||||
|
||||
@@ -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.
|
||||
|
||||
<details>
|
||||
<summary>Click here to see an example `psql` interaction using this.</summary>
|
||||
|
||||
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)
|
||||
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## Deprecated features
|
||||
|
||||
We have deprecate some functions to ensure better performance and extensibilty of RLS policies.
|
||||
|
||||
@@ -16,7 +16,7 @@ export const meta = {
|
||||
></iframe>
|
||||
</div>
|
||||
|
||||
[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
|
||||
|
||||
|
||||
@@ -173,7 +173,7 @@ Update your Prisma schema by setting the `directUrl` in the datasource block:
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
directURL = env("DIRECT_URL")
|
||||
directUrl = env("DIRECT_URL")
|
||||
shadowDatabaseUrl = env("SHADOW_DATABASE_URL")
|
||||
}
|
||||
```
|
||||
|
||||
@@ -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 }) => <Layout meta={meta} children={children} />
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -18,12 +18,6 @@
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/database/api</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/cli</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
@@ -36,12 +30,6 @@
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/examples</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/functions</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
@@ -90,12 +78,6 @@
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/reference/index</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/handbook/contributing</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
@@ -108,30 +90,12 @@
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/new/auth</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/reference/index</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/tutorials/nextjs</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/database/api/generating-types</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/auth/auth-captcha</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
@@ -216,6 +180,12 @@
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/database/api</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/database/arrays</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
@@ -318,60 +288,6 @@
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/functions/auth</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/functions/cicd-workflow</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/functions/cors</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/functions/debugging</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/functions/import-maps</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/functions/local-development</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/functions/quickstart</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/functions/schedule-functions</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/functions/secrets</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/integrations/appsmith</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
@@ -534,6 +450,60 @@
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/functions/auth</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/functions/cicd-workflow</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/functions/cors</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/functions/debugging</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/functions/import-maps</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/functions/local-development</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/functions/quickstart</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/functions/schedule-functions</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/functions/secrets</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/platform/access-control</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
@@ -559,7 +529,7 @@
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/platform/database-usage</loc>
|
||||
<loc>https://supabase.com/docs/guides/platform/database-size</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
@@ -612,6 +582,12 @@
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/platform/ssl-enforcement</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/platform/sso</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
@@ -624,12 +600,24 @@
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/realtime/broadcast</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/realtime/postgres-changes</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/realtime/presence</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/realtime/quickstart</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
@@ -642,6 +630,12 @@
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/resources/examples</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/resources/glossary</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
@@ -715,49 +709,139 @@
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/database/extensions/http</loc>
|
||||
<loc>https://supabase.com/docs/guides/auth/auth-helpers/auth-ui</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/database/extensions/pgcron</loc>
|
||||
<loc>https://supabase.com/docs/guides/auth/auth-helpers/nextjs-server-components</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/database/extensions/pgnet</loc>
|
||||
<loc>https://supabase.com/docs/guides/auth/auth-helpers/nextjs</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/database/extensions/pgtap</loc>
|
||||
<loc>https://supabase.com/docs/guides/auth/auth-helpers/remix</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/database/extensions/plv8</loc>
|
||||
<loc>https://supabase.com/docs/guides/auth/auth-helpers/sveltekit</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/database/extensions/postgis</loc>
|
||||
<loc>https://supabase.com/docs/guides/auth/social-login/auth-apple</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/database/extensions/rum</loc>
|
||||
<loc>https://supabase.com/docs/guides/auth/social-login/auth-azure</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/database/extensions/uuid-ossp</loc>
|
||||
<loc>https://supabase.com/docs/guides/auth/social-login/auth-bitbucket</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/auth/social-login/auth-discord</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/auth/social-login/auth-facebook</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/auth/social-login/auth-github</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/auth/social-login/auth-gitlab</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/auth/social-login/auth-google</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/auth/social-login/auth-keycloak</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/auth/social-login/auth-linkedin</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/auth/social-login/auth-notion</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/auth/social-login/auth-slack</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/auth/social-login/auth-spotify</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/auth/social-login/auth-twitch</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/auth/social-login/auth-twitter</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/auth/social-login/auth-workos</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/auth/social-login/auth-zoom</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/database/api/generating-types</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
@@ -888,36 +972,6 @@
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/auth/auth-helpers/auth-ui</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/auth/auth-helpers/nextjs-server-components</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/auth/auth-helpers/nextjs</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/auth/auth-helpers/remix</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/auth/auth-helpers/sveltekit</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/auth/phone-login/messagebird</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
@@ -948,6 +1002,12 @@
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/functions/examples/discord-bot</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/functions/examples/github-actions</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
@@ -960,6 +1020,12 @@
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/functions/examples/openai</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/functions/examples/storage-caching</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
@@ -978,6 +1044,186 @@
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/functions/examples/upstash-redis</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/platform/sso/azure</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/platform/sso/gsuite</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/platform/sso/okta</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/database/extensions/http</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/database/extensions/hypopg</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/database/extensions/pg-safeupdate</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/database/extensions/pg_graphql</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/database/extensions/pg_hashids</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/database/extensions/pg_jsonschema</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/database/extensions/pg_plan_filter</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/database/extensions/pg_stat_monitor</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/database/extensions/pg_stat_statements</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/database/extensions/pgaudit</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/database/extensions/pgcron</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/database/extensions/pgjwt</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/database/extensions/pgnet</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/database/extensions/pgrepack</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/database/extensions/pgroonga</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/database/extensions/pgrouting</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/database/extensions/pgsodium</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/database/extensions/pgtap</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/database/extensions/pgvector</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/database/extensions/plpgsql_check</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/database/extensions/plv8</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/database/extensions/postgis</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/database/extensions/rum</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/database/extensions/timescaledb</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/database/extensions/uuid-ossp</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/database/extensions/wrappers</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/resources/migrating-to-supabase/firebase-auth</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
@@ -1003,25 +1249,7 @@
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/self-hosting/realtime/config</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/platform/sso/azure</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/platform/sso/gsuite</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/platform/sso/okta</loc>
|
||||
<loc>https://supabase.com/docs/guides/resources/migrating-to-supabase/render</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
@@ -1032,114 +1260,36 @@
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/resources/postgres/dropping-all-tables-in-schema</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/resources/postgres/first-row-in-group</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/resources/postgres/which-version-of-postgres</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/self-hosting/realtime/config</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/self-hosting/storage/config</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/auth/social-login/auth-apple</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/auth/social-login/auth-azure</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/auth/social-login/auth-bitbucket</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/auth/social-login/auth-discord</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/auth/social-login/auth-facebook</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/auth/social-login/auth-github</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/auth/social-login/auth-gitlab</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/auth/social-login/auth-google</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/auth/social-login/auth-keycloak</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/auth/social-login/auth-linkedin</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/auth/social-login/auth-notion</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/auth/social-login/auth-slack</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/auth/social-login/auth-spotify</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/auth/social-login/auth-twitch</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/auth/social-login/auth-twitter</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/auth/social-login/auth-workos</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/guides/auth/social-login/auth-zoom</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/reference/javascript/initializing</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
@@ -1562,6 +1712,12 @@
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/reference/javascript/db-returns</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://supabase.com/docs/reference/javascript/functions-invoke</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
@@ -3867,4 +4023,4 @@
|
||||
<changefreq>weekly</changefreq>
|
||||
<changefreq>0.5</changefreq>
|
||||
</url>
|
||||
</urlset>
|
||||
</urlset>
|
||||
@@ -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: () => (
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "next/core-web-vitals"
|
||||
}
|
||||
@@ -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
|
||||
@@ -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 🚀
|
||||
|
||||
[](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).
|
||||
|
||||

|
||||
|
||||
**_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
|
||||
@@ -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<Database>()
|
||||
const [todos, setTodos] = useState<Todos[]>([])
|
||||
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 (
|
||||
<div className="w-full">
|
||||
<h1 className="mb-12">Todo List.</h1>
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault()
|
||||
addTodo(newTaskText)
|
||||
}}
|
||||
className="flex gap-2 my-2"
|
||||
>
|
||||
<input
|
||||
className="rounded w-full p-2"
|
||||
type="text"
|
||||
placeholder="make coffee"
|
||||
value={newTaskText}
|
||||
onChange={(e) => {
|
||||
setErrorText('')
|
||||
setNewTaskText(e.target.value)
|
||||
}}
|
||||
/>
|
||||
<button className="btn-black" type="submit">
|
||||
Add
|
||||
</button>
|
||||
</form>
|
||||
{!!errorText && <Alert text={errorText} />}
|
||||
<div className="bg-white shadow overflow-hidden rounded-md">
|
||||
<ul>
|
||||
{todos.map((todo) => (
|
||||
<Todo key={todo.id} todo={todo} onDelete={() => deleteTodo(todo.id)} />
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const Todo = ({ todo, onDelete }: { todo: Todos; onDelete: () => void }) => {
|
||||
const supabase = useSupabaseClient<Database>()
|
||||
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 (
|
||||
<li className="w-full block cursor-pointer hover:bg-gray-200 focus:outline-none focus:bg-gray-200 transition duration-150 ease-in-out">
|
||||
<div className="flex items-center px-4 py-4 sm:px-6">
|
||||
<div className="min-w-0 flex-1 flex items-center">
|
||||
<div className="text-sm leading-5 font-medium truncate">{todo.task}</div>
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
className="cursor-pointer"
|
||||
onChange={(e) => toggle()}
|
||||
type="checkbox"
|
||||
checked={isCompleted ? true : false}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
onDelete()
|
||||
}}
|
||||
className="w-4 h-4 ml-2 border-2 hover:border-black rounded"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="gray">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
const Alert = ({ text }: { text: string }) => (
|
||||
<div className="rounded-md bg-red-100 p-4 my-3">
|
||||
<div className="text-sm leading-5 text-red-700">{text}</div>
|
||||
</div>
|
||||
)
|
||||
@@ -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
|
||||
)
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
reactStrictMode: true,
|
||||
}
|
||||
|
||||
module.exports = nextConfig
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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 (
|
||||
<SessionContextProvider supabaseClient={supabase}>
|
||||
<Component {...pageProps} />
|
||||
</SessionContextProvider>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Html, Head, Main, NextScript } from 'next/document'
|
||||
|
||||
export default function Document() {
|
||||
return (
|
||||
<Html lang="en">
|
||||
<Head />
|
||||
<body>
|
||||
<Main />
|
||||
<NextScript />
|
||||
</body>
|
||||
</Html>
|
||||
)
|
||||
}
|
||||
@@ -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<Data>
|
||||
) {
|
||||
res.status(200).json({ name: 'John Doe' })
|
||||
}
|
||||
@@ -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 (
|
||||
<>
|
||||
<Head>
|
||||
<title>Create Next App</title>
|
||||
<meta name="description" content="Generated by create next app" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
</Head>
|
||||
<div className="w-full h-full bg-gray-200">
|
||||
{!session ? (
|
||||
<div className="min-w-full min-h-screen flex items-center justify-center">
|
||||
<div className="w-full h-full flex justify-center items-center p-4">
|
||||
<div className="w-full h-full sm:h-auto sm:w-2/5 max-w-sm p-5 bg-white shadow flex flex-col text-base">
|
||||
<span className="font-sans text-4xl text-center pb-2 mb-1 border-b mx-4 align-center">
|
||||
Login
|
||||
</span>
|
||||
<Auth supabaseClient={supabase} appearance={{ theme: ThemeSupa }} theme="dark" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className="w-full h-full flex flex-col justify-center items-center p-4"
|
||||
style={{ minWidth: 250, maxWidth: 600, margin: 'auto' }}
|
||||
>
|
||||
<TodoList session={session} />
|
||||
<button
|
||||
className="btn-black w-full mt-12"
|
||||
onClick={async () => {
|
||||
const { error } = await supabase.auth.signOut()
|
||||
if (error) console.log('Error logging out:', error.message)
|
||||
}}
|
||||
>
|
||||
Logout
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
After Width: | Height: | Size: 25 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="40" height="31" fill="none"><g opacity=".9"><path fill="url(#a)" d="M13 .4v29.3H7V6.3h-.2L0 10.5V5L7.2.4H13Z"/><path fill="url(#b)" d="M28.8 30.1c-2.2 0-4-.3-5.7-1-1.7-.8-3-1.8-4-3.1a7.7 7.7 0 0 1-1.4-4.6h6.2c0 .8.3 1.4.7 2 .4.5 1 .9 1.7 1.2.7.3 1.6.4 2.5.4 1 0 1.7-.2 2.5-.5.7-.3 1.3-.8 1.7-1.4.4-.6.6-1.2.6-2s-.2-1.5-.7-2.1c-.4-.6-1-1-1.8-1.4-.8-.4-1.8-.5-2.9-.5h-2.7v-4.6h2.7a6 6 0 0 0 2.5-.5 4 4 0 0 0 1.7-1.3c.4-.6.6-1.3.6-2a3.5 3.5 0 0 0-2-3.3 5.6 5.6 0 0 0-4.5 0 4 4 0 0 0-1.7 1.2c-.4.6-.6 1.2-.6 2h-6c0-1.7.6-3.2 1.5-4.5 1-1.3 2.2-2.3 3.8-3C25 .4 26.8 0 28.8 0s3.8.4 5.3 1.1c1.5.7 2.7 1.7 3.6 3a7.2 7.2 0 0 1 1.2 4.2c0 1.6-.5 3-1.5 4a7 7 0 0 1-4 2.2v.2c2.2.3 3.8 1 5 2.2a6.4 6.4 0 0 1 1.6 4.6c0 1.7-.5 3.1-1.4 4.4a9.7 9.7 0 0 1-4 3.1c-1.7.8-3.7 1.1-5.8 1.1Z"/></g><defs><linearGradient id="a" x1="20" x2="20" y1="0" y2="30.1" gradientUnits="userSpaceOnUse"><stop/><stop offset="1" stop-color="#3D3D3D"/></linearGradient><linearGradient id="b" x1="20" x2="20" y1="0" y2="30.1" gradientUnits="userSpaceOnUse"><stop/><stop offset="1" stop-color="#3D3D3D"/></linearGradient></defs></svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 283 64"><path fill="black" d="M141 16c-11 0-19 7-19 18s9 18 20 18c7 0 13-3 16-7l-7-5c-2 3-6 4-9 4-5 0-9-3-10-7h28v-3c0-11-8-18-19-18zm-9 15c1-4 4-7 9-7s8 3 9 7h-18zm117-15c-11 0-19 7-19 18s9 18 20 18c6 0 12-3 16-7l-8-5c-2 3-5 4-8 4-5 0-9-3-11-7h28l1-3c0-11-8-18-19-18zm-10 15c2-4 5-7 10-7s8 3 9 7h-19zm-39 3c0 6 4 10 10 10 4 0 7-2 9-5l8 5c-3 5-9 8-17 8-11 0-19-7-19-18s8-18 19-18c8 0 14 3 17 8l-8 5c-2-3-5-5-9-5-6 0-10 4-10 10zm83-29v46h-9V5h9zM37 0l37 64H0L37 0zm92 5-27 48L74 5h10l18 30 17-30h10zm59 12v10l-3-1c-6 0-10 4-10 10v15h-9V17h9v9c0-5 6-9 13-9z"/></svg>
|
||||
|
After Width: | Height: | Size: 629 B |
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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: [],
|
||||
}
|
||||
@@ -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"]
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
IconZoomOut: 'src/components/Icon/icons/IconZoomOut/index.tsx',
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -39,8 +39,8 @@ interface AccordionProps {
|
||||
iconPosition?: Align
|
||||
onChange?: (item: string | string[]) => void
|
||||
openBehaviour: 'single' | 'multiple'
|
||||
type: Type
|
||||
size: Size
|
||||
type?: Type
|
||||
size?: Size
|
||||
defaultValue?: string | string[] | undefined
|
||||
justified?: Boolean
|
||||
chevronAlign?: Align
|
||||
@@ -98,7 +98,7 @@ function Accordion({
|
||||
className={containerClasses.join(' ')}
|
||||
children={
|
||||
<AccordionContext.Provider value={{ ...contextValue }}>
|
||||
<div className={containerClasses.join(' ')}>{children}</div>
|
||||
<div>{children}</div>
|
||||
</AccordionContext.Provider>
|
||||
}
|
||||
></RadixAccordion.Root>
|
||||
|
||||
@@ -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`)
|
||||
|
||||
@@ -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 <IconBase icon={Table} {...props} />
|
||||
}
|
||||
|
||||
export default IconTable
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export { default as IconTable } from './IconTable'
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: `
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -108,7 +108,7 @@ const Users = () => {
|
||||
{inviteEnabled && <InviteUserModal />}
|
||||
</div>
|
||||
</div>
|
||||
<section className="users-table-container mt-4 overflow-visible px-6">
|
||||
<section className="thin-scrollbars mt-4 overflow-visible px-6">
|
||||
<div className="section-block--body relative overflow-x-auto rounded">
|
||||
<div className="inline-block min-w-full align-middle">
|
||||
<UsersList />
|
||||
|
||||
@@ -14,4 +14,5 @@ export const HIDDEN_EXTENSIONS = [
|
||||
'supabase_vault',
|
||||
'intagg',
|
||||
'xml2',
|
||||
'pg_tle',
|
||||
]
|
||||
@@ -0,0 +1,4 @@
|
||||
export type showApiKey = {
|
||||
key: string
|
||||
name: string
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
import { FC, useState } from 'react'
|
||||
import Link from 'next/link'
|
||||
import React from 'react'
|
||||
import { Button, IconDownload, IconExternalLink } from 'ui'
|
||||
import { API_ADMIN_URL } from 'lib/constants'
|
||||
import { get } from 'lib/common/fetch'
|
||||
import { useParams, useStore } from 'hooks'
|
||||
import CodeSnippet from 'components/to-be-cleaned/Docs/CodeSnippet'
|
||||
|
||||
interface Props {
|
||||
selectedLang: string
|
||||
}
|
||||
|
||||
export default function GeneratingTypes({ selectedLang }: Props) {
|
||||
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 (
|
||||
<>
|
||||
<h2 className="doc-heading flex items-center justify-between">
|
||||
<span>Generating types</span>
|
||||
<Link href="https://supabase.com/docs/guides/database/api/generating-types">
|
||||
<a target="_blank">
|
||||
<Button type="default" icon={<IconExternalLink />}>
|
||||
Documentation
|
||||
</Button>
|
||||
</a>
|
||||
</Link>
|
||||
</h2>
|
||||
<div className="doc-section">
|
||||
<article className="text ">
|
||||
<p>
|
||||
Supabase APIs are generated from your database, which means that we can use database
|
||||
introspection to generate type-safe API definitions.
|
||||
</p>
|
||||
<p>
|
||||
You can generate types from your database either through the{' '}
|
||||
<Link href="https://supabase.com/docs/guides/database/api/generating-types">
|
||||
Supabase CLI
|
||||
</Link>
|
||||
, or by downloading the types file via the button on the right and importing it in your
|
||||
application within <code>src/index.ts</code>.
|
||||
</p>
|
||||
</article>
|
||||
<article
|
||||
className={`code ${selectedLang === 'js' ? 'flex items-center justify-center' : ''}`}
|
||||
>
|
||||
<div className="grid gap-2">
|
||||
<p className="text-center">
|
||||
{selectedLang === 'js' && (
|
||||
<Button
|
||||
type="default"
|
||||
disabled={isGeneratingTypes}
|
||||
loading={isGeneratingTypes}
|
||||
icon={<IconDownload strokeWidth={1.5} />}
|
||||
onClick={generateTypes}
|
||||
>
|
||||
Generate and download types
|
||||
</Button>
|
||||
)}
|
||||
</p>
|
||||
<p className="text-xs text-scale-1100 bg-scale-200 p-4 mt-2">
|
||||
Remember to re-generate and download this file as you make changes to your tables.
|
||||
</p>
|
||||
</div>
|
||||
<CodeSnippet selectedLang={selectedLang} snippet={localSnippets.cliLogin()} />
|
||||
<CodeSnippet
|
||||
selectedLang={selectedLang}
|
||||
snippet={localSnippets.generateTypes(ref ?? '')}
|
||||
/>
|
||||
</article>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
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
|
||||
`,
|
||||
},
|
||||
}),
|
||||
}
|
||||
@@ -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<Props> = ({
|
||||
selectedLang,
|
||||
setSelectedLang,
|
||||
showApiKey,
|
||||
setShowApiKey,
|
||||
apiKey,
|
||||
autoApiService,
|
||||
}) => {
|
||||
const canReadServiceKey = checkPermissions(
|
||||
PermissionAction.READ,
|
||||
'service_api_keys.service_role_key'
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="p-1 w-1/2 ml-auto">
|
||||
<div className="z-0 flex justify-end">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setSelectedLang('js')}
|
||||
className={`${
|
||||
selectedLang == 'js'
|
||||
? 'bg-scale-300 font-medium text-scale-1200 dark:bg-scale-200'
|
||||
: 'bg-scale-100 text-scale-900 dark:bg-scale-100'
|
||||
} relative inline-flex items-center border-r border-scale-200 p-1 px-2 text-sm transition hover:text-scale-1200 focus:outline-none`}
|
||||
>
|
||||
JavaScript
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setSelectedLang('bash')}
|
||||
className={`${
|
||||
selectedLang == 'bash'
|
||||
? 'bg-scale-300 font-medium text-scale-1200 dark:bg-scale-200'
|
||||
: 'bg-scale-100 text-scale-900 dark:bg-scale-100'
|
||||
} relative inline-flex items-center border-r border-scale-200 p-1 px-2 text-sm transition hover:text-scale-1200 focus:outline-none`}
|
||||
>
|
||||
Bash
|
||||
</button>
|
||||
{selectedLang == 'bash' && (
|
||||
<div className="flex">
|
||||
<div className="flex items-center gap-2 p-1 pl-2 text-xs text-scale-900">
|
||||
<IconKey size={12} strokeWidth={1.5} />
|
||||
<span>Project API key :</span>
|
||||
</div>
|
||||
<Dropdown
|
||||
align="end"
|
||||
side="bottom"
|
||||
className="cursor-pointer border-none bg-transparent p-0 pl-2 pr-8 text-sm text-scale-900"
|
||||
overlay={
|
||||
<>
|
||||
<Dropdown.Item key="hide" onClick={() => setShowApiKey(DEFAULT_KEY)}>
|
||||
hide
|
||||
</Dropdown.Item>
|
||||
{apiKey && (
|
||||
<Dropdown.Item
|
||||
key="anon"
|
||||
onClick={() =>
|
||||
setShowApiKey({
|
||||
key: apiKey,
|
||||
name: 'anon (public)',
|
||||
})
|
||||
}
|
||||
>
|
||||
anon (public)
|
||||
</Dropdown.Item>
|
||||
)}
|
||||
{canReadServiceKey && (
|
||||
<Dropdown.Item
|
||||
key="service"
|
||||
onClick={() =>
|
||||
setShowApiKey({
|
||||
key: autoApiService.serviceApiKey,
|
||||
name: 'service_role (secret)',
|
||||
})
|
||||
}
|
||||
>
|
||||
service_role (secret)
|
||||
</Dropdown.Item>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Button type="default">{showApiKey.name}</Button>
|
||||
</Dropdown>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default LangSelector
|
||||
@@ -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 (
|
||||
<>
|
||||
<h2 className="text-scale-1200mt-0">
|
||||
<span className="px-6 py-2 text-2xl">{resourceId}</span>
|
||||
<h2 className="doc-section__table-name text-scale-1200 mt-0 flex items-center px-6 gap-2">
|
||||
<span className="bg-slate-300 dark:bg-slate-400 p-2 rounded-lg">
|
||||
<IconTable size="small" />
|
||||
</span>
|
||||
<span className="text-2xl font-bold">{resourceId}</span>
|
||||
</h2>
|
||||
|
||||
<div className="doc-section">
|
||||
<article className="text ">
|
||||
<article className="text">
|
||||
<label className="font-mono text-xs uppercase text-scale-900 inline-block mb-2">
|
||||
Description
|
||||
</label>
|
||||
<Description
|
||||
content={description}
|
||||
metadata={{ table: resourceId }}
|
||||
@@ -47,7 +54,7 @@ const ResourceContent = ({
|
||||
<div>
|
||||
{properties.map((x) => (
|
||||
<div className="doc-section py-4" key={x.id}>
|
||||
<article className="text">
|
||||
<div className="text">
|
||||
<Param
|
||||
key={x.id}
|
||||
name={x.id}
|
||||
@@ -61,8 +68,8 @@ const ResourceContent = ({
|
||||
}}
|
||||
onDesciptionUpdated={refreshDocs}
|
||||
/>
|
||||
</article>
|
||||
<article className="code">
|
||||
</div>
|
||||
<div className="code">
|
||||
<CodeSnippet
|
||||
selectedLang={selectedLang}
|
||||
snippet={Snippets.readColumns({
|
||||
@@ -73,7 +80,7 @@ const ResourceContent = ({
|
||||
columnName: x.id,
|
||||
})}
|
||||
/>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -87,8 +94,8 @@ const ResourceContent = ({
|
||||
To read rows in <code>{resourceId}</code>, use the <code>select</code> method.
|
||||
</p>
|
||||
<p>
|
||||
<a href="https://supabase.com/docs/client/select" target="_blank">
|
||||
Learn more.
|
||||
<a href="https://supabase.com/docs/reference/javascript/select" target="_blank">
|
||||
Learn more
|
||||
</a>
|
||||
</p>
|
||||
</article>
|
||||
@@ -120,8 +127,11 @@ const ResourceContent = ({
|
||||
<h4 className="mt-0 text-white">Filtering</h4>
|
||||
<p>Supabase provides a wide range of filters.</p>
|
||||
<p>
|
||||
<a href="https://supabase.com/docs/client/using-filters" target="_blank">
|
||||
Learn more.
|
||||
<a
|
||||
href="https://supabase.com/docs/reference/javascript/using-filters"
|
||||
target="_blank"
|
||||
>
|
||||
Learn more
|
||||
</a>
|
||||
</p>
|
||||
</article>
|
||||
@@ -147,8 +157,8 @@ const ResourceContent = ({
|
||||
<code>insert</code> will also return the replaced values for UPSERT.
|
||||
</p>
|
||||
<p>
|
||||
<a href="https://supabase.com/docs/client/insert" target="_blank">
|
||||
Learn more.
|
||||
<a href="https://supabase.com/docs/reference/javascript/insert" target="_blank">
|
||||
Learn more
|
||||
</a>
|
||||
</p>
|
||||
</article>
|
||||
@@ -183,8 +193,8 @@ const ResourceContent = ({
|
||||
<code>update</code> will also return the replaced values for UPDATE.
|
||||
</p>
|
||||
<p>
|
||||
<a href="https://supabase.com/docs/client/update" target="_blank">
|
||||
Learn more.
|
||||
<a href="https://supabase.com/docs/reference/javascript/update" target="_blank">
|
||||
Learn more
|
||||
</a>
|
||||
</p>
|
||||
</article>
|
||||
@@ -207,8 +217,8 @@ const ResourceContent = ({
|
||||
default, so remember to specify your filters!
|
||||
</p>
|
||||
<p>
|
||||
<a href="https://supabase.com/docs/client/delete" target="_blank">
|
||||
Learn more.
|
||||
<a href="https://supabase.com/docs/reference/javascript/delete" target="_blank">
|
||||
Learn more
|
||||
</a>
|
||||
</p>
|
||||
</article>
|
||||
@@ -230,8 +240,8 @@ const ResourceContent = ({
|
||||
users depending on Row Level Security (RLS) policies.
|
||||
</p>
|
||||
<p>
|
||||
<a href="https://supabase.com/docs/client/subscribe" target="_blank">
|
||||
Learn more.
|
||||
<a href="https://supabase.com/docs/reference/javascript/subscribe" target="_blank">
|
||||
Learn more
|
||||
</a>
|
||||
</p>
|
||||
</article>
|
||||
@@ -264,21 +274,6 @@ const ResourceContent = ({
|
||||
</article>
|
||||
</div>
|
||||
</>
|
||||
<>
|
||||
<h3 className="text-scale-1200 mt-4 px-6">Much more</h3>
|
||||
<div className="doc-section py-4">
|
||||
<article className="text ">
|
||||
<p>
|
||||
These docs are a work in progress! See our{' '}
|
||||
<a href="https://supabase.com/docs/" target="_blank">
|
||||
docs
|
||||
</a>{' '}
|
||||
for the additional functionality Supabase has to offer.
|
||||
</p>
|
||||
</article>
|
||||
<article className="code"></article>
|
||||
</div>
|
||||
</>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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<Props> = ({ members, member, roles }) => {
|
||||
const MemberActions: FC<Props> = ({ member, roles }) => {
|
||||
const { ui } = useStore()
|
||||
const { slug } = useParams()
|
||||
const { rolesRemovable } = getRolesManagementPermissions(roles)
|
||||
|
||||
@@ -191,6 +191,7 @@ const MembersView: FC<Props> = ({ searchString, roles, members }) => {
|
||||
value={r.id}
|
||||
label={r.name}
|
||||
disabled={disableRoleEdit}
|
||||
className="w-36"
|
||||
>
|
||||
{r.name}
|
||||
</Listbox.Option>
|
||||
@@ -232,9 +233,7 @@ const MembersView: FC<Props> = ({ searchString, roles, members }) => {
|
||||
)}
|
||||
</Table.td>
|
||||
<Table.td>
|
||||
{!memberIsUser && (
|
||||
<MemberActions members={members} member={x} roles={roles} />
|
||||
)}
|
||||
{!memberIsUser && <MemberActions member={x} roles={roles} />}
|
||||
</Table.td>
|
||||
</Table.tr>
|
||||
</Fragment>
|
||||
|
||||
@@ -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'
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
}) => (
|
||||
<div className="flex flex-col gap-4 px-5 py-6 mx-auto 1xl:px-28 lg:px-16 xl:px-24 2xl:px-32">
|
||||
<h1 className="text-2xl text-scale-1200">{title}</h1>
|
||||
<div className="flex flex-row justify-between">
|
||||
<DatePickers
|
||||
onChange={handleDatepickerChange}
|
||||
to={data[0]?.params?.iso_timestamp_end || ''}
|
||||
from={data[0]?.params?.iso_timestamp_start || ''}
|
||||
helpers={REPORTS_DATEPICKER_HELPERS}
|
||||
/>
|
||||
<div>
|
||||
{showDatePickers && (
|
||||
<DatePickers
|
||||
onChange={handleDatepickerChange}
|
||||
to={data[0]?.params?.iso_timestamp_end || ''}
|
||||
from={data[0]?.params?.iso_timestamp_start || ''}
|
||||
helpers={REPORTS_DATEPICKER_HELPERS}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
type="default"
|
||||
size="tiny"
|
||||
|
||||
@@ -122,7 +122,7 @@ const SSLConfiguration = () => {
|
||||
)}
|
||||
<Toggle
|
||||
checked={isEnforced}
|
||||
disabled={isLoading || isSubmitting}
|
||||
disabled={isLoading || isSubmitting || !canUpdateSSLEnforcement}
|
||||
onChange={toggleSSLEnforcement}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -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 } = {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { FC } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { Button, IconAlertCircle, IconCode, IconLock } from 'ui'
|
||||
import * as Tooltip from '@radix-ui/react-tooltip'
|
||||
import { Button, IconAlertCircle, IconLock } from 'ui'
|
||||
import type { PostgresPolicy, PostgresTable } from '@supabase/postgres-meta'
|
||||
|
||||
import { useStore, checkPermissions } from 'hooks'
|
||||
@@ -9,9 +9,17 @@ import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
|
||||
interface Props {
|
||||
table: PostgresTable
|
||||
apiPreviewPanelOpen: boolean
|
||||
setApiPreviewPanelOpen: (apiPreviewPanelOpen: boolean) => void
|
||||
refreshDocs: () => void
|
||||
}
|
||||
|
||||
const GridHeaderActions: FC<Props> = ({ table }) => {
|
||||
const GridHeaderActions: FC<Props> = ({
|
||||
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)
|
||||
@@ -20,6 +28,24 @@ const GridHeaderActions: FC<Props> = ({ table }) => {
|
||||
!checkPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'tables') &&
|
||||
!checkPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'columns')
|
||||
|
||||
function handlePreviewToggle() {
|
||||
setApiPreviewPanelOpen(!apiPreviewPanelOpen)
|
||||
refreshDocs()
|
||||
}
|
||||
|
||||
const RenderAPIPreviewToggle = () => {
|
||||
return (
|
||||
<Button
|
||||
size="tiny"
|
||||
type="default"
|
||||
icon={<IconCode size={14} strokeWidth={2} />}
|
||||
onClick={handlePreviewToggle}
|
||||
>
|
||||
API Quickstart
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center space-x-3">
|
||||
{isReadOnly && (
|
||||
@@ -44,6 +70,9 @@ const GridHeaderActions: FC<Props> = ({ table }) => {
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
)}
|
||||
<div className="mt-[1px]">
|
||||
<RenderAPIPreviewToggle />
|
||||
</div>
|
||||
<Link href={`/project/${projectRef}/auth/policies#${table.id}`}>
|
||||
<a>
|
||||
<Button
|
||||
|
||||
@@ -6,12 +6,20 @@ import type { PostgresColumn, PostgresTable } from '@supabase/postgres-meta'
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
|
||||
import { SchemaView } from 'types'
|
||||
import { checkPermissions, useFlag, useStore } from 'hooks'
|
||||
import { checkPermissions, useFlag, useStore, useParams } from 'hooks'
|
||||
import GridHeaderActions from './GridHeaderActions'
|
||||
import NotFoundState from './NotFoundState'
|
||||
import SidePanelEditor from './SidePanelEditor'
|
||||
import { Dictionary, parseSupaTable, SupabaseGrid, SupabaseGridRef } from 'components/grid'
|
||||
import { IconBookOpen, SidePanel } from 'ui'
|
||||
import ActionBar from './SidePanelEditor/ActionBar'
|
||||
import { GeneralContent, ResourceContent } from '../Docs'
|
||||
import { useProjectApiQuery } from 'data/config/project-api-query'
|
||||
import { useProjectJsonSchemaQuery } from 'data/docs/project-json-schema-query'
|
||||
import { snakeToCamel } from 'lib/helpers'
|
||||
import { JsonEditValue } from './SidePanelEditor/RowEditor/RowEditor.types'
|
||||
import LangSelector from '../Docs/LangSelector'
|
||||
import GeneratingTypes from '../Docs/GeneratingTypes'
|
||||
|
||||
interface Props {
|
||||
/** Theme for the editor */
|
||||
@@ -61,11 +69,27 @@ const TableGridEditor: FC<Props> = ({
|
||||
}) => {
|
||||
const { meta, ui, vault } = useStore()
|
||||
const router = useRouter()
|
||||
const { ref: projectRef, page, id } = useParams()
|
||||
const gridRef = useRef<SupabaseGridRef>(null)
|
||||
const projectRef = ui.selectedProject?.ref
|
||||
|
||||
const tables = meta.tables.list()
|
||||
|
||||
const { data: settings } = useProjectApiQuery({ projectRef: projectRef })
|
||||
|
||||
const autoApiService = {
|
||||
...settings?.autoApiService,
|
||||
endpoint: `${settings?.autoApiService.protocol ?? 'https'}://${
|
||||
settings?.autoApiService.endpoint ?? '-'
|
||||
}`,
|
||||
}
|
||||
const DEFAULT_KEY = { name: 'hide', key: 'SUPABASE_KEY' }
|
||||
|
||||
const isVaultEnabled = useFlag('vaultExtension')
|
||||
const [encryptedColumns, setEncryptedColumns] = useState([])
|
||||
const [apiPreviewPanelOpen, setApiPreviewPanelOpen] = useState(false)
|
||||
|
||||
const [selectedLang, setSelectedLang] = useState<any>('js')
|
||||
const [showApiKey, setShowApiKey] = useState<any>(DEFAULT_KEY)
|
||||
|
||||
const isReadOnly =
|
||||
!checkPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'tables') &&
|
||||
@@ -76,6 +100,48 @@ const TableGridEditor: FC<Props> = ({
|
||||
setEncryptedColumns(columns)
|
||||
}
|
||||
|
||||
function getResourcesFromJsonSchema(value: any) {
|
||||
const { paths } = value || {}
|
||||
const functionPath = 'rpc/'
|
||||
let resources: any = {}
|
||||
|
||||
Object.entries(paths || []).forEach(([name, val]) => {
|
||||
let trimmed = name.slice(1)
|
||||
let id = trimmed.replace(functionPath, '')
|
||||
let displayName = id.replace(/_/g, ' ')
|
||||
let camelCase = snakeToCamel(id)
|
||||
let enriched = { id, displayName, camelCase }
|
||||
if (!trimmed.length) return
|
||||
else resources[id] = enriched
|
||||
})
|
||||
|
||||
return resources
|
||||
}
|
||||
|
||||
const apiService = settings?.autoApiService
|
||||
const anonKey = apiService?.service_api_keys.find((x) => x.name === 'anon key')
|
||||
? apiService.defaultApiKey
|
||||
: undefined
|
||||
const swaggerUrl = settings?.autoApiService?.restUrl
|
||||
|
||||
const {
|
||||
data: jsonSchema,
|
||||
error: jsonSchemaError,
|
||||
refetch,
|
||||
} = useProjectJsonSchemaQuery({
|
||||
projectRef,
|
||||
swaggerUrl,
|
||||
apiKey: anonKey,
|
||||
})
|
||||
|
||||
if (jsonSchemaError) console.log('jsonSchemaError', jsonSchemaError)
|
||||
|
||||
const resources = getResourcesFromJsonSchema(jsonSchema)
|
||||
|
||||
const refreshDocs = async () => {
|
||||
await refetch()
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedTable !== undefined && selectedTable.id !== undefined && isVaultEnabled) {
|
||||
getEncryptedColumns(selectedTable)
|
||||
@@ -83,7 +149,7 @@ const TableGridEditor: FC<Props> = ({
|
||||
}, [selectedTable?.id])
|
||||
|
||||
if (isUndefined(selectedTable)) {
|
||||
return <NotFoundState id={Number(router.query.id)} />
|
||||
return <NotFoundState id={Number(id)} />
|
||||
}
|
||||
|
||||
const tableId = selectedTable?.id
|
||||
@@ -177,8 +243,16 @@ const TableGridEditor: FC<Props> = ({
|
||||
editable={!isReadOnly && canUpdateTables && canEditViaTableEditor}
|
||||
schema={selectedTable.schema}
|
||||
table={gridTable}
|
||||
refreshDocs={refreshDocs}
|
||||
headerActions={
|
||||
canEditViaTableEditor && <GridHeaderActions table={selectedTable as PostgresTable} />
|
||||
canEditViaTableEditor && (
|
||||
<GridHeaderActions
|
||||
table={selectedTable as PostgresTable}
|
||||
apiPreviewPanelOpen={apiPreviewPanelOpen}
|
||||
setApiPreviewPanelOpen={setApiPreviewPanelOpen}
|
||||
refreshDocs={refreshDocs}
|
||||
/>
|
||||
)
|
||||
}
|
||||
onAddColumn={onAddColumn}
|
||||
onEditColumn={onSelectEditColumn}
|
||||
@@ -206,6 +280,82 @@ const TableGridEditor: FC<Props> = ({
|
||||
closePanel={onClosePanel}
|
||||
/>
|
||||
)}
|
||||
|
||||
<SidePanel
|
||||
key="WrapperTableEditor"
|
||||
size="xxlarge"
|
||||
visible={apiPreviewPanelOpen}
|
||||
onCancel={() => setApiPreviewPanelOpen(false)}
|
||||
header={
|
||||
<span className="flex items-center gap-2">
|
||||
<IconBookOpen size="tiny" />
|
||||
API
|
||||
</span>
|
||||
}
|
||||
customFooter={
|
||||
<ActionBar
|
||||
backButtonLabel="Close"
|
||||
hideApply={true}
|
||||
formId="wrapper-table-editor-form"
|
||||
closePanel={() => setApiPreviewPanelOpen(false)}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div className="Docs Docs--table-editor">
|
||||
<SidePanel.Content>
|
||||
{jsonSchemaError ? (
|
||||
<div className="p-6 mx-auto text-center sm:w-full md:w-3/4">
|
||||
<div className="text-scale-1000">
|
||||
<p>Error connecting to API</p>
|
||||
<p>{`${jsonSchemaError}`}</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{jsonSchema ? (
|
||||
<>
|
||||
<div className="sticky top-0 z-10 bg-scale-100 dark:bg-scale-300">
|
||||
<LangSelector
|
||||
selectedLang={selectedLang}
|
||||
setSelectedLang={setSelectedLang}
|
||||
showApiKey={showApiKey}
|
||||
setShowApiKey={setShowApiKey}
|
||||
apiKey={anonKey}
|
||||
autoApiService={autoApiService}
|
||||
/>
|
||||
</div>
|
||||
<GeneralContent
|
||||
autoApiService={autoApiService}
|
||||
selectedLang={selectedLang}
|
||||
showApiKey={true}
|
||||
page={page}
|
||||
/>
|
||||
|
||||
<GeneratingTypes selectedLang={selectedLang} />
|
||||
|
||||
{jsonSchema?.definitions && (
|
||||
<ResourceContent
|
||||
autoApiService={autoApiService}
|
||||
selectedLang={selectedLang}
|
||||
resourceId={tables.find((table) => table.id === Number(id))?.name}
|
||||
resources={resources}
|
||||
definitions={jsonSchema.definitions}
|
||||
paths={jsonSchema.paths}
|
||||
showApiKey={showApiKey.key}
|
||||
refreshDocs={refreshDocs}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className="p-6 mx-auto text-center sm:w-full md:w-3/4">
|
||||
<h3 className="text-lg">Building docs ...</h3>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</SidePanel.Content>
|
||||
</div>
|
||||
</SidePanel>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
@@ -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<Props> = ({
|
||||
onSelectDeleteBucket = () => {},
|
||||
onSelectToggleBucketPublic = () => {},
|
||||
}) => {
|
||||
const canUpdateBuckets = checkPermissions(PermissionAction.STORAGE_ADMIN_WRITE, '*')
|
||||
|
||||
return (
|
||||
<ProductMenuItem
|
||||
key={bucket.id}
|
||||
@@ -35,7 +39,7 @@ const BucketRow: FC<Props> = ({
|
||||
action={
|
||||
bucket.status === STORAGE_ROW_STATUS.LOADING ? (
|
||||
<IconLoader className="animate-spin" size={16} strokeWidth={2} />
|
||||
) : bucket.status === STORAGE_ROW_STATUS.READY ? (
|
||||
) : canUpdateBuckets && bucket.status === STORAGE_ROW_STATUS.READY ? (
|
||||
<Dropdown
|
||||
side="bottom"
|
||||
align="start"
|
||||
|
||||
@@ -40,11 +40,8 @@ const StorageLayout: FC<Props> = ({ title, children }) => {
|
||||
buckets,
|
||||
} = storageExplorerStore || {}
|
||||
|
||||
// [Joshen] At the moment, without the service key, we won't be able to pull any data from storage
|
||||
const { data: settings, isLoading } = useProjectApiQuery({ projectRef })
|
||||
const apiService = settings?.autoApiService
|
||||
const serviceKey = find(apiService?.service_api_keys ?? [], (key) => key.tags === 'service_role')
|
||||
const canAccessStorage = !isLoading && apiService && serviceKey
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoading && apiService) initializeStorageStore(apiService)
|
||||
@@ -52,15 +49,13 @@ const StorageLayout: FC<Props> = ({ 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',
|
||||
@@ -100,15 +95,15 @@ const StorageLayout: FC<Props> = ({ title, children }) => {
|
||||
}
|
||||
}
|
||||
|
||||
if (!isLoading && !canAccessStorage) {
|
||||
return (
|
||||
<BaseLayout>
|
||||
<main style={{ maxHeight: '100vh' }} className="flex-1 overflow-y-auto">
|
||||
<NoPermission isFullPage resourceText="access your project's storage" />
|
||||
</main>
|
||||
</BaseLayout>
|
||||
)
|
||||
}
|
||||
// if (!isLoading && !canAccessStorage) {
|
||||
// return (
|
||||
// <BaseLayout>
|
||||
// <main style={{ maxHeight: '100vh' }} className="flex-1 overflow-y-auto">
|
||||
// <NoPermission isFullPage resourceText="access your project's storage" />
|
||||
// </main>
|
||||
// </BaseLayout>
|
||||
// )
|
||||
// }
|
||||
|
||||
return (
|
||||
<ProjectLayout title={title || 'Storage'} product="Storage" productMenu={<StorageMenu />}>
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
import { FC } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { find } from 'lodash'
|
||||
import { useRouter } from 'next/router'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import * as Tooltip from '@radix-ui/react-tooltip'
|
||||
import { Button, Menu, IconLoader, Alert, IconEdit } from 'ui'
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
|
||||
import { useParams } from 'hooks'
|
||||
import { checkPermissions, useParams } from 'hooks'
|
||||
import BucketRow from './BucketRow'
|
||||
import { useStorageStore } from 'localStores/storageExplorer/StorageExplorerStore'
|
||||
import { useProjectApiQuery } from 'data/config/project-api-query'
|
||||
|
||||
interface Props {}
|
||||
|
||||
const StorageMenu: FC<Props> = () => {
|
||||
const router = useRouter()
|
||||
const { ref, bucketId } = useParams()
|
||||
const canCreateBuckets = checkPermissions(PermissionAction.STORAGE_ADMIN_WRITE, '*')
|
||||
|
||||
const page = router.pathname.split('/')[4] as
|
||||
| undefined
|
||||
@@ -33,11 +33,6 @@ const StorageMenu: FC<Props> = () => {
|
||||
openToggleBucketPublicModal,
|
||||
} = storageExplorerStore || {}
|
||||
|
||||
const { data: settings, isLoading } = useProjectApiQuery({ projectRef: ref })
|
||||
const apiService = settings?.autoApiService
|
||||
const serviceKey = find(apiService?.service_api_keys ?? [], (key) => key.tags === 'service_role')
|
||||
const canAccessStorage = !isLoading && apiService && serviceKey !== undefined
|
||||
|
||||
return (
|
||||
<Menu type="pills" className="my-6 flex flex-grow flex-col px-5">
|
||||
<div className="mb-6 px-2">
|
||||
@@ -51,14 +46,14 @@ const StorageMenu: FC<Props> = () => {
|
||||
<IconEdit size={14} />
|
||||
</div>
|
||||
}
|
||||
disabled={!canAccessStorage}
|
||||
disabled={!canCreateBuckets}
|
||||
style={{ justifyContent: 'start' }}
|
||||
onClick={openCreateBucketModal}
|
||||
>
|
||||
New bucket
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
{!canAccessStorage && (
|
||||
{!canCreateBuckets && (
|
||||
<Tooltip.Content side="bottom">
|
||||
<Tooltip.Arrow className="radix-tooltip-arrow" />
|
||||
<div
|
||||
|
||||
@@ -3,12 +3,12 @@ import SimpleCodeBlock from 'components/to-be-cleaned/SimpleCodeBlock'
|
||||
const CodeSnippet = ({ selectedLang, snippet }) => {
|
||||
if (!snippet[selectedLang]) return null
|
||||
return (
|
||||
<>
|
||||
<div className="codeblock-container">
|
||||
<h4>{snippet.title}</h4>
|
||||
<SimpleCodeBlock className={snippet[selectedLang].language}>
|
||||
{snippet[selectedLang].code}
|
||||
</SimpleCodeBlock>
|
||||
</>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default CodeSnippet
|
||||
@@ -1,59 +0,0 @@
|
||||
import Snippets from '../Snippets'
|
||||
import CodeSnippet from '../CodeSnippet'
|
||||
|
||||
export default function Introduction({ autoApiService, selectedLang }) {
|
||||
return (
|
||||
<>
|
||||
<h2 className="doc-heading">Introduction</h2>
|
||||
<div className="doc-section">
|
||||
<article className="text ">
|
||||
<p>
|
||||
This API provides an easy way to integrate with your Postgres database. The API
|
||||
documentation below is specifically generated for your database.
|
||||
</p>
|
||||
<p>
|
||||
This is an <b>auto-generating</b> API, so as you make changes to your database, this
|
||||
documentation will change too.
|
||||
</p>
|
||||
<p>
|
||||
<b>Please note:</b> 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.
|
||||
</p>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<h2 className="doc-heading">API URL</h2>
|
||||
<div className="doc-section ">
|
||||
<article className="text ">
|
||||
<p>The API URL for your project.</p>
|
||||
</article>
|
||||
<article className="code">
|
||||
<CodeSnippet
|
||||
selectedLang={selectedLang}
|
||||
snippet={Snippets.endpoint(autoApiService.endpoint)}
|
||||
/>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<h2 className="doc-heading">Client Libraries</h2>
|
||||
<div className="doc-section">
|
||||
<article className="text ">
|
||||
<p>Your API consists of both a RESTful interface and a Realtime interface.</p>
|
||||
<p>
|
||||
For interacting with the Realtime streams, we provide client libraries that handle the
|
||||
websockets.
|
||||
</p>
|
||||
</article>
|
||||
<article className="code">
|
||||
<CodeSnippet selectedLang={selectedLang} snippet={Snippets.install()} />
|
||||
<CodeSnippet
|
||||
selectedLang={selectedLang}
|
||||
snippet={Snippets.init(autoApiService.endpoint)}
|
||||
/>
|
||||
</article>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -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 (
|
||||
<>
|
||||
<h2 className="doc-heading">Introduction</h2>
|
||||
<div className="doc-section doc-section--introduction">
|
||||
<article className="text ">
|
||||
<p>
|
||||
This API provides an easy way to integrate with your Postgres database. The API
|
||||
documentation below is specifically generated for your database.
|
||||
</p>
|
||||
<p>
|
||||
This is an <b>auto-generating</b> API, so as you make changes to your database, this
|
||||
documentation will change too.
|
||||
</p>
|
||||
<p>
|
||||
<b>Note:</b> 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.
|
||||
</p>
|
||||
|
||||
<div className="not-prose mb-6">
|
||||
<p>Read the reference documentation:</p>
|
||||
<div className="flex items-center gap-4 mt-2">
|
||||
{libs.map((lib) => (
|
||||
<a
|
||||
href={lib.url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="
|
||||
flex items-center gap-1 rounded-md px-3 py-1
|
||||
bg-scale-300 dark:bg-scale-500
|
||||
hover:bg-scale-500 hover:dark:bg-scale-700 transition-colors
|
||||
!text-scale-1100 hover:text-scale-1100
|
||||
"
|
||||
>
|
||||
<Image
|
||||
src={`/img/icons/reference-${isDarkTheme ? lib.icon : `${lib.icon}-light`}.svg`}
|
||||
width={16}
|
||||
height={16}
|
||||
alt={lib.name}
|
||||
/>
|
||||
{lib.name}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<h2 className="doc-heading">API URL</h2>
|
||||
<div className="doc-section ">
|
||||
<article className="text ">
|
||||
<p>The API URL for your project.</p>
|
||||
</article>
|
||||
<article className="code">
|
||||
<CodeSnippet
|
||||
selectedLang={selectedLang}
|
||||
snippet={Snippets.endpoint(autoApiService.endpoint)}
|
||||
/>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<h2 className="doc-heading">Client Libraries</h2>
|
||||
<div className="doc-section doc-section--client-libraries">
|
||||
<article className="text">
|
||||
<p>Your API consists of both a RESTful interface and a Realtime interface.</p>
|
||||
<p>
|
||||
For interacting with the Realtime streams, we provide client libraries that handle the
|
||||
websockets.
|
||||
</p>
|
||||
</article>
|
||||
<article className="code">
|
||||
<CodeSnippet selectedLang={selectedLang} snippet={Snippets.install()} />
|
||||
<CodeSnippet
|
||||
selectedLang={selectedLang}
|
||||
snippet={Snippets.init(autoApiService.endpoint)}
|
||||
/>
|
||||
</article>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -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<Props> = ({ 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<Props> = ({ selectedLang }) => {
|
||||
<article className="code"></article>
|
||||
</div>
|
||||
|
||||
<h2 className="doc-heading flex items-center justify-between">
|
||||
<span>Generating types</span>
|
||||
<Link href="https://supabase.com/docs/guides/database/api/generating-types">
|
||||
<a target="_blank">
|
||||
<Button type="default" icon={<IconExternalLink />}>
|
||||
Documentation
|
||||
</Button>
|
||||
</a>
|
||||
</Link>
|
||||
</h2>
|
||||
<div className="doc-section">
|
||||
<article className="text ">
|
||||
<p>
|
||||
Supabase APIs are generated from your database, which means that we can use database
|
||||
introspection to generate type-safe API definitions.
|
||||
</p>
|
||||
<p>
|
||||
You can generate types from your database either through the{' '}
|
||||
<Link href="https://supabase.com/docs/guides/database/api/generating-types">Supabase CLI</Link>,
|
||||
or by downloading the types file via the button on the right and importing it in your
|
||||
application within <code>src/index.ts</code>.
|
||||
</p>
|
||||
</article>
|
||||
<article className={`code ${selectedLang === 'js' ? 'flex items-center' : ''}`}>
|
||||
{selectedLang === 'js' && (
|
||||
<Button
|
||||
type="default"
|
||||
disabled={isGeneratingTypes}
|
||||
loading={isGeneratingTypes}
|
||||
icon={<IconDownload strokeWidth={1.5} />}
|
||||
onClick={generateTypes}
|
||||
>
|
||||
Generate and download types
|
||||
</Button>
|
||||
)}
|
||||
<CodeSnippet selectedLang={selectedLang} snippet={localSnippets.cliLogin()} />
|
||||
<CodeSnippet
|
||||
selectedLang={selectedLang}
|
||||
snippet={localSnippets.generateTypes(ref ?? '')}
|
||||
/>
|
||||
</article>
|
||||
</div>
|
||||
<GeneratingTypes selectedLang={selectedLang} />
|
||||
|
||||
<h2 className="doc-heading">
|
||||
GraphQL <span className="lowercase">vs</span> Supabase
|
||||
@@ -141,22 +69,6 @@ const Introduction: FC<Props> = ({ 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: {
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
import { Badge } from 'ui'
|
||||
import Description from './Description'
|
||||
|
||||
const Param = ({
|
||||
name,
|
||||
type,
|
||||
format,
|
||||
required,
|
||||
description,
|
||||
metadata = {},
|
||||
onDesciptionUpdated = () => {},
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<div className="mb-4 flex items-center justify-between ">
|
||||
<code className="inline-block px-2">{name}</code>
|
||||
<Badge color={required ? 'amber' : 'scale'}>{required ? 'Required' : 'Optional'}</Badge>
|
||||
</div>
|
||||
{format && format != type && (
|
||||
<div className="mb-4 flex items-center gap-2">
|
||||
<label className="font-mono text-xs uppercase text-scale-900">Format:</label>
|
||||
<code className="px-2">{format}</code>
|
||||
{type}
|
||||
</div>
|
||||
)}
|
||||
{description !== false && (
|
||||
<>
|
||||
<label className="font-mono text-xs uppercase text-scale-900">Description</label>
|
||||
<Description content={description} metadata={metadata} onChange={onDesciptionUpdated} />
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default Param
|
||||
@@ -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<Props> = ({
|
||||
name,
|
||||
type,
|
||||
format,
|
||||
required,
|
||||
description,
|
||||
metadata = {},
|
||||
onDesciptionUpdated = () => {},
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<div className="mb-4 flex items-center justify-between ">
|
||||
<div className="flex gap-2 items-center">
|
||||
<div className="flex items-center gap-2">
|
||||
<label className="font-mono text-xs uppercase text-scale-900 min-w-[55px]">
|
||||
Column
|
||||
</label>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="text-md text-scale-1200 pb-0.5">{name}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Badge color={required ? 'amber' : 'scale'}>{required ? 'Required' : 'Optional'}</Badge>
|
||||
</div>
|
||||
{format && (
|
||||
<div className="grid gap-2 mt-6">
|
||||
<div className="mb-4 flex items-center gap-2">
|
||||
<label className="font-mono text-xs uppercase text-scale-900 min-w-[55px]">Type</label>
|
||||
<div>
|
||||
<span className="flex grow-0 bg-slate-300 px-2 py-0.5 rounded-md text-scale-1000">
|
||||
<span className="flex items-center gap-2 text-sm">
|
||||
<IconCode size="tiny" />
|
||||
<span>{getColumnType(type, format)}</span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-4 flex items-center gap-2">
|
||||
<label className="font-mono text-xs uppercase text-scale-900 min-w-[55px]">
|
||||
Format
|
||||
</label>
|
||||
<div>
|
||||
<span className="flex grow-0 bg-slate-300 px-2 py-0.5 rounded-md text-scale-1000">
|
||||
<span className="flex items-center gap-2 text-sm">
|
||||
<IconDatabase size="tiny" />
|
||||
<span>{format}</span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{description !== false && (
|
||||
<div className="grid gap-2 mt-2">
|
||||
<label className="font-mono text-xs uppercase text-scale-900">Description</label>
|
||||
<Description content={description} metadata={metadata} onChange={onDesciptionUpdated} />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default Param
|
||||
@@ -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 (
|
||||
<Menu id={id} animation="fade">
|
||||
<Item
|
||||
onClick={({ props }) => {
|
||||
onCreateNewFolder(props.index)
|
||||
}}
|
||||
>
|
||||
New folder
|
||||
</Item>
|
||||
<Separator />
|
||||
<Item
|
||||
onClick={({ props }) => {
|
||||
onSelectAllItems(props.index)
|
||||
}}
|
||||
>
|
||||
Select all items
|
||||
</Item>
|
||||
<Submenu label="View">
|
||||
<Item onClick={() => onSelectView(STORAGE_VIEWS.COLUMNS)}>As columns</Item>
|
||||
<Item onClick={() => onSelectView(STORAGE_VIEWS.LIST)}>As list</Item>
|
||||
</Submenu>
|
||||
<Submenu label="Sort by">
|
||||
<Item onClick={() => onSelectSort(STORAGE_SORT_BY.NAME)}>Name</Item>
|
||||
<Item onClick={() => onSelectSort(STORAGE_SORT_BY.CREATED_AT)}>Last created</Item>
|
||||
<Item onClick={() => onSelectSort(STORAGE_SORT_BY.UPDATED_AT)}>Last modified</Item>
|
||||
<Item onClick={() => onSelectSort(STORAGE_SORT_BY.LAST_ACCESSED_AT)}>Last accessed</Item>
|
||||
</Submenu>
|
||||
<Submenu label="Sort by order">
|
||||
<Item onClick={() => onSelectSortByOrder(STORAGE_SORT_BY_ORDER.ASC)}>Ascending</Item>
|
||||
<Item onClick={() => onSelectSortByOrder(STORAGE_SORT_BY_ORDER.DESC)}>Descending</Item>
|
||||
</Submenu>
|
||||
</Menu>
|
||||
)
|
||||
}
|
||||
|
||||
export default ColumnContextMenu
|
||||
@@ -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<Props> = ({ 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 (
|
||||
<Menu id={id} animation="fade" className="!bg-scale-300 border border-scale-500">
|
||||
<Item onClick={({ props }) => onSelectCreateFolder(props.index)}>
|
||||
<IconFolderPlus size="tiny" />
|
||||
<span className="ml-2 text-xs">New folder</span>
|
||||
</Item>
|
||||
<Separator />
|
||||
<Item onClick={({ props }) => onSelectAllItemsInColumn(props.index)}>
|
||||
<span className="ml-2 text-xs">Select all items</span>
|
||||
</Item>
|
||||
<Submenu
|
||||
label={
|
||||
<div>
|
||||
<span className="ml-2 text-xs">View</span>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Item onClick={() => setView(STORAGE_VIEWS.COLUMNS)}>
|
||||
<span className="ml-2 text-xs">As columns</span>
|
||||
</Item>
|
||||
<Item onClick={() => setView(STORAGE_VIEWS.LIST)}>
|
||||
<span className="ml-2 text-xs">As list</span>
|
||||
</Item>
|
||||
</Submenu>
|
||||
<Submenu
|
||||
label={
|
||||
<div>
|
||||
<span className="ml-2 text-xs">Sort by</span>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Item onClick={() => setSortBy(STORAGE_SORT_BY.NAME)}>
|
||||
<span className="ml-2 text-xs">Name</span>
|
||||
</Item>
|
||||
<Item onClick={() => setSortBy(STORAGE_SORT_BY.CREATED_AT)}>
|
||||
<span className="ml-2 text-xs">Last created</span>
|
||||
</Item>
|
||||
<Item onClick={() => setSortBy(STORAGE_SORT_BY.UPDATED_AT)}>
|
||||
<span className="ml-2 text-xs">Last modified</span>
|
||||
</Item>
|
||||
<Item onClick={() => setSortBy(STORAGE_SORT_BY.LAST_ACCESSED_AT)}>
|
||||
<span className="ml-2 text-xs">Last accessed</span>
|
||||
</Item>
|
||||
</Submenu>
|
||||
<Submenu
|
||||
label={
|
||||
<div>
|
||||
<span className="ml-2 text-xs">Sort by order</span>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Item onClick={() => setSortByOrder(STORAGE_SORT_BY_ORDER.ASC)}>
|
||||
<span className="ml-2 text-xs">Ascending</span>
|
||||
</Item>
|
||||
<Item onClick={() => setSortByOrder(STORAGE_SORT_BY_ORDER.DESC)}>
|
||||
<span className="ml-2 text-xs">Descending</span>
|
||||
</Item>
|
||||
</Submenu>
|
||||
</Menu>
|
||||
)
|
||||
}
|
||||
|
||||
export default ColumnContextMenu
|
||||
@@ -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"
|
||||
>
|
||||
<ColumnContextMenu
|
||||
id={CONTEXT_MENU_KEYS.STORAGE_COLUMN}
|
||||
onCreateNewFolder={onSelectCreateFolder}
|
||||
onSelectAllItems={onSelectAllItemsInColumn}
|
||||
onSelectView={onChangeView}
|
||||
onSelectSort={onChangeSortBy}
|
||||
onSelectSortByOrder={onChangeSortByOrder}
|
||||
/>
|
||||
<ColumnContextMenu id={CONTEXT_MENU_KEYS.STORAGE_COLUMN} />
|
||||
<ItemContextMenu id={CONTEXT_MENU_KEYS.STORAGE_ITEM} />
|
||||
<FolderContextMenu
|
||||
id={CONTEXT_MENU_KEYS.STORAGE_FOLDER}
|
||||
onRenameFolder={(folder) => setSelectedItemToRename(folder)}
|
||||
onDeleteFolder={(folder) => setSelectedItemsToDelete([folder])}
|
||||
/>
|
||||
<FolderContextMenu id={CONTEXT_MENU_KEYS.STORAGE_FOLDER} />
|
||||
{view === STORAGE_VIEWS.COLUMNS ? (
|
||||
<div className="flex">
|
||||
{columns.map((column, index) => (
|
||||
|
||||
@@ -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<Props> = ({
|
||||
|
||||
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<Props> = ({
|
||||
{/* @ts-ignore */}
|
||||
<input ref={uploadButtonRef} type="file" multiple onChange={onFilesUpload} />
|
||||
</div>
|
||||
<Button
|
||||
icon={<IconUpload size={16} strokeWidth={2} />}
|
||||
type="text"
|
||||
disabled={breadcrumbs.length === 0}
|
||||
onClick={onSelectUpload}
|
||||
>
|
||||
Upload files
|
||||
</Button>
|
||||
<Button
|
||||
icon={<IconFolderPlus size={16} strokeWidth={2} />}
|
||||
type="text"
|
||||
disabled={breadcrumbs.length === 0}
|
||||
onClick={() => addNewFolderPlaceholder(-1)}
|
||||
>
|
||||
Create folder
|
||||
</Button>
|
||||
<Tooltip.Root delayDuration={0}>
|
||||
<Tooltip.Trigger className="w-full">
|
||||
<Button
|
||||
icon={<IconUpload size={16} strokeWidth={2} />}
|
||||
type="text"
|
||||
disabled={!canUpdateStorage || breadcrumbs.length === 0}
|
||||
onClick={onSelectUpload}
|
||||
>
|
||||
Upload files
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
{!canUpdateStorage && (
|
||||
<Tooltip.Content side="bottom">
|
||||
<Tooltip.Arrow className="radix-tooltip-arrow" />
|
||||
<div
|
||||
className={[
|
||||
'rounded bg-scale-100 py-1 px-2 leading-none shadow',
|
||||
'border border-scale-200',
|
||||
].join(' ')}
|
||||
>
|
||||
<span className="text-xs text-scale-1200">
|
||||
You need additional permissions to upload files
|
||||
</span>
|
||||
</div>
|
||||
</Tooltip.Content>
|
||||
)}
|
||||
</Tooltip.Root>
|
||||
<Tooltip.Root delayDuration={0}>
|
||||
<Tooltip.Trigger className="w-full">
|
||||
<Button
|
||||
icon={<IconFolderPlus size={16} strokeWidth={2} />}
|
||||
type="text"
|
||||
disabled={!canUpdateStorage || breadcrumbs.length === 0}
|
||||
onClick={() => addNewFolderPlaceholder(-1)}
|
||||
>
|
||||
Create folder
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
{!canUpdateStorage && (
|
||||
<Tooltip.Content side="bottom">
|
||||
<Tooltip.Arrow className="radix-tooltip-arrow" />
|
||||
<div
|
||||
className={[
|
||||
'rounded bg-scale-100 py-1 px-2 leading-none shadow',
|
||||
'border border-scale-200',
|
||||
].join(' ')}
|
||||
>
|
||||
<span className="text-xs text-scale-1200">
|
||||
You need additional permissions to create folders
|
||||
</span>
|
||||
</div>
|
||||
</Tooltip.Content>
|
||||
)}
|
||||
</Tooltip.Root>
|
||||
</div>
|
||||
|
||||
{/* Search: Disabled for now */}
|
||||
|
||||
@@ -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<Props> = ({
|
||||
index: itemIndex,
|
||||
item = {},
|
||||
view = STORAGE_VIEWS.COLUMNS,
|
||||
columnIndex = 0,
|
||||
@@ -95,6 +100,7 @@ const FileExplorerRow: FC<Props> = ({
|
||||
addNewFolder,
|
||||
renameFolder,
|
||||
renameFile,
|
||||
selectedBucket,
|
||||
setSelectedItems,
|
||||
setSelectedItemsToDelete,
|
||||
setSelectedItemToRename,
|
||||
@@ -104,13 +110,16 @@ const FileExplorerRow: FC<Props> = ({
|
||||
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<Props> = ({
|
||||
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<Props> = ({
|
||||
const rowOptions =
|
||||
item.type === STORAGE_ROW_TYPES.FOLDER
|
||||
? [
|
||||
{
|
||||
name: 'Rename',
|
||||
icon: <IconEdit size="tiny" />,
|
||||
onClick: () => setSelectedItemToRename(itemWithColumnIndex),
|
||||
},
|
||||
{
|
||||
name: 'Download',
|
||||
icon: <IconDownload size="tiny" />,
|
||||
onClick: () => downloadFolder(itemWithColumnIndex),
|
||||
},
|
||||
{ name: 'Separator', icon: undefined, onClick: undefined },
|
||||
{
|
||||
name: 'Delete',
|
||||
icon: <IconTrash2 size="tiny" />,
|
||||
onClick: () => setSelectedItemsToDelete([itemWithColumnIndex]),
|
||||
},
|
||||
]
|
||||
: [
|
||||
...(!item.isCorrupted
|
||||
...(canUpdateFiles
|
||||
? [
|
||||
{
|
||||
name: 'Get URL',
|
||||
icon: <IconClipboard size="tiny" />,
|
||||
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: <IconEdit size="tiny" />,
|
||||
onClick: () => setSelectedItemToRename(itemWithColumnIndex),
|
||||
},
|
||||
{
|
||||
name: 'Move',
|
||||
icon: <IconMove size="tiny" />,
|
||||
onClick: () => setSelectedItemsToMove([itemWithColumnIndex]),
|
||||
},
|
||||
{
|
||||
name: 'Download',
|
||||
icon: <IconDownload size="tiny" />,
|
||||
onClick: async () => await downloadFile(itemWithColumnIndex),
|
||||
},
|
||||
{ name: 'Separator', icon: undefined, onClick: undefined },
|
||||
]
|
||||
: []),
|
||||
{
|
||||
name: 'Delete',
|
||||
icon: <IconTrash2 size="tiny" />,
|
||||
onClick: () => setSelectedItemsToDelete([itemWithColumnIndex]),
|
||||
name: 'Download',
|
||||
icon: <IconDownload size="tiny" />,
|
||||
onClick: () => downloadFolder(itemWithColumnIndex),
|
||||
},
|
||||
...(canUpdateFiles
|
||||
? [
|
||||
{ name: 'Separator', icon: undefined, onClick: undefined },
|
||||
{
|
||||
name: 'Delete',
|
||||
icon: <IconTrash2 size="tiny" />,
|
||||
onClick: () => setSelectedItemsToDelete([itemWithColumnIndex]),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
]
|
||||
: [
|
||||
...(!item.isCorrupted
|
||||
? [
|
||||
...(isPublic
|
||||
? [
|
||||
{
|
||||
name: 'Get URL',
|
||||
icon: <IconClipboard size="tiny" />,
|
||||
onClick: async () => await copyFileURLToClipboard(itemWithColumnIndex),
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
name: 'Get URL',
|
||||
icon: <IconClipboard size="tiny" />,
|
||||
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: <IconEdit size="tiny" />,
|
||||
onClick: () => setSelectedItemToRename(itemWithColumnIndex),
|
||||
},
|
||||
{
|
||||
name: 'Move',
|
||||
icon: <IconMove size="tiny" />,
|
||||
onClick: () => setSelectedItemsToMove([itemWithColumnIndex]),
|
||||
},
|
||||
{
|
||||
name: 'Download',
|
||||
icon: <IconDownload size="tiny" />,
|
||||
onClick: async () => await downloadFile(itemWithColumnIndex),
|
||||
},
|
||||
{ name: 'Separator', icon: undefined, onClick: undefined },
|
||||
]
|
||||
: []),
|
||||
]
|
||||
: []),
|
||||
...(canUpdateFiles
|
||||
? [
|
||||
{
|
||||
name: 'Delete',
|
||||
icon: <IconTrash2 size="tiny" />,
|
||||
onClick: () => setSelectedItemsToDelete([itemWithColumnIndex]),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
]
|
||||
|
||||
const size = item.metadata ? formatBytes(item.metadata.size) : '-'
|
||||
@@ -343,7 +391,7 @@ const FileExplorerRow: FC<Props> = ({
|
||||
checked={isSelected}
|
||||
onChange={(event) => {
|
||||
event.stopPropagation()
|
||||
onCheckItem(itemWithColumnIndex)
|
||||
onCheckItem((event.nativeEvent as KeyboardEvent).shiftKey)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -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 (
|
||||
<Menu id={id} animation="fade" className="!bg-scale-300 border border-scale-500">
|
||||
<Item onClick={({ props }) => onRenameFolder(props.item)}>
|
||||
<IconEdit size="tiny" />
|
||||
<span className="ml-2 text-xs">Rename</span>
|
||||
</Item>
|
||||
<Item onClick={({ props }) => downloadFolder(props.item)}>
|
||||
<IconDownload size="tiny" />
|
||||
<span className="ml-2 text-xs">Download</span>
|
||||
</Item>
|
||||
<Separator />
|
||||
<Item onClick={({ props }) => onDeleteFolder(props.item)}>
|
||||
<IconTrash2 size="tiny" />
|
||||
<span className="ml-2 text-xs">Delete</span>
|
||||
</Item>
|
||||
</Menu>
|
||||
)
|
||||
}
|
||||
|
||||
export default FolderContextMenu
|
||||
@@ -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<Props> = ({ id = '' }) => {
|
||||
const storageExplorerStore = useStorageStore()
|
||||
const { downloadFolder, setSelectedItemToRename, setSelectedItemsToDelete } = storageExplorerStore
|
||||
const canUpdateFiles = checkPermissions(PermissionAction.STORAGE_ADMIN_WRITE, '*')
|
||||
|
||||
return (
|
||||
<Menu id={id} animation="fade" className="!bg-scale-300 border border-scale-500">
|
||||
{canUpdateFiles && (
|
||||
<Item onClick={({ props }) => setSelectedItemToRename(props.item)}>
|
||||
<IconEdit size="tiny" />
|
||||
<span className="ml-2 text-xs">Rename</span>
|
||||
</Item>
|
||||
)}
|
||||
<Item onClick={({ props }) => downloadFolder(props.item)}>
|
||||
<IconDownload size="tiny" />
|
||||
<span className="ml-2 text-xs">Download</span>
|
||||
</Item>
|
||||
{canUpdateFiles && (
|
||||
<>
|
||||
<Separator />
|
||||
<Item onClick={({ props }) => setSelectedItemsToDelete([props.item])}>
|
||||
<IconTrash2 size="tiny" />
|
||||
<span className="ml-2 text-xs">Delete</span>
|
||||
</Item>
|
||||
</>
|
||||
)}
|
||||
</Menu>
|
||||
)
|
||||
}
|
||||
|
||||
export default FolderContextMenu
|
||||
@@ -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<Props> = ({ 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 = '' }) => {
|
||||
</Item>
|
||||
</Submenu>
|
||||
)}
|
||||
<Item onClick={({ props }) => onHandleClick('rename', props.item)}>
|
||||
<IconEdit size="tiny" />
|
||||
<span className="ml-2 text-xs">Rename</span>
|
||||
</Item>
|
||||
<Item onClick={({ props }) => onHandleClick('move', props.item)}>
|
||||
<IconMove size="tiny" />
|
||||
<span className="ml-2 text-xs">Move</span>
|
||||
</Item>
|
||||
<Item onClick={({ props }) => onHandleClick('download', props.item)}>
|
||||
<IconDownload size="tiny" />
|
||||
<span className="ml-2 text-xs">Download</span>
|
||||
</Item>
|
||||
<Separator />
|
||||
<Item onClick={({ props }) => setSelectedItemsToDelete([props.item])}>
|
||||
<IconTrash2 size="tiny" />
|
||||
<span className="ml-2 text-xs">Delete</span>
|
||||
</Item>
|
||||
{canUpdateFiles && (
|
||||
<>
|
||||
<Item onClick={({ props }) => onHandleClick('rename', props.item)}>
|
||||
<IconEdit size="tiny" />
|
||||
<span className="ml-2 text-xs">Rename</span>
|
||||
</Item>
|
||||
<Item onClick={({ props }) => onHandleClick('move', props.item)}>
|
||||
<IconMove size="tiny" />
|
||||
<span className="ml-2 text-xs">Move</span>
|
||||
</Item>
|
||||
<Item onClick={({ props }) => onHandleClick('download', props.item)}>
|
||||
<IconDownload size="tiny" />
|
||||
<span className="ml-2 text-xs">Download</span>
|
||||
</Item>
|
||||
<Separator />
|
||||
<Item onClick={({ props }) => setSelectedItemsToDelete([props.item])}>
|
||||
<IconTrash2 size="tiny" />
|
||||
<span className="ml-2 text-xs">Delete</span>
|
||||
</Item>
|
||||
</>
|
||||
)}
|
||||
</Menu>
|
||||
)
|
||||
}
|
||||
@@ -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<Props> = ({
|
||||
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()
|
||||
}
|
||||
@@ -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 */}
|
||||
<div className="flex space-x-2 border-b border-panel-border-light pb-4 dark:border-panel-border-dark">
|
||||
<Link href={`${file.previewUrl}&download`}>
|
||||
<a>
|
||||
<Button
|
||||
type="default"
|
||||
icon={<IconDownload size={16} strokeWidth={2} />}
|
||||
disabled={file.isCorrupted}
|
||||
>
|
||||
Download
|
||||
</Button>
|
||||
</a>
|
||||
</Link>
|
||||
<Button
|
||||
type="default"
|
||||
icon={<IconDownload size={16} strokeWidth={2} />}
|
||||
disabled={file.isCorrupted}
|
||||
onClick={async () => await downloadFile(file)}
|
||||
>
|
||||
Download
|
||||
</Button>
|
||||
{selectedBucket.public ? (
|
||||
<Button
|
||||
type="outline"
|
||||
@@ -238,15 +240,35 @@ const PreviewPane = () => {
|
||||
</Dropdown>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
type="outline"
|
||||
shadow={false}
|
||||
size="tiny"
|
||||
icon={<IconTrash2 size={16} strokeWidth={2} />}
|
||||
onClick={() => setSelectedItemsToDelete([file])}
|
||||
>
|
||||
Delete file
|
||||
</Button>
|
||||
<Tooltip.Root delayDuration={0}>
|
||||
<Tooltip.Trigger>
|
||||
<Button
|
||||
type="outline"
|
||||
disabled={!canUpdateFiles}
|
||||
shadow={false}
|
||||
size="tiny"
|
||||
icon={<IconTrash2 size={16} strokeWidth={2} />}
|
||||
onClick={() => setSelectedItemsToDelete([file])}
|
||||
>
|
||||
Delete file
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
{!canUpdateFiles && (
|
||||
<Tooltip.Content side="bottom">
|
||||
<Tooltip.Arrow className="radix-tooltip-arrow" />
|
||||
<div
|
||||
className={[
|
||||
'rounded bg-scale-100 py-1 px-2 leading-none shadow',
|
||||
'border border-scale-200',
|
||||
].join(' ')}
|
||||
>
|
||||
<span className="text-xs text-scale-1200">
|
||||
You need additional permissions to delete this file
|
||||
</span>
|
||||
</div>
|
||||
</Tooltip.Content>
|
||||
)}
|
||||
</Tooltip.Root>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
|
||||
@@ -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 (
|
||||
<div
|
||||
ref={storageExplorerRef}
|
||||
@@ -191,10 +177,6 @@ const StorageExplorer = observer(({ bucket }) => {
|
||||
onFilesUpload={onFilesUpload}
|
||||
onSelectAllItemsInColumn={onSelectAllItemsInColumn}
|
||||
onSelectColumnEmptySpace={onSelectColumnEmptySpace}
|
||||
onSelectCreateFolder={onSelectCreateFolder}
|
||||
onChangeView={onChangeView}
|
||||
onChangeSortBy={onChangeSortBy}
|
||||
onChangeSortByOrder={onChangeSortByOrder}
|
||||
onColumnLoadMore={(index, column) =>
|
||||
fetchMoreFolderContents(index, column, itemSearchString)
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<span
|
||||
className={[
|
||||
`text-mono
|
||||
h-[18px],
|
||||
flex
|
||||
items-center
|
||||
justify-center
|
||||
px-1.5
|
||||
rounded-full
|
||||
tracking-widest
|
||||
text-[10px]
|
||||
font-medium
|
||||
truncate`,
|
||||
color,
|
||||
].join(' ')}
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
const ProductMenuItem: FC<Props> = ({
|
||||
name = '',
|
||||
isActive,
|
||||
@@ -34,9 +60,9 @@ const ProductMenuItem: FC<Props> = ({
|
||||
<div className="flex w-full items-center justify-between gap-1">
|
||||
<div
|
||||
title={hoverText ? hoverText : typeof name === 'string' ? name : ''}
|
||||
className={`flex items-center gap-2 w-full ${textClassName}`}
|
||||
className={'flex items-center gap-2 truncate w-full ' + textClassName}
|
||||
>
|
||||
<span className="truncate">{name} </span>
|
||||
<span className="truncate">{name}{' '}</span>
|
||||
{label !== undefined && (
|
||||
<span className="text-orange-800 text-xs font-normal truncate">{label}</span>
|
||||
)}
|
||||
|
||||
@@ -21,10 +21,9 @@ export async function getPermissions(signal?: AbortSignal) {
|
||||
export type PermissionsData = Awaited<ReturnType<typeof getPermissions>>
|
||||
export type PermissionsError = unknown
|
||||
|
||||
export const usePermissionsQuery = <TData = PermissionsData>({
|
||||
enabled = true,
|
||||
...options
|
||||
}: UseQueryOptions<PermissionsData, PermissionsError, TData> = {}) =>
|
||||
export const usePermissionsQuery = <TData = PermissionsData>(
|
||||
options: UseQueryOptions<PermissionsData, PermissionsError, TData> = {}
|
||||
) =>
|
||||
useQuery<PermissionsData, PermissionsError, TData>(
|
||||
permissionKeys.list(),
|
||||
({ signal }) => getPermissions(signal),
|
||||
|
||||
@@ -12,6 +12,9 @@ export async function handleResponse<T>(
|
||||
response: Response,
|
||||
requestId: string
|
||||
): Promise<SupaResponse<T>> {
|
||||
const contentType = response.headers.get('Content-Type')
|
||||
if (contentType === 'application/octet-stream') return response as any
|
||||
|
||||
try {
|
||||
const resTxt = await response.text()
|
||||
try {
|
||||
|
||||
@@ -9,7 +9,7 @@ export async function post<T = any>(
|
||||
): Promise<SupaResponse<T>> {
|
||||
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',
|
||||
@@ -17,6 +17,7 @@ export async function post<T = any>(
|
||||
referrerPolicy: 'no-referrer-when-downgrade',
|
||||
headers,
|
||||
...otherOptions,
|
||||
signal: abortSignal,
|
||||
})
|
||||
if (!response.ok) return handleResponseError(response, requestId)
|
||||
return handleResponse(response, requestId)
|
||||
|
||||
@@ -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
|
||||
@@ -10,7 +10,6 @@ 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'
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -79,11 +79,6 @@ const DocView: FC<any> = observer(({}) => {
|
||||
: undefined
|
||||
const swaggerUrl = data?.autoApiService?.restUrl
|
||||
|
||||
const canReadServiceKey = checkPermissions(
|
||||
PermissionAction.READ,
|
||||
'service_api_keys.service_role_key'
|
||||
)
|
||||
|
||||
const {
|
||||
data: jsonSchema,
|
||||
error: jsonSchemaError,
|
||||
@@ -104,7 +99,7 @@ const DocView: FC<any> = observer(({}) => {
|
||||
|
||||
if (error || jsonSchemaError)
|
||||
return (
|
||||
<div className="mx-auto p-6 text-center sm:w-full md:w-3/4">
|
||||
<div className="p-6 mx-auto text-center sm:w-full md:w-3/4">
|
||||
<p className="text-scale-1000">
|
||||
<p>Error connecting to API</p>
|
||||
<p>{`${error || jsonSchemaError}`}</p>
|
||||
@@ -113,7 +108,7 @@ const DocView: FC<any> = observer(({}) => {
|
||||
)
|
||||
if (!data || !jsonSchema || !PageState.jsonSchema)
|
||||
return (
|
||||
<div className="mx-auto p-6 text-center sm:w-full md:w-3/4">
|
||||
<div className="p-6 mx-auto text-center sm:w-full md:w-3/4">
|
||||
<h3 className="text-xl">Building docs ...</h3>
|
||||
</div>
|
||||
)
|
||||
@@ -129,81 +124,17 @@ const DocView: FC<any> = observer(({}) => {
|
||||
const PAGE_KEY: any = resource || rpc || page || 'index'
|
||||
|
||||
return (
|
||||
<div className="Docs h-full w-full overflow-y-auto" key={PAGE_KEY}>
|
||||
<div className="w-full h-full overflow-y-auto Docs Docs--api-page" key={PAGE_KEY}>
|
||||
<div className="Docs--inner-wrapper">
|
||||
<div className="sticky top-0 z-40 flex w-full flex-row-reverse ">
|
||||
<div className="bg-scale-100 dark:bg-scale-300" style={{ width: '50%' }}>
|
||||
<div className="z-0 flex ">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setSelectedLang('js')}
|
||||
className={`${
|
||||
selectedLang == 'js'
|
||||
? 'bg-scale-300 font-medium text-scale-1200 dark:bg-scale-200'
|
||||
: 'bg-scale-100 text-scale-900 dark:bg-scale-100'
|
||||
} relative inline-flex items-center border-r border-scale-200 p-1 px-2 text-sm transition hover:text-scale-1200 focus:outline-none`}
|
||||
>
|
||||
JavaScript
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setSelectedLang('bash')}
|
||||
className={`${
|
||||
selectedLang == 'bash'
|
||||
? 'bg-scale-300 font-medium text-scale-1200 dark:bg-scale-200'
|
||||
: 'bg-scale-100 text-scale-900 dark:bg-scale-100'
|
||||
} relative inline-flex items-center border-r border-scale-200 p-1 px-2 text-sm transition hover:text-scale-1200 focus:outline-none`}
|
||||
>
|
||||
Bash
|
||||
</button>
|
||||
{selectedLang == 'bash' && (
|
||||
<div className="flex">
|
||||
<div className="flex items-center gap-2 p-1 pl-2 text-xs text-scale-900">
|
||||
<IconKey size={12} strokeWidth={1.5} />
|
||||
<span>Project API key :</span>
|
||||
</div>
|
||||
<Dropdown
|
||||
align="end"
|
||||
side="bottom"
|
||||
className="cursor-pointer border-none bg-transparent p-0 pl-2 pr-8 text-sm text-scale-900"
|
||||
overlay={
|
||||
<>
|
||||
<Dropdown.Item key="hide" onClick={() => setShowApiKey(DEFAULT_KEY)}>
|
||||
hide
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
key="anon"
|
||||
onClick={() =>
|
||||
setShowApiKey({
|
||||
key: anonKey,
|
||||
name: 'anon (public)',
|
||||
})
|
||||
}
|
||||
>
|
||||
anon (public)
|
||||
</Dropdown.Item>
|
||||
{canReadServiceKey && (
|
||||
<Dropdown.Item
|
||||
key="service"
|
||||
onClick={() =>
|
||||
setShowApiKey({
|
||||
key: autoApiService.serviceApiKey,
|
||||
name: 'service_role (secret)',
|
||||
})
|
||||
}
|
||||
>
|
||||
service_role (secret)
|
||||
</Dropdown.Item>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Button type="default">{showApiKey.name}</Button>
|
||||
</Dropdown>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="sticky top-0 z-40 flex flex-row-reverse w-full ">
|
||||
<LangSelector
|
||||
selectedLang={selectedLang}
|
||||
setSelectedLang={setSelectedLang}
|
||||
showApiKey={showApiKey}
|
||||
setShowApiKey={setShowApiKey}
|
||||
apiKey={anonKey}
|
||||
autoApiService={autoApiService}
|
||||
/>
|
||||
</div>
|
||||
<div className="">
|
||||
{resource ? (
|
||||
|
||||
@@ -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 = (
|
||||
<div className="w-5 h-5 text-brand-1400 text-brand-900 flex items-center justify-center">
|
||||
<IconCheckCircle strokeWidth={2} size={16} />
|
||||
</div>
|
||||
)
|
||||
const warnAlert = (
|
||||
<div className="w-6 h-6 text-yellow-900 flex items-center justify-center">
|
||||
<IconAlertCircle strokeWidth={2} size={16} />
|
||||
</div>
|
||||
)
|
||||
const dangerAlert = (
|
||||
<div className="w-6 h-6 text-red-900 flex items-center justify-center">
|
||||
<IconAlertCircle strokeWidth={2} size={16} />
|
||||
</div>
|
||||
)
|
||||
|
||||
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 optimising 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 (
|
||||
<Layout title="Query Performance" showDatePickers={false}>
|
||||
{tableIndexEfficiencyEnabled && (
|
||||
<Accordion
|
||||
openBehaviour="multiple"
|
||||
chevronAlign="right"
|
||||
className=" border p-2 bg-scale-300 rounded"
|
||||
>
|
||||
<Accordion.Item
|
||||
header={
|
||||
<div className="flex flex-row gap-2 items-center p-2">
|
||||
<span className="text-xl">Index Efficiency</span>
|
||||
{showIndexWarning ? warnAlert : checkAlert}
|
||||
</div>
|
||||
}
|
||||
id="1"
|
||||
className="flex flex-row gap-8"
|
||||
>
|
||||
{!isLoading && queryHitRate && (
|
||||
<div>
|
||||
<div className="flex flex-row px-8 py-4 gap-8">
|
||||
<div className="w-1/2 bg-slate-200 rounded-md p-4">
|
||||
Index Hit Rate
|
||||
<div className="flex items-center gap-2">
|
||||
{indexHitRate >= 0.99
|
||||
? checkAlert
|
||||
: indexHitRate >= 0.95
|
||||
? warnAlert
|
||||
: dangerAlert}
|
||||
<div className="flex items-baseline">
|
||||
<span className="text-3xl">
|
||||
{(queryHitRate[0]?.data![0]?.ratio * 100).toFixed(2)}
|
||||
</span>
|
||||
<span className="text-xl">%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-1/2 bg-slate-200 rounded-md p-4">
|
||||
{queryHitRate[0]?.data![1]?.name == 'table hit rate' && 'Table Hit Rate'}
|
||||
<div className="flex items-center gap-2">
|
||||
{tableHitRate >= 0.99
|
||||
? checkAlert
|
||||
: tableHitRate >= 0.95
|
||||
? warnAlert
|
||||
: dangerAlert}
|
||||
<div className="flex items-baseline">
|
||||
<span className="text-3xl">
|
||||
{(queryHitRate[0]?.data![1]?.ratio * 100).toFixed(2)}
|
||||
</span>
|
||||
<span className="text-xl">%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-8 pt-4 m-0">
|
||||
<p className="text-scale-1100 text-sm max-w-2xl">
|
||||
For best performance, ensure that the cache hit rate ratios above 99%. <br />{' '}
|
||||
Consider upgrading to an instance with more memory if the ratios dip below 95%.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Accordion.Item>
|
||||
</Accordion>
|
||||
)}
|
||||
|
||||
<ReactMarkdown className={helperTextClassNames} children={HeaderText} />
|
||||
|
||||
<div className="mb-8">
|
||||
<Button type="default" onClick={() => setShowResetgPgStatStatements(true)}>
|
||||
Reset analysis
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<ConfirmModal
|
||||
danger
|
||||
visible={showResetgPgStatStatements}
|
||||
title="Reset query performance analysis"
|
||||
description={
|
||||
'This will reset the `extensions.pg_stat_statements` table that is used to calculate query performance. This data will repopulate immediately after.'
|
||||
}
|
||||
buttonLabel="Clear table"
|
||||
buttonLoadingLabel="Deleting"
|
||||
onSelectCancel={() => 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)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="flex flex-col">
|
||||
<Tabs type="underlined" size="medium">
|
||||
<Tabs.Panel key={1} id="1" label="Most time consuming">
|
||||
<div className={panelClassNames}>
|
||||
<ReactMarkdown className={helperTextClassNames} children={TimeConsumingHelperText} />
|
||||
<div className="thin-scrollbars max-w-full overflow-scroll">
|
||||
<Table
|
||||
head={
|
||||
<>
|
||||
<Table.th className="table-cell">Role</Table.th>
|
||||
<Table.th className="table-cell">Time Consumed</Table.th>
|
||||
<Table.th className="table-cell">Calls</Table.th>
|
||||
<Table.th className="table-cell">Total Time</Table.th>
|
||||
<Table.th className="table-cell">Query</Table.th>
|
||||
</>
|
||||
}
|
||||
body={
|
||||
!isLoading && mostTimeConsuming ? (
|
||||
mostTimeConsuming[0].data!.map((item, i) => {
|
||||
return (
|
||||
<Table.tr key={i} hoverable className="relative">
|
||||
<Table.td className="table-cell whitespace-nowrap w-36">
|
||||
{item.rolname}
|
||||
</Table.td>
|
||||
<Table.td className="table-cell whitespace-nowrap">
|
||||
{item.prop_total_time}
|
||||
</Table.td>
|
||||
<Table.td className="table-cell whitespace-nowrap">
|
||||
{item.calls}
|
||||
</Table.td>
|
||||
<Table.td className="table-cell whitespace-nowrap">
|
||||
{item.total_time.toFixed(2)}ms
|
||||
</Table.td>
|
||||
<Table.td className="relative table-cell whitespace-nowrap w-36">
|
||||
<p className="w-96 block truncate font-mono">{item.query}</p>
|
||||
<QueryActions
|
||||
sql={item.query}
|
||||
className="absolute inset-y-0 right-0"
|
||||
/>
|
||||
</Table.td>
|
||||
</Table.tr>
|
||||
)
|
||||
})
|
||||
) : (
|
||||
<></>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Tabs.Panel>
|
||||
<Tabs.Panel key={2} id="2" label="Most frequent">
|
||||
<div className={panelClassNames}>
|
||||
<ReactMarkdown className={helperTextClassNames} children={MostFrequentHelperText} />
|
||||
<div className="thin-scrollbars max-w-full overflow-scroll">
|
||||
<Table
|
||||
head={
|
||||
<>
|
||||
{/* <Table.th className="table-cell">source</Table.th> */}
|
||||
<Table.th className="table-cell">Role</Table.th>
|
||||
<Table.th className="table-cell">Avg. Roles</Table.th>
|
||||
<Table.th className="table-cell">Calls</Table.th>
|
||||
<Table.th className="table-cell">Max Time</Table.th>
|
||||
<Table.th className="table-cell">Mean Time</Table.th>
|
||||
<Table.th className="table-cell">Min Time</Table.th>
|
||||
<Table.th className="table-cell">Total Time</Table.th>
|
||||
<Table.th className="table-cell">Query</Table.th>
|
||||
</>
|
||||
}
|
||||
body={
|
||||
!isLoading && mostFrequentlyInvoked ? (
|
||||
mostFrequentlyInvoked[0].data!.map((item, i) => {
|
||||
return (
|
||||
<Table.tr key={i} hoverable className="relative">
|
||||
<Table.td className="table-cell whitespace-nowrap w-28">
|
||||
{item.rolname}
|
||||
</Table.td>
|
||||
<Table.td className="table-cell whitespace-nowrap">
|
||||
{item.avg_rows}
|
||||
</Table.td>
|
||||
<Table.td className="table-cell whitespace-nowrap">
|
||||
{item.calls}
|
||||
</Table.td>
|
||||
<Table.td className="table-cell whitespace-nowrap">
|
||||
{item.max_time.toFixed(2)}ms
|
||||
</Table.td>
|
||||
<Table.td className="table-cell whitespace-nowrap truncate">
|
||||
{item.mean_time.toFixed(2)}ms
|
||||
</Table.td>
|
||||
<Table.td className="table-cell whitespace-nowrap truncate">
|
||||
{item.min_time.toFixed(2)}ms
|
||||
</Table.td>
|
||||
<Table.td className="table-cell whitespace-nowrap truncate">
|
||||
{item.total_time.toFixed(2)}ms
|
||||
</Table.td>
|
||||
<Table.td className="relative table-cell whitespace-nowrap w-24">
|
||||
<p className="w-48 block truncate font-mono ">{item.query}</p>
|
||||
<QueryActions
|
||||
sql={item.query}
|
||||
className="absolute inset-y-0 right-0"
|
||||
/>
|
||||
</Table.td>
|
||||
</Table.tr>
|
||||
)
|
||||
})
|
||||
) : (
|
||||
<></>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Tabs.Panel>
|
||||
<Tabs.Panel key={3} id="3" label="Slowest execution time">
|
||||
<div className={panelClassNames}>
|
||||
<ReactMarkdown
|
||||
className={helperTextClassNames}
|
||||
children={SlowestExecutionHelperText}
|
||||
/>
|
||||
<div className="thin-scrollbars max-w-full overflow-scroll">
|
||||
<Table
|
||||
head={
|
||||
<>
|
||||
<Table.th className="table-cell">Role</Table.th>
|
||||
<Table.th className="table-cell">Avg Rows</Table.th>
|
||||
<Table.th className="table-cell">Calls</Table.th>
|
||||
<Table.th className="table-cell">Max Time</Table.th>
|
||||
<Table.th className="table-cell">Mean Time</Table.th>
|
||||
<Table.th className="table-cell">Min Time</Table.th>
|
||||
<Table.th className="table-cell">Total Time</Table.th>
|
||||
<Table.th className="table-cell">Query</Table.th>
|
||||
</>
|
||||
}
|
||||
body={
|
||||
!isLoading && slowestExecutionTime ? (
|
||||
slowestExecutionTime[0].data!.map((item, i) => {
|
||||
return (
|
||||
<Table.tr key={i} hoverable className="relative">
|
||||
<Table.td className="table-cell whitespace-nowrap w-24">
|
||||
{item.rolname}
|
||||
</Table.td>
|
||||
<Table.td className="table-cell whitespace-nowrap">
|
||||
{item.avg_rows}
|
||||
</Table.td>
|
||||
<Table.td className="table-cell whitespace-nowrap">
|
||||
{item.calls}
|
||||
</Table.td>
|
||||
<Table.td className="table-cell whitespace-nowrap">
|
||||
{item.max_time.toFixed(2)}ms
|
||||
</Table.td>
|
||||
<Table.td className="table-cell whitespace-nowrap truncate">
|
||||
{item.mean_time.toFixed(2)}ms
|
||||
</Table.td>
|
||||
<Table.td className="table-cell whitespace-nowrap truncate">
|
||||
{item.min_time.toFixed(2)}ms
|
||||
</Table.td>
|
||||
<Table.td className="table-cell whitespace-nowrap truncate">
|
||||
{item.total_time.toFixed(2)}ms
|
||||
</Table.td>
|
||||
<Table.td className="relative table-cell whitespace-nowrap">
|
||||
<p className="w-48 block truncate font-mono">{item.query}</p>
|
||||
<QueryActions
|
||||
sql={'item.query'}
|
||||
className="absolute inset-y-0 right-0"
|
||||
/>
|
||||
</Table.td>
|
||||
</Table.tr>
|
||||
)
|
||||
})
|
||||
) : (
|
||||
<></>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Tabs.Panel>
|
||||
</Tabs>
|
||||
</div>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
const QueryActions = ({ sql, className }: { sql: string; className: string }) => {
|
||||
if (sql.includes('insufficient privilege')) return null
|
||||
|
||||
return (
|
||||
<div className={[className, 'flex justify-center items-center mr-4'].join(' ')}>
|
||||
<CopyButton type="default" text={sql} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
QueryPerformanceReport.getLayout = (page) => (
|
||||
<ReportsLayout title="Query performance">{page}</ReportsLayout>
|
||||
)
|
||||
|
||||
export default observer(QueryPerformanceReport)
|
||||
@@ -0,0 +1 @@
|
||||
<svg width="10" height="15" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M.494 7.974h5v2h-5v-2Z" fill="url(#a)"/><path d="M.494 9.974h5v2h-5v-2Z" fill="url(#b)"/><path fill-rule="evenodd" clip-rule="evenodd" d="M2.682 3a2.318 2.318 0 1 1 4.636 0v1.682h2.155L9.79 5v9l-.318.318h-9L.154 14V5l.319-.318h2.209V3Zm.636 1.682h3.364V3a1.682 1.682 0 0 0-3.364 0v1.682Zm-2.527.636v2.338h4.703l.319.318v4l-.319.319H.791v1.389h8.363V5.318H.791Zm.022 6.338h4.363v-1.363H.813v1.363Zm0-2h4.363V8.293H.813v1.363Z" fill="url(#c)"/><defs><linearGradient id="a" x1="-1.666" y1=".682" x2="13.393" y2=".682" gradientUnits="userSpaceOnUse"><stop stop-color="#429B72"/><stop offset="1" stop-color="#333" stop-opacity=".6"/></linearGradient><linearGradient id="b" x1="-1.666" y1=".682" x2="13.393" y2=".682" gradientUnits="userSpaceOnUse"><stop stop-color="#429B72"/><stop offset="1" stop-color="#333" stop-opacity=".6"/></linearGradient><linearGradient id="c" x1="-1.666" y1=".682" x2="13.393" y2=".682" gradientUnits="userSpaceOnUse"><stop stop-color="#429B72"/><stop offset="1" stop-color="#333" stop-opacity=".6"/></linearGradient></defs></svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="2202" height="2476"><defs><linearGradient id="b" x1="22.367%" x2="87.411%" y1="78.665%" y2="25.105%"><stop offset="0%" stop-color="#ADADAD" stop-opacity=".5"/><stop offset="100%" stop-opacity=".5"/></linearGradient><path id="a" d="M981.153 35.355c78.66-46.107 154.6-47.464 233.93-2.033 292.92 168.838 586.52 337.673 879.45 506.511 35.25 20.342 61.7 46.108 79.33 77.297l.546.938c17.94 31.001 26.574 67.962 26.574 108.909v1021.274c-.11 40.36-8.92 76.03-25.77 107.02-17.62 32.54-44.07 58.99-80.68 80.01-292.93 168.84-586.53 337-879.45 506.51-72.65 42.58-143.56 44.1-215.94 7.76-6.43-3.19-12.88-6.68-19.35-10.47-116.62-69.16-700.431-403.45-874.693-504.48-35.937-21.02-63.059-48.14-80.691-80.01l.756-.44C8.606 1823.221 0 1787.661 0 1747.461V727.656c0-82.723 34.58-146.462 106.456-187.823C280.039 438.801 864.523 103.841 981.153 35.355Zm117.98 470.575c-404.121 0-730.944 327.503-730.944 730.951 0 132.22 35.257 256.98 96.962 364.12 126.118 219.01 363.44 366.83 634.662 366.83 271.91 0 509.23-148.5 635.34-368.87l-318.01-183.75c-62.38 110.52-181.04 185.79-317.33 185.79-135.61 0-253.591-73.91-315.973-183.76-30.513-53.56-48.144-114.59-48.144-180.36 0-201.39 162.737-364.121 364.117-364.121 134.26 0 251.56 73.23 314.62 181.721l318.01-183.078c-126.79-218.333-362.76-365.473-633.31-365.473Zm720.1 558.041h-72.55v58.99h-58.31v72.56h58.31v87.47h-58.31v72.55h58.31v58.99h72.55l-.001-58.99h88.15l.001 58.99h72.55v-58.99h58.99v-72.55h-58.99v-87.47h58.99v-72.56h-58.99v-58.99h-72.55l-.001 58.99h-88.15l.001-58.99Zm88.149 131.55v87.47h-88.15v-87.47h88.15Z"/></defs><g fill="none" fill-rule="nonzero" transform="translate(.507 .109)"><use xlink:href="#a" fill="#4C4C4C"/><use xlink:href="#a" fill="url(#b)"/></g></svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="100%" height="100%" viewBox="0 0 2500 2500" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<path d="M174.672,1866.26L173.916,1866.7C191.548,1898.57 218.67,1925.69 254.607,1946.71C428.869,2047.74 1012.68,2382.03 1129.3,2451.19C1135.77,2454.98 1142.22,2458.47 1148.65,2461.66C1221.03,2498 1291.94,2496.48 1364.59,2453.9C1657.51,2284.39 1951.11,2116.23 2244.04,1947.39C2280.65,1926.37 2307.1,1899.92 2324.72,1867.38C2341.57,1836.39 2350.38,1800.72 2350.49,1760.36C2350.49,1759.87 2350.49,1759.38 2350.49,1758.89L2350.49,739.086C2350.49,697.725 2341.68,660.432 2323.37,629.239L2323.37,629.241L2323.37,629.239C2305.74,598.05 2279.29,572.284 2244.04,551.942C1951.11,383.104 1657.51,214.269 1364.59,45.431C1285.26,0 1209.32,1.357 1130.66,47.464C1014.03,115.95 429.546,450.91 255.963,551.942C184.087,593.303 149.507,657.042 149.507,739.765L149.507,1759.57C149.507,1799.77 158.113,1835.33 174.672,1866.26Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
<path d="M1566.65,1427.32C1504.27,1537.84 1385.61,1613.11 1249.32,1613.11C1113.71,1613.11 995.729,1539.2 933.347,1429.35C902.834,1375.79 885.203,1314.76 885.203,1248.99C885.203,1047.6 1047.94,884.869 1249.32,884.869C1383.58,884.869 1500.88,958.099 1563.94,1066.59L1881.95,883.512C1755.16,665.179 1519.19,518.039 1248.64,518.039C844.519,518.039 517.696,845.542 517.696,1248.99C517.696,1381.21 552.953,1505.97 614.658,1613.11C740.776,1832.12 978.098,1979.94 1249.32,1979.94C1521.23,1979.94 1758.55,1831.44 1884.66,1611.07L1566.65,1427.32ZM1968.74,1076.08L1896.19,1076.08L1896.19,1426.64L1968.74,1426.64L1968.74,1076.08ZM2129.44,1076.08L2056.89,1076.08L2056.89,1426.64L2129.44,1426.64L2129.44,1076.08Z" style="fill-rule:nonzero;"/>
|
||||
<g>
|
||||
<path d="M2188.43,1135.07L1837.88,1135.07L1837.88,1207.63L2188.43,1207.63L2188.43,1135.07ZM2188.43,1295.1L1837.88,1295.1L1837.88,1367.65L2188.43,1367.65L2188.43,1295.1Z" style="fill-rule:nonzero;"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
@@ -0,0 +1,9 @@
|
||||
<svg width="81" height="80" viewBox="0 0 81 80" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M62.8209 10.9141L44.7971 10.9339L15.7344 39.9967L24.701 48.9743L32.5819 41.1376L62.8209 10.9141ZM45.3142 37.7255C45.0585 37.7136 44.7948 37.7015 44.6294 37.92L29.1317 53.4154L38.0337 62.2629L38.0234 62.273L44.394 68.649C44.4358 68.6861 44.4773 68.7276 44.5196 68.7698C44.6939 68.944 44.881 69.1309 45.151 69.0705C48.0938 69.0639 51.0367 69.065 53.9798 69.0661C56.9231 69.0672 59.8666 69.0683 62.8106 69.0617L47.1433 53.3803L62.8055 37.728L45.6203 37.7235C45.5247 37.7352 45.4201 37.7303 45.3142 37.7255Z" fill="url(#paint0_linear_1785_15060)"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_1785_15060" x1="15.7344" y1="69.0821" x2="72.627" y2="23.028" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#171717" stop-opacity="0.5"/>
|
||||
<stop offset="1"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 925 B |
@@ -0,0 +1,9 @@
|
||||
<svg width="81" height="80" viewBox="0 0 81 80" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M62.8209 10.9141L44.7971 10.9339L15.7344 39.9967L24.701 48.9743L32.5819 41.1376L62.8209 10.9141ZM45.3142 37.7255C45.0585 37.7136 44.7948 37.7015 44.6294 37.92L29.1317 53.4154L38.0337 62.2629L38.0234 62.273L44.394 68.649C44.4358 68.6861 44.4773 68.7276 44.5196 68.7698C44.6939 68.944 44.881 69.1309 45.151 69.0705C48.0938 69.0639 51.0367 69.065 53.9798 69.0661C56.9231 69.0672 59.8666 69.0683 62.8106 69.0617L47.1433 53.3803L62.8055 37.728L45.6203 37.7235C45.5247 37.7352 45.4201 37.7303 45.3142 37.7255Z" fill="url(#paint0_linear_1785_15060)"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_1785_15060" x1="15.7344" y1="69.0821" x2="72.627" y2="23.028" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#EDEDED" stop-opacity="0.5"/>
|
||||
<stop offset="1" stop-color="white"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 944 B |
@@ -0,0 +1,9 @@
|
||||
<svg width="81" height="80" viewBox="0 0 81 80" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.7305 13.2305H67.2695V66.7695H13.7305V13.2305ZM54.6588 58.1144C52.1773 58.1144 50.7743 56.8201 49.6959 55.0593L45.6073 57.4345C47.0843 60.3528 50.1029 62.5794 54.7753 62.5794C59.5538 62.5794 63.1121 60.0979 63.1121 55.5683C63.1121 51.3668 60.6986 49.498 56.4239 47.6649L55.1662 47.1261C53.0076 46.1913 52.0728 45.5803 52.0728 44.071C52.0728 42.8498 53.0068 41.915 54.4804 41.915C55.9251 41.915 56.8556 42.5243 57.7182 44.071L61.6359 41.5555C59.9788 38.6406 57.6791 37.5273 54.4804 37.5273C49.9873 37.5273 47.1124 40.3998 47.1124 44.173C47.1124 48.2691 49.5242 50.2068 53.1546 51.7534L54.4124 52.2931C56.7069 53.2967 58.0751 53.9077 58.0751 55.6329C58.0751 57.0725 56.7435 58.1144 54.6588 58.1144ZM35.1613 58.0829C33.4327 58.0829 32.7138 56.8974 31.9234 55.4952L27.8281 57.9746C29.0145 60.4854 31.3473 62.57 35.3754 62.57C39.8336 62.57 42.8879 60.199 42.8879 54.9896V37.8146H37.8569V54.9216C37.8569 57.4362 36.8142 58.0829 35.1613 58.0829Z" fill="url(#paint0_linear_1785_15055)"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_1785_15055" x1="13.7305" y1="66.7695" x2="67.2695" y2="13.2305" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#171717" stop-opacity="0.5"/>
|
||||
<stop offset="1"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1,9 @@
|
||||
<svg width="81" height="80" viewBox="0 0 81 80" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.7305 13.2305H67.2695V66.7695H13.7305V13.2305ZM54.6588 58.1144C52.1773 58.1144 50.7743 56.8201 49.6959 55.0593L45.6073 57.4345C47.0843 60.3528 50.1029 62.5794 54.7753 62.5794C59.5538 62.5794 63.1121 60.0979 63.1121 55.5683C63.1121 51.3668 60.6986 49.498 56.4239 47.6649L55.1662 47.1261C53.0076 46.1913 52.0728 45.5803 52.0728 44.071C52.0728 42.8498 53.0068 41.915 54.4804 41.915C55.9251 41.915 56.8556 42.5243 57.7182 44.071L61.6359 41.5555C59.9788 38.6406 57.6791 37.5273 54.4804 37.5273C49.9873 37.5273 47.1124 40.3998 47.1124 44.173C47.1124 48.2691 49.5242 50.2068 53.1546 51.7534L54.4124 52.2931C56.7069 53.2967 58.0751 53.9077 58.0751 55.6329C58.0751 57.0725 56.7435 58.1144 54.6588 58.1144ZM35.1613 58.0829C33.4327 58.0829 32.7138 56.8974 31.9234 55.4952L27.8281 57.9746C29.0145 60.4854 31.3473 62.57 35.3754 62.57C39.8336 62.57 42.8879 60.199 42.8879 54.9896V37.8146H37.8569V54.9216C37.8569 57.4362 36.8142 58.0829 35.1613 58.0829Z" fill="url(#paint0_linear_1785_15055)"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_1785_15055" x1="13.7305" y1="66.7695" x2="67.2695" y2="13.2305" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#EDEDED" stop-opacity="0.5"/>
|
||||
<stop offset="1" stop-color="white"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
@@ -0,0 +1,9 @@
|
||||
<svg width="80" height="80" viewBox="0 0 80 80" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M32.7668 10.4216C34.8343 10.0558 37.1841 9.84591 39.6447 9.83448C42.1052 9.82305 44.6707 10.01 47.1433 10.4216C51.0493 11.0726 54.3399 14.0028 54.3399 17.9034V31.6089C54.3399 35.628 51.1452 38.923 47.1433 38.923H32.7668C27.8856 38.923 23.7752 43.1136 23.7752 47.8643V54.4403H18.8264C14.6433 54.4403 12.2 51.4041 11.1768 47.143C9.79667 41.4184 9.85529 37.9973 11.1768 32.5148C12.3225 27.7318 15.9853 25.2175 20.1684 25.2175H25.5701H39.9634V23.389H25.5701V17.9034C25.5701 13.7499 26.676 11.4977 32.7668 10.4216ZM34.5617 16.9976C34.5617 15.4804 33.3476 14.2464 31.8609 14.2464C30.3688 14.2464 29.1601 15.4804 29.1601 16.9976C29.1601 18.5094 30.3688 19.732 31.8609 19.732C33.3476 19.732 34.5617 18.5094 34.5617 16.9976ZM56.1348 31.6092V25.2178H61.5365C65.7249 25.2178 67.6994 28.3508 68.7332 32.5151C70.1719 38.2988 70.2359 42.6346 68.7332 47.1432C67.2784 51.5227 65.7196 54.4405 61.5365 54.4405H54.3399H39.9634V56.269H54.3399V61.7546C54.3399 65.9081 50.7668 68.0195 47.1432 69.0687C41.6919 70.6505 37.3228 70.4083 32.7667 69.0687C28.962 67.9496 25.5701 65.6553 25.5701 61.7546V48.0491C25.5701 44.1054 28.8287 40.735 32.7667 40.735H47.1432C51.9338 40.735 56.1348 36.5644 56.1348 31.6092ZM50.7499 62.6605C50.7499 61.1486 49.5412 59.9261 48.0491 59.9261C46.5624 59.9261 45.3483 61.1486 45.3483 62.6605C45.3483 64.1777 46.5624 65.4116 48.0491 65.4116C49.5412 65.4116 50.7499 64.1777 50.7499 62.6605Z" fill="url(#paint0_linear_1785_15271)"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_1785_15271" x1="10.1636" y1="70.1661" x2="70.492" y2="10.4969" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#171717" stop-opacity="0.5"/>
|
||||
<stop offset="1"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
@@ -0,0 +1,9 @@
|
||||
<svg width="80" height="80" viewBox="0 0 80 80" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M32.7668 10.4216C34.8343 10.0558 37.1841 9.84591 39.6447 9.83448C42.1052 9.82305 44.6707 10.01 47.1433 10.4216C51.0493 11.0726 54.3399 14.0028 54.3399 17.9034V31.6089C54.3399 35.628 51.1452 38.923 47.1433 38.923H32.7668C27.8856 38.923 23.7752 43.1136 23.7752 47.8643V54.4403H18.8264C14.6433 54.4403 12.2 51.4041 11.1768 47.143C9.79667 41.4184 9.85529 37.9973 11.1768 32.5148C12.3225 27.7318 15.9853 25.2175 20.1684 25.2175H25.5701H39.9634V23.389H25.5701V17.9034C25.5701 13.7499 26.676 11.4977 32.7668 10.4216ZM34.5617 16.9976C34.5617 15.4804 33.3476 14.2464 31.8609 14.2464C30.3688 14.2464 29.1601 15.4804 29.1601 16.9976C29.1601 18.5094 30.3688 19.732 31.8609 19.732C33.3476 19.732 34.5617 18.5094 34.5617 16.9976ZM56.1348 31.6092V25.2178H61.5365C65.7249 25.2178 67.6994 28.3508 68.7332 32.5151C70.1719 38.2988 70.2359 42.6346 68.7332 47.1432C67.2784 51.5227 65.7196 54.4405 61.5365 54.4405H54.3399H39.9634V56.269H54.3399V61.7546C54.3399 65.9081 50.7668 68.0195 47.1432 69.0687C41.6919 70.6505 37.3228 70.4083 32.7667 69.0687C28.962 67.9496 25.5701 65.6553 25.5701 61.7546V48.0491C25.5701 44.1054 28.8287 40.735 32.7667 40.735H47.1432C51.9338 40.735 56.1348 36.5644 56.1348 31.6092ZM50.7499 62.6605C50.7499 61.1486 49.5412 59.9261 48.0491 59.9261C46.5624 59.9261 45.3483 61.1486 45.3483 62.6605C45.3483 64.1777 46.5624 65.4116 48.0491 65.4116C49.5412 65.4116 50.7499 64.1777 50.7499 62.6605Z" fill="url(#paint0_linear_1785_15271)"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_1785_15271" x1="10.1636" y1="70.1661" x2="70.492" y2="10.4969" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#EDEDED" stop-opacity="0.5"/>
|
||||
<stop offset="1" stop-color="white"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |