Merge branch 'master' into patch-3

This commit is contained in:
Kang Ming authored and GitHub committed 2023-09-18 15:17:00 -07:00
commit 8684b32ea3
90 files changed
+2892 -1974

No files matched your search

+36 -2
View File
@@ -116,11 +116,45 @@ Now when you run a local development docs server you will see the new docs site.
---
## Running Docker
To test your changes, you need to run Supabase using Docker. Docker lets you use features of your locally modified Supabase version.
#### Prerequsites
First, make sure you have the Docker application installed on your device. You can download and install it from [here](https://docs.docker.com/get-docker/).
#### Getting Started
1. Navigate to the `docker` directory in your forked repo
```sh
cd docker
```
2. Copy the example `env` file
```sh
cp .env.example .env
```
3. Run docker
```sh
docker-compose up
```
This command initializes the containers specified in the docker-compose.yml file. It might take a few moments to complete, depending on your computer and internet connection.
Once the `docker-compose up` process completes, you should have your local version of Supabase up and running within Docker containers. You can access and test the features you've modified or added.
Remember to keep the Docker application open as long as you're working with your local Supabase instance.
## Create a pull request
After making your changes, open a pull request (PR). Once you submit your pull request, others from the Supabase team/community will review it with you.
After making your changes, open a pull request. Once you submit your pull request, the Supabase team will review it with you.
If you have an issue, like a merge conflict, or don't know how to open a pull request then check out [GitHub's pull request](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests) tutorial on how to resolve merge conflicts and other issues. Once your PR has been merged, you will be proudly listed as a contributor in the [contributor chart](https://github.com/supabase/supabase/graphs/contributors).
Once your PR has been merged, you will be proudly listed as a contributor in the [contributor chart](https://github.com/supabase/supabase/graphs/contributors).
---
+4 -4
View File
@@ -63,7 +63,7 @@ const HomePageCover = (props) => {
const GettingStarted = () => (
<div
className="
border border-scale-400 bg-scale-200
border bg-scale-200
relative overflow-hidden
grid grid-cols-12
rounded-lg
@@ -71,7 +71,7 @@ const HomePageCover = (props) => {
"
>
<div className="col-span-full flex flex-col md:flex-row xl:flex-col justify-between gap-1 md:gap-3">
<div className="md:max-w-xs xl:max-w-none">
<div className="md:max-w-xs shrink w-fit xl:max-w-none">
<div className="flex items-center gap-3 mb-3">
<IconBackground>
<IconPlay className="text-brand-600 dark:text-brand w-4" strokeWidth={2} />
@@ -82,7 +82,7 @@ const HomePageCover = (props) => {
Discover how to set up a database to an app making queries in just a few minutes.
</p>
</div>
<div className="flex flex-wrap md:grid md:grid-cols-4 2xl:grid-cols-8 gap-2 sm:gap-3">
<div className="flex shrink-0 flex-wrap md:grid md:grid-cols-5 gap-2 sm:gap-3">
{frameworks.map((framework, i) => (
<Link key={i} href={framework.href} passHref>
<a className="no-underline">
@@ -113,7 +113,7 @@ const HomePageCover = (props) => {
</p>
</div>
</div>
<div className="w-full xl:max-w-[375px] 2xl:max-w-[620px] -mb-40">
<div className="w-full xl:max-w-[440px] -mb-40">
<GettingStarted />
</div>
</div>
@@ -464,6 +464,8 @@ export const actions = {
```js src/routes/login/+page.server.ts
// src/routes/login/+page.server.ts
import { fail } from '@sveltejs/kit'
export const actions = {
default: async ({ request, url, locals: { supabase } }) => {
const formData = await request.formData()
+5 -7
View File
@@ -7,17 +7,15 @@ import Head from 'next/head'
import { CodeBlock } from 'ui'
// Parameters are grouped on the page by tag
const TAGS = ['General', 'Auth', 'API', 'Database', 'Dashboard', 'Local', 'Edge-Functions']
const tocList = []
const content = TAGS.map((tag) => {
tocList.push({ text: tag, link: `${tag.toLowerCase()}-config`, level: 2 })
const content = specFile.info.tags.map((tag) => {
tocList.push({ text: tag.title, link: `${tag.id}-config`, level: 2 })
return (
<div>
<Heading tag="h2">{tag} Config</Heading>
<Heading tag="h2">{tag.title} Config</Heading>
{specFile.parameters
.filter((param: Parameter) => param.tags[0] === tag.toLowerCase())
.map((parameter: Parameter, index) => {
.filter((param: Parameter) => param.tags[0] === tag.id)
.map((parameter: Parameter) => {
tocList.push({ text: parameter.id, link: `#${parameter.id}`, level: 3 })
return <Info parameter={parameter} />
})}
+8 -2
View File
@@ -85,9 +85,15 @@ The Logs tab displays logs emitted during function execution.
## Logging Postgres Queries
By default, query logs are disabled for new Supabase projects, as they can reveal metadata about the contents of your database (such as table and column names).
By default, only DDL statements are logged for new Supabase projects.
To enable query logs:
<Admonition type="note">
For projects created before 18th July 2023, statement execution logging is disabled completely by default.
</Admonition>
To enable query logs for other categories of statements:
1. [Enable the pgAudit extension](https://supabase.com/dashboard/project/_/database/extensions).
2. [Restart your project](https://supabase.com/dashboard/project/_/settings/general) using the **Fast database reboot** option.
+1 -1
View File
@@ -26,7 +26,7 @@ export const official = [
{
name: 'Docker',
description: 'Deploy Supabase within your own infrastructure using Docker Compose.',
href: '/docs/guides/self-hosting/docker',
href: '/guides/self-hosting/docker',
},
{
name: 'BYO Cloud',
-5
View File
@@ -9,7 +9,6 @@ import {
IconPlay,
TextLink,
} from 'ui'
import { LWAnnouncement } from 'ui'
import HomeMenuIconPicker from '~/components/Navigation/NavigationMenu/HomeMenuIconPicker'
@@ -21,10 +20,6 @@ export const meta = {
}
<div className="flex flex-col">
<div className="mt-8 sm:mt-0 md:mt-4 xl:mt-16">
<LWAnnouncement />
</div>
<div>
<div className="max-w-xl">
## Products
+1
View File
@@ -60,6 +60,7 @@ Sreyas Udayavarman
Stanislav M
Steve Chavez
Stojan Dimitrovski
Supun Sudaraka Kalidasa
Terry Sutton
Thor Schaeff
Tyler Shukert
+61 -54
View File
@@ -13,7 +13,7 @@ import Developers from '~/components/Nav/Developers'
import ScrollProgress from '~/components/ScrollProgress'
import { useIsLoggedIn, useTheme } from 'common'
import { useIsLoggedIn, useIsUserLoading, useTheme } from 'common'
import TextLink from '../TextLink'
import Image from 'next/image'
import * as supabaseLogoWordmarkDark from 'common/assets/images/supabase-logo-wordmark--dark.png'
@@ -26,6 +26,7 @@ const Nav = () => {
const [openProduct, setOpenProduct] = useState(false)
const [openDevelopers, setOpenDevelopers] = useState(false)
const isLoggedIn = useIsLoggedIn()
const isUserLoading = useIsUserLoading()
const isHomePage = router.pathname === '/'
const isLaunchWeekPage = router.pathname.includes('launch-week')
@@ -290,60 +291,66 @@ const Nav = () => {
</div>
</div>
<div className="flex items-center gap-2">
<a href="https://github.com/supabase/supabase" target="_blank">
<Button
className="hidden group lg:flex"
type="text"
icon={
<div className="flex items-center justify-center w-4 h-4 text-brand-300">
<div
className={[
`text-scale-900 flex h-3 w-3 items-center justify-center
transition-all
group-hover:h-4
group-hover:w-4
group-hover:text-yellow-900
group-focus:h-4
group-focus:w-4
group-focus:text-yellow-900`,
showLaunchWeekNavMode && '!text-white',
].join(' ')}
>
<IconStar strokeWidth={2} />
</div>
</div>
}
>
<span
// className={isLaunchWeekPage ? '!text-white' : ''}
>
Star us on GitHub
</span>
</Button>
</a>
{isLoggedIn ? (
<Link href="/dashboard/projects">
<a>
<Button className="hidden text-white lg:block">Dashboard</Button>
</a>
</Link>
) : (
{!isUserLoading && (
<>
<Link href="https://supabase.com/dashboard">
<a>
<Button type="default" className="hidden lg:block">
Sign in
</Button>
</a>
</Link>
<Link href="https://supabase.com/dashboard">
<a>
<Button className="hidden text-white lg:block">Start your project</Button>
</a>
</Link>
<a href="https://github.com/supabase/supabase" target="_blank">
<Button
className="hidden group lg:flex"
type="text"
icon={
<div className="flex items-center justify-center w-4 h-4 text-brand-300">
<div
className={[
`text-scale-900 flex h-3 w-3 items-center justify-center
transition-all
group-hover:h-4
group-hover:w-4
group-hover:text-yellow-900
group-focus:h-4
group-focus:w-4
group-focus:text-yellow-900`,
showLaunchWeekNavMode && '!text-white',
].join(' ')}
>
<IconStar strokeWidth={2} />
</div>
</div>
}
>
<span
// className={isLaunchWeekPage ? '!text-white' : ''}
>
Star us on GitHub
</span>
</Button>
</a>
{isLoggedIn ? (
<Link href="/dashboard/projects">
<a>
<Button className="hidden text-white lg:block">Dashboard</Button>
</a>
</Link>
) : (
<>
<Link href="https://supabase.com/dashboard">
<a>
<Button type="default" className="hidden lg:block">
Sign in
</Button>
</a>
</Link>
<Link href="https://supabase.com/dashboard">
<a>
<Button className="hidden text-white lg:block">
Start your project
</Button>
</a>
</Link>
</>
)}
</>
)}
</div>
+1 -1
View File
@@ -153,7 +153,7 @@ transforms:
if .metadata.parsed.error_severity == "info" {
.metadata.parsed.error_severity = "log"
}
.metadata.parsed.error_severity = upcase(.metadata.parsed.error_severity)
.metadata.parsed.error_severity = upcase!(.metadata.parsed.error_severity)
sinks:
logflare_auth:
+2
View File
@@ -103,6 +103,8 @@ export const useSession = () => useAuth().session
export const useUser = () => useSession()?.user ?? null
export const useIsUserLoading = () => useAuth().isLoading
export const useIsLoggedIn = () => {
const user = useUser()
+3 -1
View File
@@ -63,7 +63,9 @@ function Toggle({
}, [])
function handleBlurEvent(e: React.FocusEvent<HTMLButtonElement>) {
if (handleBlur) handleBlur(e)
setTimeout(() => {
if (handleBlur) handleBlur(e)
}, 100)
if (onBlur) onBlur(e)
}
+424 -40
View File
@@ -7,37 +7,46 @@ configspec: '001'
# This section outlines the general information for the tool.
info:
id: 'cli' # {string} A unique ID for this tool.
version: 'next' # {string} The current version number of the tool.
version: '1.93.0' # {string} The current version number of the tool.
title: 'CLI' # {string} A readable name.
source: 'https://github.com/supabase/cli' # {string} Where developers can find the source code.
bugs: 'https://github.com/supabase/cli/issues' # {string} Where developers can file bugs.
spec: 'https://github.com/supabase/supabase/blob/master/web/spec/cli_config_001.yml' # {string} Where developers can find this spec (to link directly in the docs).
spec: 'https://github.com/supabase/supabase/spec/cli_v1_config.yaml' # {string} Where developers can find this spec (to link directly in the docs).
description: |
A `config.toml` file is generated after running `supabase init`.
A `supabase/config.toml` file is generated after running `supabase init`.
This file is located in the `supabase` folder under `supabase/config.toml`. You can edit this file to change the settings for your locally running project. After you make changes, you will need to restart using `supabase stop` and then `supabase start` for the changes to take effect.
You can edit this file to change the settings for your locally running project. After you make changes, you will need to restart using `supabase stop` and then `supabase start` for the changes to take effect.
tags:
- id: general
title: General
description: General settings.
- id: auth
title: Auth Settings
description: Auth Settings.
title: Auth
description: Auth settings.
- id: api
title: API Settings
description: Auth Settings.
title: API
description: Auth settings.
- id: database
title: Database Settings
description: Database Settings.
title: Database
description: Database settings.
- id: dashboard
title: Dashboard Settings
description: Dashboard Settings.
title: Dashboard
description: Dashboard settings.
- id: realtime
title: Realtime
description: Dashboard settings.
- id: storage
title: Storage
description: Storage settings.
- id: edge-functions
title: Edge-Functions
description: Edge-Functions settings.
- id: analytics
title: Analytics
description: Analytics settings.
- id: local
title: Local Development
description: Local Development.
- id: edge-functions
title: Edge Functions
description: Edge Functions.
description: Local Development settings.
# This section is an array of public functions which a user might need to execute.
parameters:
@@ -48,10 +57,22 @@ parameters:
description: |
A string used to distinguish different Supabase projects on the same host. Defaults to the working directory name when running `supabase init`.
- id: 'api.enabled'
title: 'api.enabled'
tags: ['api']
required: false
default: 'true'
description: |
Enable the local PostgREST service.
links:
- name: 'PostgREST configuration'
link: 'https://postgrest.org/en/stable/configuration.html'
- id: 'api.port'
title: 'api.port'
tags: ['api']
required: true
required: false
default: '54321'
description: |
Port to use for the API URL.
links:
@@ -67,8 +88,7 @@ parameters:
required: false
default: '["public", "storage", "graphql_public"]'
description: |
Schemas to expose in your API. Tables, views and functions in this schema
will get API endpoints. `public` and `storage` are always included.
Schemas to expose in your API. Tables, views and functions in this schema will get API endpoints. `public` and `storage` are always included.
links:
- name: 'PostgREST configuration'
link: 'https://postgrest.org/en/stable/configuration.html'
@@ -77,10 +97,9 @@ parameters:
title: 'api.extra_search_path'
tags: ['api']
required: false
default: '["public"]'
default: '["public", "extensions"]'
description: |
Extra schemas to add to the search_path of every request. public is always
included.
Extra schemas to add to the search_path of every request. public is always included.
links:
- name: 'PostgREST configuration'
link: 'https://postgrest.org/en/stable/configuration.html'
@@ -99,7 +118,8 @@ parameters:
- id: 'db.port'
title: 'db.port'
tags: ['database']
required: true
required: false
default: '54322'
description: |
Port to use for the local database URL.
links:
@@ -118,26 +138,130 @@ parameters:
- id: 'db.major_version'
title: 'db.major_version'
tags: ['database']
required: true
default: '14'
required: false
default: '15'
description: |
The database major version to use. This has to be the same as your remote database's. Run `SHOW server_version;` on the remote database to check.
links:
- name: 'PostgreSQL configuration'
link: 'https://postgrest.org/en/stable/configuration.html'
- id: 'db.pooler.enabled'
title: 'db.pooler.enabled'
tags: ['database']
required: false
default: 'false'
description: |
Enable the local PgBouncer service.
links:
- name: 'PgBouncer Configuration'
link: https://www.pgbouncer.org/config.html
- id: 'db.pooler.port'
title: 'db.pooler.port'
tags: ['database']
required: false
default: '54329'
description: |
Port to use for the local connection pooler.
links:
- name: 'PgBouncer Configuration'
link: https://www.pgbouncer.org/config.html#listen_port
- id: 'db.pooler.pool_mode'
title: 'db.pooler.pool_mode'
tags: ['database']
required: false
default: '"transaction"'
description: |
Specifies when a server connection can be reused by other clients. Configure one of the supported pooler modes: `transaction`, `session`.
links:
- name: 'PgBouncer Configuration'
link: https://www.pgbouncer.org/config.html#pool_mode
- id: 'db.pooler.default_pool_size'
title: 'db.pooler.default_pool_size'
tags: ['database']
required: false
default: '20'
description: |
How many server connections to allow per user/database pair.
links:
- name: 'PgBouncer Configuration'
link: https://www.pgbouncer.org/config.html#default_pool_size
- id: 'db.pooler.max_client_conn'
title: 'db.pooler.max_client_conn'
tags: ['database']
required: false
default: '100'
description: |
Maximum number of client connections allowed.
links:
- name: 'PgBouncer Configuration'
link: https://www.pgbouncer.org/config.html#max_client_conn
- id: 'realtime.enabled'
title: 'realtime.enabled'
tags: ['realtime']
required: false
default: 'true'
description: |
Enable the local Realtime service.
links: []
- id: 'realtime.ip_version'
title: 'realtime.ip_version'
tags: ['realtime']
required: false
default: '"IPv6"'
description: |
Bind realtime via either IPv4 or IPv6. (default: IPv6)
links: []
- id: 'studio.enabled'
title: 'studio.enabled'
tags: ['dashboard']
required: false
default: 'true'
description: |
Enable the local Supabase Studio dashboard.
links: []
- id: 'studio.port'
title: 'studio.port'
tags: ['dashboard']
required: true
required: false
default: '54323'
description: |
Port to use for Supabase Studio.
links: []
- id: 'studio.api_url'
title: 'studio.api_url'
tags: ['dashboard']
required: false
default: '"http://localhost"'
description: |
External URL of the API server that frontend connects to.
links: []
- id: 'inbucket.enabled'
title: 'inbucket.enabled'
tags: ['local']
required: false
default: 'true'
description: |
Enable the local InBucket service.
links:
- name: 'Inbucket documentation'
link: 'https://www.inbucket.org'
- id: 'inbucket.port'
title: 'inbucket.port'
tags: ['local']
required: true
required: false
default: 'true'
description: |
Port to use for the email testing server web interface.
@@ -150,6 +274,7 @@ parameters:
title: 'inbucket.smtp_port'
tags: ['local']
required: false
default: '54325'
description: |
Port to use for the email testing server SMTP port.
@@ -164,6 +289,7 @@ parameters:
title: 'inbucket.pop3_port'
tags: ['local']
required: false
default: '54326'
description: |
Port to use for the email testing server POP3 port.
@@ -174,20 +300,43 @@ parameters:
- name: 'Inbucket documentation'
link: 'https://www.inbucket.org'
- id: 'storage.enabled'
title: 'storage.enabled'
tags: ['storage']
required: false
default: 'true'
description: |
Enable the local Storage service.
links:
- name: 'Storage server configuration'
link: 'https://supabase.com/docs/guides/self-hosting/storage/config'
- id: 'storage.file_size_limit'
title: 'storage.file_size_limit'
tags: ['local']
tags: ['storage']
required: false
default: '"50MiB"'
description: |
The maximum file size allowed (e.g. "5MB", "500KB").
links:
- name: 'Storage server configuration'
link: 'https://supabase.com/docs/guides/self-hosting/storage/config'
- id: 'auth.enabled'
title: 'auth.enabled'
tags: ['auth']
required: false
default: 'true'
description: |
Enable the local GoTrue service.
links:
- name: 'Auth Server configuration'
link: 'https://supabase.com/docs/reference/auth'
- id: 'auth.site_url'
title: 'auth.site_url'
tags: ['auth']
required: true
required: false
default: '"http://localhost:3000"'
description: |
The base URL of your website. Used as an allow-list for redirects and for constructing URLs used in emails.
@@ -198,7 +347,7 @@ parameters:
- id: 'auth.additional_redirect_urls'
title: 'auth.additional_redirect_urls'
tags: ['auth']
required: true
required: false
default: '["https://localhost:3000"]'
description: |
A list of _exact_ URLs that auth providers are permitted to redirect to post authentication.
@@ -209,7 +358,7 @@ parameters:
- id: 'auth.jwt_expiry'
title: 'auth.jwt_expiry'
tags: ['auth']
required: true
required: false
default: '3600'
description: |
How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 seconds (one week).
@@ -217,10 +366,32 @@ parameters:
- name: 'Auth Server configuration'
link: 'https://supabase.com/docs/reference/auth'
- id: 'auth.enable_refresh_token_rotation'
title: 'auth.enable_refresh_token_rotation'
tags: ['auth']
required: false
default: 'true'
description: |
If disabled, the refresh token will never expire.
links:
- name: 'Auth Server configuration'
link: 'https://supabase.com/docs/reference/auth'
- id: 'auth.refresh_token_reuse_interval'
title: 'auth.refresh_token_reuse_interval'
tags: ['auth']
required: false
default: '10'
description: |
Allows refresh tokens to be reused after expiry, up to the specified interval in seconds. Requires enable_refresh_token_rotation = true.
links:
- name: 'Auth Server configuration'
link: 'https://supabase.com/docs/reference/auth'
- id: 'auth.enable_signup'
title: 'auth.enable_signup'
tags: ['auth']
required: true
required: false
default: 'true'
description: |
Allow/disallow new user signups to your project.
@@ -231,7 +402,7 @@ parameters:
- id: 'auth.email.enable_signup'
title: 'auth.email.enable_signup'
tags: ['auth']
required: true
required: false
default: 'true'
description: |
Allow/disallow new user signups via email to your project.
@@ -242,7 +413,7 @@ parameters:
- id: 'auth.email.double_confirm_changes'
title: 'auth.email.double_confirm_changes'
tags: ['auth']
required: true
required: false
default: 'true'
description: |
If enabled, a user will be required to confirm any email change on both the old, and new email addresses. If disabled, only the new email is required to confirm.
@@ -253,8 +424,8 @@ parameters:
- id: 'auth.email.enable_confirmations'
title: 'auth.email.enable_confirmations'
tags: ['auth']
required: true
default: 'true'
required: false
default: 'false'
description: |
If enabled, users need to confirm their email address before signing in.
links:
@@ -293,11 +464,175 @@ parameters:
- name: 'Auth Server configuration'
link: 'https://supabase.com/docs/reference/auth'
- id: 'auth.sms.enable_signup'
title: 'auth.sms.enable_signup'
tags: ['auth']
required: false
default: 'true'
description: |
Allow/disallow new user signups via SMS to your project.
links:
- name: 'Auth Server configuration'
link: 'https://supabase.com/docs/reference/auth'
- id: 'auth.sms.enable_confirmations'
title: 'auth.sms.enable_confirmations'
tags: ['auth']
required: false
default: 'false'
description: |
If enabled, users need to confirm their phone number before signing in.
links:
- name: 'Auth Server configuration'
link: 'https://supabase.com/docs/reference/auth'
- id: 'auth.sms.test_otp'
title: 'auth.sms.test_otp'
tags: ['auth']
required: false
description: |
Use pre-defined map of phone number to OTP for testing.
links:
- name: 'Auth Server configuration'
link: 'https://supabase.com/docs/reference/auth'
usage: |
[auth.sms.test_otp]
4152127777 = "123456"
- id: 'auth.sms.provider.enabled'
title: 'auth.sms.<provider>.enabled'
tags: ['auth']
required: false
default: 'false'
description: |
Use an external SMS provider. The full list of providers are:
- `twilio`
- `twilio_verify`
- `messagebird`
- `textlocal`
- `vonage`
links:
- name: 'Auth Server configuration'
link: 'https://supabase.com/docs/reference/auth'
- id: 'auth.sms.twilio.account_sid'
title: 'auth.sms.<twilio|twilio_verify>.account_sid'
tags: ['auth']
required: true
description: |
Twilio Account SID
links:
- name: 'Auth Server configuration'
link: 'https://supabase.com/docs/reference/auth'
- id: 'auth.sms.twilio.message_service_sid'
title: 'auth.sms.<twilio|twilio_verify>.message_service_sid'
tags: ['auth']
required: true
description: |
Twilio Message Service SID
links:
- name: 'Auth Server configuration'
link: 'https://supabase.com/docs/reference/auth'
- id: 'auth.sms.twilio.auth_token'
title: 'auth.sms.<twilio|twilio_verify>.auth_token'
tags: ['auth']
required: true
default: 'env(SUPABASE_AUTH_SMS_TWILIO_AUTH_TOKEN)'
description: |
Twilio Auth Token
DO NOT commit your Twilio auth token to git. Use environment variable substitution instead.
links:
- name: 'Auth Server configuration'
link: 'https://supabase.com/docs/reference/auth'
- id: 'auth.sms.messagebird.originator'
title: 'auth.sms.messagebird.originator'
tags: ['auth']
required: true
description: |
MessageBird Originator
links:
- name: 'Auth Server configuration'
link: 'https://supabase.com/docs/reference/auth'
- id: 'auth.sms.messagebird.access_key'
title: 'auth.sms.messagebird.access_key'
tags: ['auth']
required: true
default: 'env(SUPABASE_AUTH_SMS_MESSAGEBIRD_ACCESS_KEY)'
description: |
MessageBird Access Key
DO NOT commit your MessageBird access key to git. Use environment variable substitution instead.
links:
- name: 'Auth Server configuration'
link: 'https://supabase.com/docs/reference/auth'
- id: 'auth.sms.textlocal.sender'
title: 'auth.sms.textlocal.sender'
tags: ['auth']
required: true
description: |
TextLocal Sender
links:
- name: 'Auth Server configuration'
link: 'https://supabase.com/docs/reference/auth'
- id: 'auth.sms.textlocal.api_key'
title: 'auth.sms.textlocal.api_key'
tags: ['auth']
required: true
default: 'env(SUPABASE_AUTH_SMS_TEXTLOCAL_API_KEY)'
description: |
TextLocal API Key
DO NOT commit your TextLocal API key to git. Use environment variable substitution instead.
links:
- name: 'Auth Server configuration'
link: 'https://supabase.com/docs/reference/auth'
- id: 'auth.sms.vonage.from'
title: 'auth.sms.vonage.from'
tags: ['auth']
required: true
description: |
Vonage From
links:
- name: 'Auth Server configuration'
link: 'https://supabase.com/docs/reference/auth'
- id: 'auth.sms.vonage.api_key'
title: 'auth.sms.vonage.api_key'
tags: ['auth']
required: true
description: |
Vonage API Key
links:
- name: 'Auth Server configuration'
link: 'https://supabase.com/docs/reference/auth'
- id: 'auth.sms.vonage.api_secret'
title: 'auth.sms.vonage.api_secret'
tags: ['auth']
required: true
default: 'env(SUPABASE_AUTH_SMS_VONAGE_API_SECRET)'
description: |
Vonage API Secret
DO NOT commit your Vonage API secret to git. Use environment variable substitution instead.
links:
- name: 'Auth Server configuration'
link: 'https://supabase.com/docs/reference/auth'
- id: 'auth.external.provider.enabled'
title: 'auth.external.<provider>.enabled'
tags: ['auth']
required: true
default: 'true'
required: false
default: 'false'
description: |
Use an external OAuth provider. The full list of providers are:
@@ -309,10 +644,15 @@ parameters:
- `github`
- `gitlab`
- `google`
- `keycloak`
- `linkedin`
- `notion`
- `twitch`
- `twitter`
- `slack`
- `spotify`
- `workos`
- `zoom`
links:
- name: 'Auth Server configuration'
link: 'https://supabase.com/docs/reference/auth'
@@ -331,8 +671,11 @@ parameters:
title: 'auth.external.<provider>.secret'
tags: ['auth']
required: true
default: 'env(SUPABASE_AUTH_EXTERNAL_<PROVIDER>_SECRET)'
description: |
Client secret for the external OAuth provider.
DO NOT commit your OAuth provider secret to git. Use environment variable substitution instead.
links:
- name: 'Auth Server configuration'
link: 'https://supabase.com/docs/reference/auth'
@@ -341,7 +684,6 @@ parameters:
title: 'auth.external.<provider>.url'
tags: ['auth']
required: false
default: '""'
description: |
The base URL used for constructing the URLs to request authorization and
access tokens. Used by gitlab and keycloak. For gitlab it defaults to
@@ -355,7 +697,6 @@ parameters:
title: 'auth.external.<provider>.redirect_uri'
tags: ['auth']
required: false
default: '""'
description: |
The URI a OAuth2 provider will redirect to with the code and state values.
links:
@@ -388,3 +729,46 @@ parameters:
links:
- name: '`supabase functions` CLI subcommands'
link: 'https://supabase.com/docs/reference/cli/supabase-functions'
- id: 'analytics.enabled'
title: 'analytics.enabled'
tags: ['analytics']
required: false
default: 'false'
description: |
Enable the local Logflare service.
links:
- name: Self-hosted Logflare Configuration
link: https://supabase.com/docs/reference/self-hosting-analytics/list-endpoints#getting-started
- id: 'analytics.port'
title: 'analytics.port'
tags: ['analytics']
required: false
default: '54327'
description: |
Port to the local Logflare service.
links: []
- id: 'analytics.vector_port'
title: 'analytics.vector_port'
tags: ['analytics']
required: false
default: '54328'
description: |
Port to the local syslog ingest service.
links: []
- id: 'analytics.backend'
title: 'analytics.backend'
tags: ['analytics']
required: false
default: '"postgres"'
description: |
Configure one of the supported backends:
- `postgres`
- `bigquery`
links:
- name: Self-hosted Logflare Configuration
link: https://supabase.com/docs/reference/self-hosting-analytics/list-endpoints#getting-started
+2
View File
@@ -33,3 +33,5 @@ yarn-error.log*
.env.production.local
.vercel
/public/dashboard
+164 -153
View File
@@ -36,168 +36,179 @@ export const SupabaseGrid = forwardRef<SupabaseGridRef, SupabaseGridProps>((prop
)
})
const SupabaseGridLayout = forwardRef<SupabaseGridRef, SupabaseGridProps>((props, ref) => {
const {
editable,
storageRef,
gridProps,
headerActions,
showCustomChildren,
customHeader,
children,
onAddRow,
onAddColumn,
updateTableRow,
onEditForeignKeyColumnValue,
onImportData,
} = props
const dispatch = useDispatch()
const state = useTrackedState()
const SupabaseGridLayout = forwardRef<SupabaseGridRef, SupabaseGridProps>(
function SupabaseGridLayout(props, ref) {
const {
editable,
storageRef,
gridProps,
headerActions,
showCustomChildren,
customHeader,
children,
onAddRow,
onAddColumn,
updateTableRow,
onEditForeignKeyColumnValue,
onImportData,
} = props
const dispatch = useDispatch()
const state = useTrackedState()
const gridRef = useRef<DataGridHandle>(null)
const [mounted, setMounted] = useState(false)
const gridRef = useRef<DataGridHandle>(null)
const [mounted, setMounted] = useState(false)
const [{ sort, filter }, setParams] = useUrlState({
arrayKeys: ['sort', 'filter'],
})
const sorts = formatSortURLParams(sort as string[])
const filters = formatFilterURLParams(filter as string[])
const [{ sort, filter }, setParams] = useUrlState({
arrayKeys: ['sort', 'filter'],
})
const sorts = formatSortURLParams(sort as string[])
const filters = formatFilterURLParams(filter as string[])
const { project } = useProjectContext()
const { data, isLoading, isRefetching } = useTableRowsQuery(
{
queryKey: [props.table.schema, props.table.name],
projectRef: project?.ref,
connectionString: project?.connectionString,
table: props.table,
sorts,
filters,
page: state.page,
limit: state.rowsPerPage,
},
{
keepPreviousData: true,
onSuccess(data) {
const { project } = useProjectContext()
const { data, error, isSuccess, isError, isLoading, isRefetching } = useTableRowsQuery(
{
queryKey: [props.table.schema, props.table.name],
projectRef: project?.ref,
connectionString: project?.connectionString,
table: props.table,
sorts,
filters,
page: state.page,
limit: state.rowsPerPage,
},
{
keepPreviousData: true,
onSuccess(data) {
dispatch({
type: 'SET_ROWS_COUNT',
payload: data.rows.length,
})
},
}
)
useImperativeHandle(ref, () => ({
rowAdded(row: Dictionary<any>) {
dispatch({
type: 'SET_ROWS_COUNT',
payload: data.rows.length,
type: 'ADD_NEW_ROW',
payload: row,
})
},
}
)
useImperativeHandle(ref, () => ({
rowAdded(row: Dictionary<any>) {
dispatch({
type: 'ADD_NEW_ROW',
payload: row,
})
},
rowEdited(row: Dictionary<any>, idx: number) {
dispatch({
type: 'EDIT_ROW',
payload: { row, idx },
})
},
}))
useEffect(() => {
if (!mounted) setMounted(true)
}, [])
useEffect(() => {
if (mounted) {
dispatch({ type: 'UPDATE_FILTERS', payload: {} })
}
}, [JSON.stringify(filters)])
useEffect(() => {
if (mounted) {
dispatch({ type: 'UPDATE_SORTS', payload: {} })
}
}, [JSON.stringify(sorts)])
useEffect(() => {
if (state.isInitialComplete && storageRef && state.table) {
saveStorageDebounced(state, storageRef, sort as string[], filter as string[])
}
}, [
state.table,
state.isInitialComplete,
state.gridColumns,
JSON.stringify(sorts),
JSON.stringify(filters),
storageRef,
])
useEffect(() => {
dispatch({
type: 'INIT_CALLBACK',
payload: { ...props },
})
}, [])
useEffect(() => {
const initializeData = async () => {
const { savedState } = await initTable(
props,
state,
dispatch,
sort as string[],
filter as string[]
)
if (savedState.sorts || savedState.filters) {
setParams((prevParams) => {
return {
...prevParams,
...(savedState.sorts && { sort: savedState.sorts }),
...(savedState.filters && { filter: savedState.filters }),
}
rowEdited(row: Dictionary<any>, idx: number) {
dispatch({
type: 'EDIT_ROW',
payload: { row, idx },
})
},
}))
useEffect(() => {
if (!mounted) setMounted(true)
}, [])
useEffect(() => {
if (mounted) {
dispatch({ type: 'UPDATE_FILTERS', payload: {} })
}
}
}, [JSON.stringify(filters)])
const refreshTable = JSON.stringify(props.table) !== JSON.stringify(state.table)
useEffect(() => {
if (mounted) {
dispatch({ type: 'UPDATE_SORTS', payload: {} })
}
}, [JSON.stringify(sorts)])
if (!state.table || refreshTable) {
initializeData()
}
}, [state.table, props.table, props.schema])
useEffect(() => {
if (state.isInitialComplete && storageRef && state.table) {
saveStorageDebounced(state, storageRef, sort as string[], filter as string[])
}
}, [
state.table,
state.isInitialComplete,
state.gridColumns,
JSON.stringify(sorts),
JSON.stringify(filters),
storageRef,
])
return (
<div className="sb-grid">
<Header
table={props.table}
sorts={sorts}
filters={filters}
isRefetching={isRefetching}
onAddRow={editable ? onAddRow : undefined}
onAddColumn={editable ? onAddColumn : undefined}
onImportData={editable ? onImportData : undefined}
headerActions={headerActions}
customHeader={customHeader}
/>
{showCustomChildren && children !== undefined ? (
<>{children}</>
) : (
<>
<Grid
ref={gridRef}
{...gridProps}
rows={data?.rows ?? []}
updateRow={updateTableRow}
onAddRow={onAddRow}
onImportData={onImportData}
onEditForeignKeyColumnValue={onEditForeignKeyColumnValue}
/>
<Footer isLoading={isLoading || isRefetching} />
<Shortcuts gridRef={gridRef} />
</>
)}
useEffect(() => {
dispatch({
type: 'INIT_CALLBACK',
payload: { ...props },
})
}, [])
{mounted &&
createPortal(<RowContextMenu table={props.table} rows={data?.rows ?? []} />, document.body)}
</div>
)
})
useEffect(() => {
const initializeData = async () => {
const { savedState } = await initTable(
props,
state,
dispatch,
sort as string[],
filter as string[]
)
if (savedState.sorts || savedState.filters) {
setParams((prevParams) => {
return {
...prevParams,
...(savedState.sorts && { sort: savedState.sorts }),
...(savedState.filters && { filter: savedState.filters }),
}
})
}
}
const refreshTable = JSON.stringify(props.table) !== JSON.stringify(state.table)
if (!state.table || refreshTable) {
initializeData()
}
}, [state.table, props.table, props.schema])
return (
<div className="sb-grid">
<Header
table={props.table}
sorts={sorts}
filters={filters}
isRefetching={isRefetching}
onAddRow={editable ? onAddRow : undefined}
onAddColumn={editable ? onAddColumn : undefined}
onImportData={editable ? onImportData : undefined}
headerActions={headerActions}
customHeader={customHeader}
/>
{showCustomChildren && children !== undefined ? (
<>{children}</>
) : (
<>
<Grid
ref={gridRef}
{...gridProps}
rows={data?.rows ?? []}
error={error}
isLoading={isLoading}
isSuccess={isSuccess}
isError={isError}
filters={filters}
setParams={setParams}
updateRow={updateTableRow}
onAddRow={onAddRow}
onImportData={onImportData}
onEditForeignKeyColumnValue={onEditForeignKeyColumnValue}
/>
<Footer isLoading={isLoading || isRefetching} />
<Shortcuts gridRef={gridRef} />
</>
)}
{mounted &&
createPortal(
<RowContextMenu table={props.table} rows={data?.rows ?? []} />,
document.body
)}
</div>
)
}
)
@@ -1,32 +1,44 @@
import { FormatterProps } from '@supabase/react-data-grid'
import Link from 'next/link'
import { PropsWithChildren } from 'react'
import { FormatterProps } from '@supabase/react-data-grid'
import * as Tooltip from '@radix-ui/react-tooltip'
import { useParams } from 'common/hooks'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import { useTableQuery } from 'data/tables/table-query'
import { Button, IconArrowRight } from 'ui'
import { SupaRow } from '../../types'
import { NullValue } from '../common'
import * as Tooltip from '@radix-ui/react-tooltip'
import { Button, IconArrowRight } from 'ui'
import { useStore } from 'hooks'
import { useParams } from 'common/hooks'
import { useTablesQuery } from 'data/tables/tables-query'
export const ForeignKeyFormatter = (props: PropsWithChildren<FormatterProps<SupaRow, unknown>>) => {
const { ref, id } = useParams()
const { meta } = useStore()
const { project } = useProjectContext()
const { ref: projectRef, id: _id } = useParams()
const id = _id ? Number(_id) : undefined
const { row, column } = props
const selectedTable = meta.tables.byId(id as string)
const { data: selectedTable } = useTableQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
id,
})
const relationship = (selectedTable?.relationships ?? []).find(
(r) =>
r.source_schema === selectedTable?.schema &&
r.source_table_name === selectedTable?.name &&
r.source_column_name === column.name
)
const targetTable = meta.tables
.list()
.find(
(table) =>
table.schema === relationship?.target_table_schema &&
table.name === relationship.target_table_name
)
const { data: tables } = useTablesQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
schema: relationship?.target_table_schema,
})
const targetTable = tables?.find(
(table) =>
table.schema === relationship?.target_table_schema &&
table.name === relationship.target_table_name
)
const value = row[column.key]
@@ -39,7 +51,7 @@ export const ForeignKeyFormatter = (props: PropsWithChildren<FormatterProps<Supa
<Tooltip.Root delayDuration={0}>
<Tooltip.Trigger>
<Link
href={`/project/${ref}/editor/${targetTable?.id}?filter=${relationship?.target_column_name}%3Aeq%3A${value}`}
href={`/project/${projectRef}/editor/${targetTable?.id}?filter=${relationship?.target_column_name}%3Aeq%3A${value}`}
>
<a>
<Button
+75 -20
View File
@@ -7,11 +7,14 @@ import { memo } from 'react-tracked'
import { ForeignRowSelectorProps } from 'components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/ForeignRowSelector/ForeignRowSelector'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import AlertError from 'components/ui/AlertError'
import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader'
import { useForeignKeyConstraintsQuery } from 'data/database/foreign-key-constraints-query'
import { useDispatch, useTrackedState } from '../../store'
import { GridProps, SupaRow } from '../../types'
import RowRenderer from './RowRenderer'
import { useUrlState } from 'hooks'
import { Button } from 'ui'
import { useDispatch, useTrackedState } from '../../store'
import { Filter, GridProps, SupaRow } from '../../types'
import RowRenderer from './RowRenderer'
const rowKeyGetter = (row: SupaRow) => {
return row?.idx ?? -1
@@ -27,6 +30,12 @@ const updateColumnResizeDebounced = AwesomeDebouncePromise(updateColumnResize, 5
interface IGrid extends GridProps {
rows: any[]
error: any
isLoading: boolean
isSuccess: boolean
isError: boolean
filters: Filter[]
setParams: ReturnType<typeof useUrlState>[1]
updateRow: (previousRow: any, updatedData: any) => void
onAddRow?: () => void
onImportData?: () => void
@@ -48,6 +57,12 @@ export const Grid = memo(
gridClass,
rowClass,
rows,
error,
isLoading,
isSuccess,
isError,
filters,
setParams,
updateRow,
onAddRow,
onImportData,
@@ -121,6 +136,12 @@ export const Grid = memo(
}
}
const removeAllFilters = () => {
setParams((prevParams) => {
return { ...prevParams, filter: [] }
})
}
return (
<div
className={containerClass}
@@ -142,26 +163,60 @@ export const Grid = memo(
rowClass={rowClass}
style={{ height: '100%' }}
noRowsFallback={
<div
style={{ height: `calc(100% - 35px)` }}
className="flex flex-col items-center justify-center"
>
<p className="text-base">This table is empty</p>
{onAddRow !== undefined && onImportData !== undefined && (
<>
{isLoading && (
<div className="p-2">
<GenericSkeletonLoader />
</div>
)}
{isError && (
<div className="p-2">
<AlertError error={error} subject="Failed to retrieve rows from table" />
</div>
)}
{isSuccess && (
<>
<p className="text-sm text-light mt-1">Add or generate rows to get started.</p>
<div className="flex items-center space-x-2 mt-4">
{/* [Joshen] Leaving this as a placeholder */}
{/* <Button type="outline">Generate random data</Button> */}
{onAddRow !== undefined && onImportData !== undefined && (
<Button type="default" onClick={onImportData}>
Import data via CSV
</Button>
)}
</div>
{(filters ?? []).length === 0 ? (
<div
style={{ height: `calc(100% - 35px)` }}
className="flex flex-col items-center justify-center"
>
<p className="text-sm text">This table is empty</p>
{onAddRow !== undefined && onImportData !== undefined && (
<>
<p className="text-sm text-light mt-1">
Add or generate rows to get started.
</p>
<div className="flex items-center space-x-2 mt-4">
{/* [Joshen] Leaving this as a placeholder */}
{/* <Button type="outline">Generate random data</Button> */}
{onAddRow !== undefined && onImportData !== undefined && (
<Button type="default" onClick={onImportData}>
Import data via CSV
</Button>
)}
</div>
</>
)}
</div>
) : (
<div
style={{ height: `calc(100% - 35px)` }}
className="flex flex-col items-center justify-center"
>
<p className="text-sm text-light">
The filters applied has returned no results from this table
</p>
<div className="flex items-center space-x-2 mt-4">
<Button type="default" onClick={() => removeAllFilters()}>
Remove all filters
</Button>
</div>
</div>
)}
</>
)}
</div>
</>
}
/>
</div>
@@ -1,24 +1,38 @@
import { useCallback, useState } from 'react'
import { Button, IconAlertCircle, Modal } from 'ui'
import Link from 'next/link'
import { useStore } from 'hooks'
import { useCallback, useState } from 'react'
import { useParams } from 'common/hooks'
import ConfirmationModal from 'components/ui/ConfirmationModal'
import type { PostgresTable } from '@supabase/postgres-meta'
import { rlsAcknowledgedKey } from 'components/grid/constants'
import RLSDisableModalContent from 'components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/RLSDisableModal'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import ConfirmationModal from 'components/ui/ConfirmationModal'
import { ENTITY_TYPE } from 'data/entity-types/entity-type-constants'
import { useTableQuery } from 'data/tables/table-query'
import useEntityType from 'hooks/misc/useEntityType'
import { Button, IconAlertCircle, Modal } from 'ui'
export default function RLSBannerWarning() {
const { meta } = useStore()
const { ref: projectRef, id: tableID } = useParams()
const { project } = useProjectContext()
const { ref: projectRef, id: _id } = useParams()
const tableID = _id ? Number(_id) : undefined
const rlsKey = rlsAcknowledgedKey(tableID)
const isAcknowledged = localStorage?.getItem(rlsKey) === 'true' ?? false
const [isOpen, setIsOpen] = useState(false)
const tables: PostgresTable[] = meta.tables.list()
const currentTable = tables.find((table) => table.id.toString() === tableID)
const entityType = useEntityType(tableID)
const { data: currentTable } = useTableQuery(
{
projectRef: project?.ref,
connectionString: project?.connectionString,
id: tableID,
},
{
enabled: entityType?.type === ENTITY_TYPE.TABLE,
}
)
const rlsEnabled = currentTable?.rls_enabled
const isPublicTable = currentTable?.schema === 'public'
@@ -1,4 +1,4 @@
import { useState, useMemo, useCallback } from 'react'
import { useState, useMemo, useCallback, KeyboardEvent } from 'react'
import { isEqual } from 'lodash'
import { Button, IconPlus, IconFilter, Popover } from 'ui'
import update from 'immutability-helper'
@@ -102,6 +102,12 @@ const FilterOverlay = ({ table, filters: filtersFromUrl, setParams }: FilterOver
})
}
function handleEnterKeyDown(event: KeyboardEvent<HTMLInputElement>) {
if (event.key === 'Enter') {
onApplyFilter()
}
}
return (
<div className="space-y-2 py-2">
<div className="space-y-2">
@@ -113,6 +119,7 @@ const FilterOverlay = ({ table, filters: filtersFromUrl, setParams }: FilterOver
filterIdx={index}
onChange={onChangeFilter}
onDelete={onDeleteFilter}
onKeyDown={handleEnterKeyDown}
/>
))}
{filters.length == 0 && (
@@ -1,4 +1,4 @@
import { memo } from 'react'
import { memo, KeyboardEvent } from 'react'
import { Button, Input, IconChevronDown, IconX } from 'ui'
import { Filter, FilterOperator, SupaTable } from 'components/grid/types'
@@ -11,9 +11,10 @@ export interface FilterRowProps {
filter: Filter
onChange: (index: number, filter: Filter) => void
onDelete: (index: number) => void
onKeyDown: (event: KeyboardEvent<HTMLInputElement>) => void
}
const FilterRow = ({ table, filter, filterIdx, onChange, onDelete }: FilterRowProps) => {
const FilterRow = ({ table, filter, filterIdx, onChange, onDelete, onKeyDown }: FilterRowProps) => {
const column = table.columns.find((x) => x.name === filter.column)
const columnOptions =
table.columns?.map((x) => {
@@ -80,6 +81,7 @@ const FilterRow = ({ table, filter, filterIdx, onChange, onDelete }: FilterRowPr
value: event.target.value,
})
}
onKeyDown={onKeyDown}
/>
<Button
icon={<IconX strokeWidth={1.5} size={14} />}
+2 -1
View File
@@ -18,4 +18,5 @@ export const ERROR_PRIMARY_KEY_NOTFOUND =
export const RLS_ACKNOWLEDGED_KEY = 'supabase-acknowledge-rls-warning'
export const rlsAcknowledgedKey = (tableID?: string) => `${RLS_ACKNOWLEDGED_KEY}-${tableID}`
export const rlsAcknowledgedKey = (tableID?: string | number) =>
`${RLS_ACKNOWLEDGED_KEY}-${String(tableID)}`
@@ -194,8 +194,8 @@ const PolicyEditorModal = ({
<div className="">
<ConfirmationModal
visible={isClosingPolicyEditorModal}
header="Confirm to close"
buttonLabel="Confirm"
header="Discard changes"
buttonLabel="Discard"
onSelectCancel={() => setIsClosingPolicyEditorModal(false)}
onSelectConfirm={() => {
onSelectCancel()
@@ -1,8 +1,10 @@
import type { PostgresExtension, PostgresSchema } from '@supabase/postgres-meta'
import type { PostgresExtension } from '@supabase/postgres-meta'
import { useEffect, useState } from 'react'
import { Button, Form, IconDatabase, IconPlus, Input, Listbox, Modal } from 'ui'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import ShimmeringLoader from 'components/ui/ShimmeringLoader'
import { useSchemasQuery } from 'data/database/schemas-query'
import { useStore } from 'hooks'
interface EnableExtensionModalProps {
@@ -12,11 +14,15 @@ interface EnableExtensionModalProps {
}
const EnableExtensionModal = ({ visible, extension, onCancel }: EnableExtensionModalProps) => {
const { project } = useProjectContext()
const { ui, meta } = useStore()
const [defaultSchema, setDefaultSchema] = useState()
const [fetchingSchemaInfo, setFetchingSchemaInfo] = useState(false)
const schemas = meta.schemas.list()
const { data: schemas, isLoading: isSchemasLoading } = useSchemasQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
})
// [Joshen] Worth checking in with users - whether having this schema selection
// might be confusing, and if we should have a tooltip to explain that schemas
@@ -44,7 +50,7 @@ const EnableExtensionModal = ({ visible, extension, onCancel }: EnableExtensionM
return () => {
cancel = true
}
}, [visible])
}, [visible, extension.name])
const validate = (values: any) => {
const errors: any = {}
@@ -124,7 +130,7 @@ const EnableExtensionModal = ({ visible, extension, onCancel }: EnableExtensionM
return (
<div className="space-y-4 py-4">
<Modal.Content>
{fetchingSchemaInfo ? (
{fetchingSchemaInfo || isSchemasLoading ? (
<div className="space-y-2">
<ShimmeringLoader />
<div className="w-3/4">
@@ -156,8 +162,7 @@ const EnableExtensionModal = ({ visible, extension, onCancel }: EnableExtensionM
Create a new schema "{extension.name}"
</Listbox.Option>
<Modal.Separator />
{/* @ts-ignore */}
{schemas.map((schema: PostgresSchema) => {
{schemas?.map((schema) => {
return (
<Listbox.Option
key={schema.id}
@@ -7,9 +7,11 @@ import { Button, IconPlus, IconTrash, Input, Listbox, Modal, Radio, SidePanel, T
import { Dictionary } from 'components/grid'
import { Function } from 'components/interfaces/Functions/Functions.types'
import { POSTGRES_DATA_TYPES } from 'components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.constants'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import ConfirmationModal from 'components/ui/ConfirmationModal'
import Panel from 'components/ui/Panel'
import SqlEditor from 'components/ui/SqlEditor'
import { useSchemasQuery } from 'data/database/schemas-query'
import { useStore } from 'hooks'
import { isResponseOk } from 'lib/common/fetch'
import { SupaResponse } from 'types'
@@ -294,21 +296,24 @@ interface CreateFunctionProps {
}
const CreateFunction = ({ func, visible, setVisible }: CreateFunctionProps) => {
const { project } = useProjectContext()
const { ui, meta } = useStore()
const _localState = useLocalObservable(() => new CreateFunctionStore())
_localState.meta = meta as any
const [isClosingPanel, setIsClosingPanel] = useState<boolean>(false)
useEffect(() => {
const fetchSchemas = async () => {
await (_localState!.meta as any).schemas.load()
const schemas = (_localState!.meta as any).schemas.list()
_localState.setSchemas(schemas)
useSchemasQuery(
{
projectRef: project?.ref,
connectionString: project?.connectionString,
},
{
onSuccess(schemas) {
_localState.setSchemas(schemas)
},
}
fetchSchemas()
}, [])
)
useEffect(() => {
_localState.formState.reset(func)
@@ -447,8 +452,8 @@ const CreateFunction = ({ func, visible, setVisible }: CreateFunctionProps) => {
</CreateFunctionContext.Provider>
<ConfirmationModal
visible={isClosingPanel}
header="Confirm to close"
buttonLabel="Confirm"
header="Discard changes"
buttonLabel="Discard"
onSelectCancel={() => setIsClosingPanel(false)}
onSelectConfirm={() => {
setIsClosingPanel(false)
@@ -1,17 +1,20 @@
import * as Tooltip from '@radix-ui/react-tooltip'
import { PostgresFunction, PostgresSchema } from '@supabase/postgres-meta'
import { PostgresFunction } from '@supabase/postgres-meta'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { noop, partition } from 'lodash'
import { observer } from 'mobx-react-lite'
import { useState } from 'react'
import { Button, IconLock, IconSearch, Input, Listbox } from 'ui'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import ProductEmptyState from 'components/to-be-cleaned/ProductEmptyState'
import Table from 'components/to-be-cleaned/Table'
import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader'
import { useCheckPermissions, useStore } from 'hooks'
import FunctionList from './FunctionList'
import AlertError from 'components/ui/AlertError'
import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader'
import { useSchemasQuery } from 'data/database/schemas-query'
import { useCheckPermissions, useStore } from 'hooks'
import { EXCLUDED_SCHEMAS } from 'lib/constants/schemas'
import FunctionList from './FunctionList'
interface FunctionsListProps {
createFunction: () => void
@@ -24,6 +27,7 @@ const FunctionsList = ({
editFunction = noop,
deleteFunction = noop,
}: FunctionsListProps) => {
const { project } = useProjectContext()
const { meta } = useStore()
const [selectedSchema, setSelectedSchema] = useState<string>('public')
const [filterString, setFilterString] = useState<string>('')
@@ -33,11 +37,14 @@ const FunctionsList = ({
'functions'
)
const schemas: PostgresSchema[] = meta.schemas.list()
const [protectedSchemas, openSchemas] = partition(schemas, (schema) =>
meta.excludedSchemas.includes(schema?.name ?? '')
const { data: schemas } = useSchemasQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
})
const [protectedSchemas, openSchemas] = partition(schemas ?? [], (schema) =>
EXCLUDED_SCHEMAS.includes(schema?.name ?? '')
)
const schema = schemas.find((schema) => schema.name === selectedSchema)
const schema = schemas?.find((schema) => schema.name === selectedSchema)
const isLocked = protectedSchemas.some((s) => s.id === schema?.id)
const functions = meta.functions.list()
@@ -1,6 +1,6 @@
import { PostgresTable, PostgresTrigger } from '@supabase/postgres-meta'
import Image from 'next/image'
import { MutableRefObject, useEffect, useRef, useState } from 'react'
import { MutableRefObject, useEffect, useMemo, useRef, useState } from 'react'
import { useParams } from 'common/hooks'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
@@ -12,12 +12,13 @@ import {
EdgeFunctionsResponse,
useEdgeFunctionsQuery,
} from 'data/edge-functions/edge-functions-query'
import { getTable } from 'data/tables/table-query'
import { useTablesQuery } from 'data/tables/tables-query'
import { useStore } from 'hooks'
import { tryParseJson, uuidv4 } from 'lib/helpers'
import { isValidHttpUrl, tryParseJson, uuidv4 } from 'lib/helpers'
import { Button, Checkbox, Form, Input, Listbox, Modal, Radio, SidePanel } from 'ui'
import HTTPRequestFields from './HTTPRequestFields'
import { AVAILABLE_WEBHOOK_TYPES, HOOK_EVENTS } from './Hooks.constants'
import { isValidHttpUrl } from 'lib/helpers'
export interface EditHookPanelProps {
visible: boolean
@@ -29,7 +30,7 @@ export type HTTPArgument = { id: string; name: string; value: string }
const EditHookPanel = ({ visible, selectedHook, onClose }: EditHookPanelProps) => {
const { ref } = useParams()
const { meta, ui } = useStore()
const { ui } = useStore()
const submitRef = useRef<any>(null)
const [isEdited, setIsEdited] = useState(false)
const [isClosingPanel, setIsClosingPanel] = useState(false)
@@ -44,6 +45,10 @@ const EditHookPanel = ({ visible, selectedHook, onClose }: EditHookPanelProps) =
const [httpParameters, setHttpParameters] = useState<HTTPArgument[]>([])
const { project } = useProjectContext()
const { data } = useTablesQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
})
const { data: functions } = useEdgeFunctionsQuery({ projectRef: ref })
const { mutate: createDatabaseTrigger, isLoading: isCreatingDatabaseTrigger } =
useDatabaseTriggerCreateMutation({
@@ -81,7 +86,10 @@ const EditHookPanel = ({ visible, selectedHook, onClose }: EditHookPanelProps) =
})
const isSubmitting = isCreatingDatabaseTrigger || isUpdatingDatabaseTrigger
const tables = meta.tables.list().sort((a, b) => (a.schema > b.schema ? 0 : -1))
const tables = useMemo(
() => [...(data ?? [])].sort((a, b) => (a.schema > b.schema ? 0 : -1)),
[data]
)
const restUrl = project?.restUrl
const restUrlTld = new URL(restUrl as string).hostname.split('.').pop()
@@ -185,7 +193,7 @@ const EditHookPanel = ({ visible, selectedHook, onClose }: EditHookPanelProps) =
return setEventsError('Please select at least one event')
}
const selectedTable = meta.tables.byId(values.table_id)
const selectedTable = await getTable(values.table_id)
if (!selectedTable) {
return ui.setNotification({ category: 'error', message: 'Unable to find selected table' })
}
@@ -308,8 +316,8 @@ const EditHookPanel = ({ visible, selectedHook, onClose }: EditHookPanelProps) =
</SidePanel>
<ConfirmationModal
visible={isClosingPanel}
header="Confirm to close"
buttonLabel="Confirm"
header="Discard changes"
buttonLabel="Discard"
onSelectCancel={() => setIsClosingPanel(false)}
onSelectConfirm={() => {
setIsClosingPanel(false)
@@ -134,7 +134,7 @@ CREATE INDEX ON "${selectedSchema}"."${selectedEntity}" USING ${selectedIndexTyp
value={selectedSchema}
onChange={setSelectedSchema}
>
{(schemas?.result ?? []).map((schema) => (
{(schemas ?? []).map((schema) => (
<Listbox.Option key={schema.name} value={schema.name} label={schema.name}>
{schema.name}
</Listbox.Option>
@@ -137,7 +137,7 @@ const Indexes = () => {
onChange={setSelectedSchema}
className="w-[260px]"
>
{(schemas?.result ?? []).map((schema) => (
{(schemas ?? []).map((schema) => (
<Listbox.Option
key={schema.name}
value={schema.name}
@@ -1,13 +1,17 @@
import type { PostgresPublication } from '@supabase/postgres-meta'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { observer } from 'mobx-react-lite'
import { useState } from 'react'
import { Button, IconAlertCircle, IconChevronLeft, IconSearch, Input } from 'ui'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import NoSearchResults from 'components/to-be-cleaned/NoSearchResults'
import Table from 'components/to-be-cleaned/Table'
import AlertError from 'components/ui/AlertError'
import InformationBox from 'components/ui/InformationBox'
import { useCheckPermissions, useStore } from 'hooks'
import Connecting from 'components/ui/Loading/Loading'
import { useTablesQuery } from 'data/tables/tables-query'
import { useCheckPermissions } from 'hooks'
import { EXCLUDED_SCHEMAS } from 'lib/constants/schemas'
import { Button, IconAlertCircle, IconChevronLeft, IconSearch, Input } from 'ui'
import PublicationsTableItem from './PublicationsTableItem'
interface PublicationsTablesProps {
@@ -16,7 +20,7 @@ interface PublicationsTablesProps {
}
const PublicationsTables = ({ selectedPublication, onSelectBack }: PublicationsTablesProps) => {
const { meta } = useStore()
const { project } = useProjectContext()
const [filterString, setFilterString] = useState<string>('')
const canUpdatePublications = useCheckPermissions(
@@ -24,13 +28,27 @@ const PublicationsTables = ({ selectedPublication, onSelectBack }: PublicationsT
'publications'
)
const tables =
filterString.length === 0
? meta.tables.list((table: any) => !meta.excludedSchemas.includes(table.schema))
: meta.tables.list(
(table: any) =>
!meta.excludedSchemas.includes(table.schema) && table.name.includes(filterString)
const {
data: tables,
isLoading,
isSuccess,
isError,
error,
} = useTablesQuery(
{
projectRef: project?.ref,
connectionString: project?.connectionString,
},
{
select(tables) {
return tables.filter((table) =>
filterString.length === 0
? !EXCLUDED_SCHEMAS.includes(table.schema)
: !EXCLUDED_SCHEMAS.includes(table.schema) && table.name.includes(filterString)
)
},
}
)
// const publication = selectedPublication
// const enabledForAllTables = publication.tables == null
@@ -90,19 +108,28 @@ const PublicationsTables = ({ selectedPublication, onSelectBack }: PublicationsT
)}
</div>
</div>
{tables.length === 0 ? (
<NoSearchResults />
) : (
<div>
<Table
head={[
<Table.th key="header-name">Name</Table.th>,
<Table.th key="header-schema">Schema</Table.th>,
<Table.th key="header-desc" className="hidden text-left lg:table-cell">
Description
</Table.th>,
<Table.th key="header-all">
{/* Temporarily disable All tables toggle for publications. See https://github.com/supabase/supabase/pull/7233.
{isLoading && (
<div className="mt-8">
<Connecting />
</div>
)}
{isError && <AlertError error={error} subject="Failed to retrieve tables" />}
{isSuccess &&
(tables.length === 0 ? (
<NoSearchResults />
) : (
<div>
<Table
head={[
<Table.th key="header-name">Name</Table.th>,
<Table.th key="header-schema">Schema</Table.th>,
<Table.th key="header-desc" className="hidden text-left lg:table-cell">
Description
</Table.th>,
<Table.th key="header-all">
{/* Temporarily disable All tables toggle for publications. See https://github.com/supabase/supabase/pull/7233.
<div className="flex flex-row space-x-3 items-center justify-end">
<div className="text-xs leading-4 font-medium text-gray-400 text-right ">
All Tables
@@ -116,20 +143,20 @@ const PublicationsTables = ({ selectedPublication, onSelectBack }: PublicationsT
onChange={() => toggleReplicationForAllTables(publication, enabledForAllTables)}
/>
</div> */}
</Table.th>,
]}
body={tables.map((table: any, i: number) => (
<PublicationsTableItem
key={table.id}
table={table}
selectedPublication={selectedPublication}
/>
))}
/>
</div>
)}
</Table.th>,
]}
body={tables.map((table: any, i: number) => (
<PublicationsTableItem
key={table.id}
table={table}
selectedPublication={selectedPublication}
/>
))}
/>
</div>
))}
</>
)
}
export default observer(PublicationsTables)
export default PublicationsTables
@@ -1,7 +1,8 @@
import dagre from '@dagrejs/dagre'
import clsx from 'clsx'
import { observer } from 'mobx-react-lite'
import { uniqBy } from 'lodash'
import { Diamond, Fingerprint } from 'lucide-react'
import { observer } from 'mobx-react-lite'
import { useEffect, useMemo } from 'react'
import ReactFlow, {
Background,
@@ -17,11 +18,10 @@ import ReactFlow, {
import { PostgresTable } from '@supabase/postgres-meta'
import { useTheme } from 'common/Providers'
import { useStore } from 'hooks'
import { IconHash, IconKey, IconLoader, IconLock } from 'ui'
import { Diamond, Fingerprint } from 'lucide-react'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import { useTablesQuery } from 'data/tables/tables-query'
import 'reactflow/dist/style.css'
import { IconHash, IconKey, IconLoader, IconLock } from 'ui'
type TableNodeData = {
name: string
@@ -347,23 +347,32 @@ const TablesGraph = ({ tables }: { tables: PostgresTable[] }) => {
}
const SchemaGraph = ({ schema }: { schema: string }) => {
const { meta } = useStore()
const tables = meta.tables.list((table: { schema: string }) => table.schema === schema)
const { project } = useProjectContext()
const {
data: tables,
isLoading,
isError,
error,
} = useTablesQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
schema,
})
if (meta.tables.isLoading) {
if (isLoading) {
return (
<div className="flex h-full w-full items-center justify-center space-x-2">
<IconLoader className="animate-spin" size={14} />
<p className="text-sm text-scale-1000">Loading schemas...</p>
<p className="text-sm text-scale-1000">Loading table...</p>
</div>
)
}
if (meta.tables.hasError) {
if (isError) {
return (
<div className="px-6 py-4 text-scale-1000">
<p>Error connecting to API</p>
<p>{`${meta.tables.error?.message ?? 'Unknown error'}`}</p>
<p>{`${error?.message ?? 'Unknown error'}`}</p>
</div>
)
}
@@ -4,6 +4,11 @@ import { PermissionAction } from '@supabase/shared-types/out/constants'
import { noop } from 'lodash'
import { observer } from 'mobx-react-lite'
import { useState } from 'react'
import NoSearchResults from 'components/to-be-cleaned/NoSearchResults'
import Table from 'components/to-be-cleaned/Table'
import { useCheckPermissions, useStore } from 'hooks'
import { EXCLUDED_SCHEMAS } from 'lib/constants/schemas'
import {
Button,
IconChevronLeft,
@@ -15,11 +20,6 @@ import {
Input,
} from 'ui'
import NoSearchResults from 'components/to-be-cleaned/NoSearchResults'
import Table from 'components/to-be-cleaned/Table'
import { useCheckPermissions, useStore } from 'hooks'
import { ScaffoldHeader } from 'components/layouts/Scaffold'
interface ColumnListProps {
selectedTable: PostgresTable
onSelectBack: () => void
@@ -42,7 +42,7 @@ const ColumnList = ({
? selectedTable.columns
: selectedTable.columns?.filter((column: any) => column.name.includes(filterString))) ?? []
const isLocked = meta.excludedSchemas.includes(selectedTable.schema ?? '')
const isLocked = EXCLUDED_SCHEMAS.includes(selectedTable.schema ?? '')
const canUpdateColumns = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'columns')
return (
@@ -1,9 +1,18 @@
import * as Tooltip from '@radix-ui/react-tooltip'
import type { PostgresSchema, PostgresTable } from '@supabase/postgres-meta'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { noop, partition } from 'lodash'
import { observer } from 'mobx-react-lite'
import { useState } from 'react'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import NoSearchResults from 'components/to-be-cleaned/NoSearchResults'
import Table from 'components/to-be-cleaned/Table'
import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader'
import { useSchemasQuery } from 'data/database/schemas-query'
import { useTablesQuery } from 'data/tables/tables-query'
import { useCheckPermissions, useStore } from 'hooks'
import { EXCLUDED_SCHEMAS } from 'lib/constants/schemas'
import { useTableEditorStateSnapshot } from 'state/table-editor'
import {
Button,
IconCheck,
@@ -16,15 +25,9 @@ import {
Input,
Listbox,
} from 'ui'
import Table from 'components/to-be-cleaned/Table'
import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader'
import { useCheckPermissions, useStore } from 'hooks'
import { ScaffoldHeader } from 'components/layouts/Scaffold'
import AlertError from 'components/ui/AlertError'
interface TableListProps {
selectedSchema: string
onSelectSchema: (schema: string) => void
onAddTable: () => void
onEditTable: (table: any) => void
onDeleteTable: (table: any) => void
@@ -32,38 +35,53 @@ interface TableListProps {
}
const TableList = ({
selectedSchema,
onSelectSchema = noop,
onAddTable = noop,
onEditTable = noop,
onDeleteTable = noop,
onOpenTable = noop,
}: TableListProps) => {
const { meta } = useStore()
const { isLoading } = meta.tables
const { project } = useProjectContext()
const snap = useTableEditorStateSnapshot()
const [filterString, setFilterString] = useState<string>('')
const canUpdateTables = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'tables')
const schemas: PostgresSchema[] = meta.schemas.list()
const [protectedSchemas, openSchemas] = partition(schemas, (schema) =>
meta.excludedSchemas.includes(schema?.name ?? '')
const { data: schemas } = useSchemasQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
})
const [protectedSchemas, openSchemas] = partition(schemas ?? [], (schema) =>
EXCLUDED_SCHEMAS.includes(schema?.name ?? '')
)
const allTables: PostgresTable[] = meta.tables.list(
(table: PostgresTable) => table.schema === selectedSchema
const {
data: tables,
isLoading,
isSuccess,
isError,
error,
} = useTablesQuery(
{
projectRef: project?.ref,
connectionString: project?.connectionString,
schema: snap.selectedSchemaName,
},
{
select(tables) {
return filterString.length === 0
? tables
: tables.filter((table) => table.name.includes(filterString))
},
}
)
const tables =
filterString.length === 0
? allTables
: // @ts-ignore
allTables.filter((table: PostgresTable) => table.name.includes(filterString))
const publications = meta.publications.list()
const realtimePublication = publications.find(
(publication) => publication.name === 'supabase_realtime'
)
// @ts-ignore
const schema = schemas.find((schema) => schema.name === selectedSchema)
const schema = schemas?.find((schema) => schema.name === snap.selectedSchemaName)
const isLocked = protectedSchemas.some((s) => s.id === schema?.id)
return (
@@ -77,14 +95,13 @@ const TableList = ({
<div className="w-[260px]">
<Listbox
size="small"
value={selectedSchema}
onChange={onSelectSchema}
value={snap.selectedSchemaName}
onChange={snap.setSelectedSchemaName}
icon={isLocked && <IconLock size={14} strokeWidth={2} />}
>
<Listbox.Option disabled key="normal-schemas" value="normal-schemas" label="Schemas">
<p className="text-sm">Schemas</p>
</Listbox.Option>
{/* @ts-ignore */}
{openSchemas.map((schema) => (
<Listbox.Option
key={schema.id}
@@ -125,6 +142,7 @@ const TableList = ({
/>
</div>
</div>
{!isLocked && (
<div>
<Tooltip.Root delayDuration={0}>
@@ -159,11 +177,19 @@ const TableList = ({
)}
</div>
<div className="my-4">
{isLoading ? (
{isLoading && (
<div className="py-4 space-y-2">
<GenericSkeletonLoader />
</div>
)}
{isError && <AlertError error={error} subject="Failed to retrieve tables" />}
{isSuccess &&
(tables.length === 0 ? (
<NoSearchResults />
) : (
<div className="w-full">
<div className="my-4 w-full">
<Table
head={[
<Table.th key="name">Name</Table.th>,
@@ -184,17 +210,17 @@ const TableList = ({
body={
<>
{tables.length === 0 && filterString.length === 0 && (
<Table.tr key={selectedSchema}>
<Table.tr key={snap.selectedSchemaName}>
<Table.td colSpan={6}>
<p className="text-sm text-scale-1200">No tables created yet</p>
<p className="text-sm text-light">
There are no tables found in the schema "{selectedSchema}"
There are no tables found in the schema "{snap.selectedSchemaName}"
</p>
</Table.td>
</Table.tr>
)}
{tables.length === 0 && filterString.length > 0 && (
<Table.tr key={selectedSchema}>
<Table.tr key={snap.selectedSchemaName}>
<Table.td colSpan={6}>
<p className="text-sm text-scale-1200">No results found</p>
<p className="text-sm text-light">
@@ -302,21 +328,6 @@ const TableList = ({
</Tooltip.Portal>
)}
</Tooltip.Root>
{/* <Button
type="text"
icon={<IconEdit3 />}
style={{ padding: 5 }}
disabled={isLocked}
onClick={() => onEditTable(x)}
/>
<Button
type="text"
icon={<IconTrash />}
style={{ padding: 5 }}
disabled={isLocked}
onClick={() => onDeleteTable(x)}
/> */}
</div>
</Table.td>
</Table.tr>
@@ -325,8 +336,7 @@ const TableList = ({
}
/>
</div>
)}
</div>
))}
</>
)
}
@@ -1,10 +1,17 @@
import { Dictionary } from 'components/grid'
import { has, isEmpty, mapValues, union, without } from 'lodash'
import { makeAutoObservable } from 'mobx'
import { observer, useLocalObservable } from 'mobx-react-lite'
import { useRouter } from 'next/router'
import { createContext, useContext, useEffect, useState } from 'react'
import SVG from 'react-inlinesvg'
import { Dictionary } from 'components/grid'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import ConfirmationModal from 'components/ui/ConfirmationModal'
import FormEmptyBox from 'components/ui/FormBoxEmpty'
import NoTableState from 'components/ui/States/NoTableState'
import { useTablesQuery } from 'data/tables/tables-query'
import { useStore } from 'hooks'
import { BASE_PATH } from 'lib/constants'
import {
Badge,
Button,
@@ -17,12 +24,6 @@ import {
Modal,
SidePanel,
} from 'ui'
import ConfirmationModal from 'components/ui/ConfirmationModal'
import FormEmptyBox from 'components/ui/FormBoxEmpty'
import NoTableState from 'components/ui/States/NoTableState'
import { useStore } from 'hooks'
import { BASE_PATH } from 'lib/constants'
import ChooseFunctionForm from './ChooseFunctionForm'
class CreateTriggerFormState {
@@ -256,28 +257,33 @@ interface CreateTriggerProps {
}
const CreateTrigger = ({ trigger, visible, setVisible }: CreateTriggerProps) => {
const { project } = useProjectContext()
const { ui, meta } = useStore()
const [isClosingPanel, setIsClosingPanel] = useState(false)
const _localState = useLocalObservable(() => new CreateTriggerStore())
_localState.meta = meta as any
// for the empty 'no tables' state link
const router = useRouter()
const { ref } = router.query
useTablesQuery(
{
projectRef: project?.ref,
connectionString: project?.connectionString,
},
{
onSuccess(tables) {
if (_localState.tables.length <= 0) {
_localState.setTables(tables)
}
},
}
)
useEffect(() => {
const fetchTables = async () => {
await (_localState!.meta as any)!.tables!.load()
const tables = (_localState!.meta as any)!.tables.list()
_localState.setTables(tables)
}
const fetchFunctions = async () => {
await (_localState.meta as any).functions.load()
const triggerFuncs = (_localState!.meta as any)!.functions.listTriggerFunctions()
_localState.setTriggerFunctions(triggerFuncs)
}
fetchTables()
fetchFunctions()
}, [])
@@ -387,8 +393,8 @@ const CreateTrigger = ({ trigger, visible, setVisible }: CreateTriggerProps) =>
</CreateTriggerContext.Provider>
<ConfirmationModal
visible={isClosingPanel}
header="Confirm to close"
buttonLabel="Confirm"
header="Discard changes"
buttonLabel="Discard"
onSelectCancel={() => setIsClosingPanel(false)}
onSelectConfirm={() => {
setIsClosingPanel(false)
@@ -1,17 +1,19 @@
import * as Tooltip from '@radix-ui/react-tooltip'
import { PostgresSchema } from '@supabase/postgres-meta'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { noop, partition } from 'lodash'
import { observer } from 'mobx-react-lite'
import { useState } from 'react'
import { Button, IconLock, IconSearch, Input, Listbox } from 'ui'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import AlphaPreview from 'components/to-be-cleaned/AlphaPreview'
import ProductEmptyState from 'components/to-be-cleaned/ProductEmptyState'
import Table from 'components/to-be-cleaned/Table'
import AlertError from 'components/ui/AlertError'
import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader'
import { useSchemasQuery } from 'data/database/schemas-query'
import { useCheckPermissions, useStore } from 'hooks'
import { EXCLUDED_SCHEMAS } from 'lib/constants/schemas'
import TriggerList from './TriggerList'
interface TriggersListProps {
@@ -25,15 +27,19 @@ const TriggersList = ({
editTrigger = noop,
deleteTrigger = noop,
}: TriggersListProps) => {
const { project } = useProjectContext()
const { meta } = useStore()
const [selectedSchema, setSelectedSchema] = useState<string>('public')
const [filterString, setFilterString] = useState<string>('')
const schemas: PostgresSchema[] = meta.schemas.list()
const [protectedSchemas, openSchemas] = partition(schemas, (schema) =>
meta.excludedSchemas.includes(schema?.name ?? '')
const { data: schemas } = useSchemasQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
})
const [protectedSchemas, openSchemas] = partition(schemas ?? [], (schema) =>
EXCLUDED_SCHEMAS.includes(schema?.name ?? '')
)
const schema = schemas.find((schema) => schema.name === selectedSchema)
const schema = schemas?.find((schema) => schema.name === selectedSchema)
const isLocked = protectedSchemas.some((s) => s.id === schema?.id)
const triggers = meta.triggers.list()
@@ -1,12 +1,13 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useQueryClient } from '@tanstack/react-query'
import { isEmpty } from 'lodash'
import { observer } from 'mobx-react-lite'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { useState } from 'react'
import { Button, Form, IconArrowLeft, IconEdit, IconExternalLink, IconTrash, Input } from 'ui'
import { useParams } from 'common/hooks'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import {
FormActions,
FormPanel,
@@ -15,10 +16,10 @@ import {
FormSectionLabel,
FormsContainer,
} from 'components/ui/Forms'
import { invalidateSchemasQuery } from 'data/database/schemas-query'
import { useFDWCreateMutation } from 'data/fdw/fdw-create-mutation'
import { useCheckPermissions, useStore } from 'hooks'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import { Button, Form, IconArrowLeft, IconEdit, IconExternalLink, IconTrash, Input } from 'ui'
import InputField from './InputField'
import WrapperTableEditor from './WrapperTableEditor'
import { WRAPPERS } from './Wrappers.constants'
@@ -27,7 +28,8 @@ import { makeValidateRequired } from './Wrappers.utils'
const CreateWrapper = () => {
const formId = 'create-wrapper-form'
const router = useRouter()
const { ui, meta } = useStore()
const queryClient = useQueryClient()
const { ui } = useStore()
const { ref, type } = useParams()
const { project } = useProjectContext()
const canCreateWrapper = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'wrappers')
@@ -46,7 +48,7 @@ const CreateWrapper = () => {
setNewTables([])
const hasNewSchema = newTables.some((table) => table.is_new_schema)
if (hasNewSchema) meta.schemas.load()
if (hasNewSchema) invalidateSchemasQuery(queryClient, ref)
router.push(`/project/${ref}/database/wrappers`)
},
@@ -1,48 +1,50 @@
import Link from 'next/link'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useQueryClient } from '@tanstack/react-query'
import { isEmpty } from 'lodash'
import { useEffect, useState } from 'react'
import { useRouter } from 'next/router'
import { observer } from 'mobx-react-lite'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { useEffect, useState } from 'react'
import { useParams } from 'common/hooks'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import {
FormActions,
FormPanel,
FormSection,
FormSectionContent,
FormSectionLabel,
FormsContainer,
} from 'components/ui/Forms'
import Loading from 'components/ui/Loading'
import { invalidateSchemasQuery } from 'data/database/schemas-query'
import { useFDWUpdateMutation } from 'data/fdw/fdw-update-mutation'
import { useFDWsQuery } from 'data/fdw/fdws-query'
import { useCheckPermissions, useImmutableValue, useStore } from 'hooks'
import { VaultSecret } from 'types'
import {
Button,
Form,
Input,
IconArrowLeft,
IconExternalLink,
IconEdit,
IconTrash,
IconExternalLink,
IconLoader,
IconTrash,
Input,
} from 'ui'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { VaultSecret } from 'types'
import { useCheckPermissions, useImmutableValue, useStore } from 'hooks'
import { useParams } from 'common/hooks'
import { useFDWsQuery } from 'data/fdw/fdws-query'
import { useFDWUpdateMutation } from 'data/fdw/fdw-update-mutation'
import InputField from './InputField'
import { WRAPPERS } from './Wrappers.constants'
import WrapperTableEditor from './WrapperTableEditor'
import { WRAPPERS } from './Wrappers.constants'
import {
convertKVStringArrayToJson,
formatWrapperTables,
makeValidateRequired,
convertKVStringArrayToJson,
} from './Wrappers.utils'
import Loading from 'components/ui/Loading'
import {
FormPanel,
FormActions,
FormSection,
FormSectionLabel,
FormSectionContent,
FormsContainer,
} from 'components/ui/Forms'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
const EditWrapper = () => {
const formId = 'edit-wrapper-form'
const router = useRouter()
const queryClient = useQueryClient()
const { ui, meta, vault } = useStore()
const { ref, id } = useParams()
const { project } = useProjectContext()
@@ -67,7 +69,7 @@ const EditWrapper = () => {
setWrapperTables([])
const hasNewSchema = wrapperTables.some((table) => table.is_new_schema)
if (hasNewSchema) meta.schemas.load()
if (hasNewSchema) invalidateSchemasQuery(queryClient, ref)
router.push(`/project/${ref}/database/wrappers`)
},
@@ -1,6 +1,8 @@
import ActionBar from 'components/interfaces/TableGridEditor/SidePanelEditor/ActionBar'
import { useStore } from 'hooks'
import { useEffect, useState } from 'react'
import ActionBar from 'components/interfaces/TableGridEditor/SidePanelEditor/ActionBar'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import { useSchemasQuery } from 'data/database/schemas-query'
import { Form, IconDatabase, IconPlus, Input, Listbox, Modal, SidePanel } from 'ui'
import WrapperDynamicColumns from './WrapperDynamicColumns'
import { Table, TableOption } from './Wrappers.types'
@@ -160,8 +162,11 @@ const TableForm = ({
onSubmit: OnSubmitFn
initialData: any
}) => {
const { meta } = useStore()
const schemas = meta.schemas.list()
const { project } = useProjectContext()
const { data: schemas } = useSchemasQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
})
const requiredOptions =
table.options.filter((option) => option.editable && option.required && !option.defaultValue) ??
@@ -208,8 +213,8 @@ const TableForm = ({
Create a new schema
</Listbox.Option>
<Modal.Separator />
{/* @ts-ignore */}
{schemas.map((schema: PostgresSchema) => {
{schemas?.map((schema) => {
return (
<Listbox.Option
key={schema.id}
@@ -28,7 +28,7 @@ const ResourceContent = ({
const resourcePaths = paths[`/${resourceId}`]
const resourceDefinition = definitions[resourceId]
const resourceMeta = resources[resourceId]
const description = resourceDefinition.description || null
const description = resourceDefinition?.description || null
const methods = Object.keys(resourcePaths).map((x) => x.toUpperCase())
const properties = Object.entries(resourceDefinition.properties || []).map(([id, val]: any) => ({
...val,
@@ -36,7 +36,7 @@ const ProjectCard = ({
<li className="col-span-1 list-none">
<CardButton
linkHref={rewriteHref ? rewriteHref : `/project/${projectRef}`}
className="h-44 !px-0"
className="h-44 !px-0 group"
title={
<div className="w-full justify-between space-y-1.5 px-6">
<p className="flex-shrink truncate text-base">{name}</p>
@@ -84,7 +84,7 @@ export const ProjectCardStatus = ({
<div>
<Alert_Shadcn_
variant={alertType}
className="border-0 rounded-none rounded-b-md my-2 mb-2.5 [&>svg]:w-[28px] [&>svg]:h-[28px] [&>svg]:mt-1 [&>svg]:p-1.5 [&>svg]:left-6 pl-6"
className="border-0 rounded-none rounded-b-md my-2 mb-2.5 [&>svg]:w-[28px] [&>svg]:h-[28px] [&>svg]:mt-1 [&>svg]:p-1.5 [&>svg]:left-6 pl-6 group-hover:bg-destructive-300 transition-colors"
>
{projectStatus === 'isPaused' || projectStatus === 'isPausing' ? (
<IconPauseCircle strokeWidth={2} />
@@ -31,7 +31,7 @@ Check the details below before proceeding
/>
</SidePanel.Content>
<SidePanel.Content className="flex flex-col gap-2">
<label className="text-sm text-scale-900">GitHub scope / organisation</label>
<label className="text-sm text-scale-900">GitHub scope / organization</label>
<div className="flex gap-3 w-full">
<Select icon={<IconGitHub />} className="w-full">
<Select.Option value="mildtomato">MildTomato</Select.Option>
@@ -1,16 +1,10 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { indexOf } from 'lodash'
import { observer } from 'mobx-react-lite'
import { useContext, useEffect } from 'react'
import { Form, IconAlertCircle, Input, InputNumber } from 'ui'
import { useEffect } from 'react'
import { useParams } from 'common/hooks'
import MultiSelect from 'components/ui/MultiSelect'
import { useProjectPostgrestConfigQuery } from 'data/config/project-postgrest-config-query'
import { useProjectPostgrestConfigUpdateMutation } from 'data/config/project-postgrest-config-update-mutation'
import { useCheckPermissions, useStore } from 'hooks'
import { PageContext } from 'pages/project/[ref]/settings/api'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import {
FormActions,
FormPanel,
@@ -18,12 +12,22 @@ import {
FormSectionContent,
FormSectionLabel,
} from 'components/ui/Forms'
import MultiSelect from 'components/ui/MultiSelect'
import { useProjectPostgrestConfigQuery } from 'data/config/project-postgrest-config-query'
import { useProjectPostgrestConfigUpdateMutation } from 'data/config/project-postgrest-config-update-mutation'
import { useSchemasQuery } from 'data/database/schemas-query'
import { useCheckPermissions, useStore } from 'hooks'
import { Form, IconAlertCircle, Input, InputNumber } from 'ui'
const PostgrestConfig = () => {
const PageState: any = useContext(PageContext)
const { ref: projectRef } = useParams()
const { ui } = useStore()
const { meta } = PageState
const { project } = useProjectContext()
const { data: schemas } = useSchemasQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
})
const { data: config, isError } = useProjectPostgrestConfigQuery({ projectRef })
const { mutate: updatePostgrestConfig, isLoading: isUpdating } =
@@ -53,15 +57,12 @@ const PostgrestConfig = () => {
const permanentSchema = ['public', 'storage']
const hiddenSchema = ['auth', 'pgbouncer', 'hooks', 'extensions']
const schema =
meta.schemas
.list(
(x: any) => {
const find = indexOf(hiddenSchema, x.name)
if (find < 0) return x
},
{ allSchemas: true }
)
.map((x: any) => {
schemas
?.filter((x) => {
const find = indexOf(hiddenSchema, x.name)
if (find < 0) return x
})
.map((x) => {
return {
id: x.id,
value: x.name,
@@ -1,24 +1,33 @@
import { useState } from 'react'
import { SidePanel, IconBookOpen } from 'ui'
import { useStore } from 'hooks'
import { useParams } from 'common/hooks'
import { ResourceContent } from '../Docs'
import LangSelector from '../Docs/LangSelector'
import GeneratingTypes from '../Docs/GeneratingTypes'
import ActionBar from './SidePanelEditor/ActionBar'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import { useProjectApiQuery } from 'data/config/project-api-query'
import { useProjectJsonSchemaQuery } from 'data/docs/project-json-schema-query'
import { useTableQuery } from 'data/tables/table-query'
import { snakeToCamel } from 'lib/helpers'
import { IconBookOpen, SidePanel } from 'ui'
import { ResourceContent } from '../Docs'
import GeneratingTypes from '../Docs/GeneratingTypes'
import LangSelector from '../Docs/LangSelector'
import ActionBar from './SidePanelEditor/ActionBar'
interface APIDocumentationPanelProps {
export interface APIDocumentationPanelProps {
visible: boolean
onClose: () => void
}
const APIDocumentationPanel = ({ visible, onClose }: APIDocumentationPanelProps) => {
const { meta } = useStore()
const { ref, page, id } = useParams()
const { project } = useProjectContext()
const { ref, page, id: _id } = useParams()
const id = _id ? Number(_id) : undefined
const { data: table } = useTableQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
id,
})
const {
data: jsonSchema,
error: jsonSchemaError,
@@ -32,7 +41,6 @@ const APIDocumentationPanel = ({ visible, onClose }: APIDocumentationPanelProps)
const [selectedLang, setSelectedLang] = useState<any>('js')
const [showApiKey, setShowApiKey] = useState<any>(DEFAULT_KEY)
const tables = meta.tables.list()
const autoApiService = {
...settings?.autoApiService,
endpoint: `${settings?.autoApiService.protocol ?? 'https'}://${
@@ -113,7 +121,7 @@ const APIDocumentationPanel = ({ visible, onClose }: APIDocumentationPanelProps)
<ResourceContent
autoApiService={autoApiService}
selectedLang={selectedLang}
resourceId={tables.find((table) => table.id === Number(id))?.name}
resourceId={table?.name}
resources={resources}
definitions={jsonSchema.definitions}
paths={jsonSchema.paths}
@@ -0,0 +1,239 @@
import { useQueryClient } from '@tanstack/react-query'
import Link from 'next/link'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import ConfirmationModal from 'components/ui/ConfirmationModal'
import { entityTypeKeys } from 'data/entity-types/keys'
import { sqlKeys } from 'data/sql/keys'
import { tableKeys } from 'data/tables/keys'
import { useGetTables } from 'data/tables/tables-query'
import { useStore, useUrlState } from 'hooks'
import { TableLike } from 'hooks/misc/useTable'
import { noop } from 'lib/void'
import { useTableEditorStateSnapshot } from 'state/table-editor'
import { Alert, Button, Checkbox, IconExternalLink, Modal } from 'ui'
export type DeleteConfirmationDialogsProps = {
projectRef?: string
selectedTable?: TableLike
onAfterDeleteTable?: (tables: TableLike[]) => void
}
const DeleteConfirmationDialogs = ({
projectRef,
selectedTable,
onAfterDeleteTable = noop,
}: DeleteConfirmationDialogsProps) => {
const { project } = useProjectContext()
const snap = useTableEditorStateSnapshot()
const [, setParams] = useUrlState({ arrayKeys: ['filter', 'sort'] })
const queryClient = useQueryClient()
const { meta, ui } = useStore()
const getTables = useGetTables({
projectRef: project?.ref,
connectionString: project?.connectionString,
})
const removeDeletedColumnFromFiltersAndSorts = (columnName: string) => {
setParams((prevParams) => {
const existingFilters = (prevParams?.filter ?? []) as string[]
const existingSorts = (prevParams?.sort ?? []) as string[]
return {
...prevParams,
filter: existingFilters.filter((filter: string) => {
const [column] = filter.split(':')
if (column !== columnName) return filter
}),
sort: existingSorts.filter((sort: string) => {
const [column] = sort.split(':')
if (column !== columnName) return sort
}),
}
})
}
const isDeleteWithCascade = snap.confirmationDialog?.isDeleteWithCascade ?? false
const onConfirmDeleteColumn = async () => {
if (!(snap.confirmationDialog?.type === 'column')) return
const selectedColumnToDelete = snap.confirmationDialog.column
try {
if (selectedColumnToDelete === undefined) return
const response: any = await meta.columns.del(selectedColumnToDelete.id, isDeleteWithCascade)
if (response.error) throw response.error
removeDeletedColumnFromFiltersAndSorts(selectedColumnToDelete.name)
ui.setNotification({
category: 'success',
message: `Successfully deleted column "${selectedColumnToDelete.name}"`,
})
await Promise.all([
queryClient.invalidateQueries(sqlKeys.query(project?.ref, ['foreign-key-constraints'])),
queryClient.invalidateQueries(
tableKeys.table(project?.ref, selectedColumnToDelete!.table_id)
),
queryClient.invalidateQueries(
sqlKeys.query(project?.ref, [selectedTable!.schema, selectedTable!.name])
),
queryClient.invalidateQueries(
sqlKeys.query(project?.ref, [
'table-definition',
selectedTable!.schema,
selectedTable!.name,
])
),
])
if (snap.selectedSchemaName) await meta.views.loadBySchema(snap.selectedSchemaName)
} catch (error: any) {
ui.setNotification({
category: 'error',
message: `Failed to delete ${selectedColumnToDelete!.name}: ${error.message}`,
})
} finally {
snap.closeConfirmationDialog()
}
}
const onConfirmDeleteTable = async () => {
if (!(snap.confirmationDialog?.type === 'table')) return
const selectedTableToDelete = selectedTable
try {
if (selectedTableToDelete === undefined) return
const response: any = await meta.tables.del(selectedTableToDelete.id, isDeleteWithCascade)
if (response.error) throw response.error
const tables = await getTables(snap.selectedSchemaName)
await queryClient.invalidateQueries(entityTypeKeys.list(projectRef))
onAfterDeleteTable(tables)
ui.setNotification({
category: 'success',
message: `Successfully deleted table "${selectedTableToDelete.name}"`,
})
if (snap.selectedSchemaName) await meta.views.loadBySchema(snap.selectedSchemaName)
} catch (error: any) {
ui.setNotification({
error,
category: 'error',
message: `Failed to delete ${selectedTableToDelete?.name}: ${error.message}`,
})
} finally {
snap.closeConfirmationDialog()
}
}
return (
<>
<ConfirmationModal
danger
size="small"
visible={snap.confirmationDialog?.type === 'column'}
header={`Confirm deletion of column "${
snap.confirmationDialog?.type === 'column' && snap.confirmationDialog.column.name
}"`}
buttonLabel="Delete"
buttonLoadingLabel="Deleting"
onSelectCancel={() => {
snap.closeConfirmationDialog()
}}
onSelectConfirm={onConfirmDeleteColumn}
>
<Modal.Content>
<div className="py-4 space-y-4">
<p className="text-sm text-scale-1100">
Are you sure you want to delete the selected column? This action cannot be undone.
</p>
<Checkbox
label="Drop column with cascade?"
description="Deletes the column and its dependent objects"
checked={isDeleteWithCascade}
onChange={() => snap.toggleConfirmationIsWithCascade()}
/>
{isDeleteWithCascade && (
<Alert
withIcon
variant="warning"
title="Warning: Dropping with cascade may result in unintended consequences"
>
<p className="mb-4">
All dependent objects will be removed, as will any objects that depend on them,
recursively.
</p>
<Link href="https://www.postgresql.org/docs/current/ddl-depend.html">
<a target="_blank" rel="noreferrer">
<Button size="tiny" type="default" icon={<IconExternalLink />}>
About dependency tracking
</Button>
</a>
</Link>
</Alert>
)}
</div>
</Modal.Content>
</ConfirmationModal>
<ConfirmationModal
danger
size="small"
visible={snap.confirmationDialog?.type === 'table'}
header={
<span className="break-words">{`Confirm deletion of table "${selectedTable?.name}"`}</span>
}
buttonLabel="Delete"
buttonLoadingLabel="Deleting"
onSelectCancel={() => {
snap.closeConfirmationDialog()
}}
onSelectConfirm={onConfirmDeleteTable}
>
<Modal.Content>
<div className="py-4 space-y-4">
<p className="text-sm text-scale-1100">
Are you sure you want to delete the selected table? This action cannot be undone.
</p>
<Checkbox
label="Drop table with cascade?"
description="Deletes the table and its dependent objects"
checked={isDeleteWithCascade}
onChange={() => snap.toggleConfirmationIsWithCascade(!isDeleteWithCascade)}
/>
{isDeleteWithCascade && (
<Alert
withIcon
variant="warning"
title="Warning: Dropping with cascade may result in unintended consequences"
>
<p className="mb-4">
All dependent objects will be removed, as will any objects that depend on them,
recursively.
</p>
<Link href="https://www.postgresql.org/docs/current/ddl-depend.html">
<a target="_blank" rel="noreferrer">
<Button size="tiny" type="default" icon={<IconExternalLink />}>
About dependency tracking
</Button>
</a>
</Link>
</Alert>
)}
</div>
</Modal.Content>
</ConfirmationModal>
</>
)
}
export default DeleteConfirmationDialogs
@@ -1,19 +1,17 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { observer } from 'mobx-react-lite'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import ProductEmptyState from 'components/to-be-cleaned/ProductEmptyState'
import { useEntityTypesQuery } from 'data/entity-types/entity-types-infinite-query'
import { useCheckPermissions, useLocalStorage, useStore } from 'hooks'
import { useCheckPermissions, useLocalStorage } from 'hooks'
import { EXCLUDED_SCHEMAS } from 'lib/constants/schemas'
import { useTableEditorStateSnapshot } from 'state/table-editor'
interface EmptyStateProps {
selectedSchema: string
onAddTable: () => void
}
export interface EmptyStateProps {}
const EmptyState = ({ selectedSchema, onAddTable }: EmptyStateProps) => {
const { meta } = useStore()
const isProtectedSchema = meta.excludedSchemas.includes(selectedSchema)
const EmptyState = ({}: EmptyStateProps) => {
const snap = useTableEditorStateSnapshot()
const isProtectedSchema = EXCLUDED_SCHEMAS.includes(snap.selectedSchemaName)
const canCreateTables =
useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'tables') && !isProtectedSchema
@@ -27,7 +25,7 @@ const EmptyState = ({ selectedSchema, onAddTable }: EmptyStateProps) => {
{
projectRef: project?.ref,
connectionString: project?.connectionString,
schema: selectedSchema,
schema: snap.selectedSchemaName,
sort,
},
{
@@ -43,7 +41,7 @@ const EmptyState = ({ selectedSchema, onAddTable }: EmptyStateProps) => {
<ProductEmptyState
title="Table Editor"
ctaButtonLabel={canCreateTables ? 'Create a new table' : undefined}
onClickCta={canCreateTables ? onAddTable : undefined}
onClickCta={canCreateTables ? snap.onAddTable : undefined}
>
<p className="text-sm text-scale-1100">There are no tables available in this schema.</p>
</ProductEmptyState>
@@ -52,7 +50,7 @@ const EmptyState = ({ selectedSchema, onAddTable }: EmptyStateProps) => {
<ProductEmptyState
title="Table Editor"
ctaButtonLabel={canCreateTables ? 'Create a new table' : undefined}
onClickCta={canCreateTables ? onAddTable : undefined}
onClickCta={canCreateTables ? snap.onAddTable : undefined}
>
<p className="text-sm text-scale-1100">
Select a table from the navigation panel on the left to view its data
@@ -65,4 +63,4 @@ const EmptyState = ({ selectedSchema, onAddTable }: EmptyStateProps) => {
)
}
export default observer(EmptyState)
export default EmptyState
@@ -4,18 +4,19 @@ import type {
PostgresTable,
PostgresType,
} from '@supabase/postgres-meta'
import { useParams } from 'common'
import { Dictionary } from 'components/grid'
import { isEmpty, noop } from 'lodash'
import Link from 'next/link'
import { useEffect, useState } from 'react'
import { Button, Checkbox, IconExternalLink, Input, SidePanel, Toggle } from 'ui'
import { useParams } from 'common'
import { Dictionary } from 'components/grid'
import { EncryptionKeySelector } from 'components/interfaces/Settings/Vault'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import { FormSection, FormSectionContent, FormSectionLabel } from 'components/ui/Forms'
import { useForeignKeyConstraintsQuery } from 'data/database/foreign-key-constraints-query'
import { useStore } from 'hooks'
import { EXCLUDED_SCHEMAS } from 'lib/constants/schemas'
import { Button, Checkbox, IconExternalLink, Input, SidePanel, Toggle } from 'ui'
import { ForeignKeySelector } from '..'
import ActionBar from '../ActionBar'
import { TEXT_TYPES } from '../SidePanelEditor.constants'
@@ -82,9 +83,7 @@ const ColumnEditor = ({
const foreignKeyMeta = data || []
const keys = vault.listKeys()
const enumTypes = meta.types.list(
(type: PostgresType) => !meta.excludedSchemas.includes(type.schema)
)
const enumTypes = meta.types.list((type: PostgresType) => !EXCLUDED_SCHEMAS.includes(type.schema))
const [pgsodiumExtension] = meta.extensions.list(
(ext: PostgresExtension) => ext.name.toLowerCase() === 'pgsodium'
@@ -1,7 +1,13 @@
import type { PostgresColumn, PostgresSchema, PostgresTable } from '@supabase/postgres-meta'
import { Dictionary } from 'components/grid'
import { find, get, isEmpty, sortBy } from 'lodash'
import { useEffect, useState } from 'react'
import { Dictionary } from 'components/grid'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import InformationBox from 'components/ui/InformationBox'
import { FOREIGN_KEY_CASCADE_ACTION } from 'data/database/database-query-constants'
import { useSchemasQuery } from 'data/database/schemas-query'
import { useTablesQuery } from 'data/tables/tables-query'
import {
AlertDescription_Shadcn_,
AlertTitle_Shadcn_,
@@ -13,10 +19,6 @@ import {
Listbox,
SidePanel,
} from 'ui'
import InformationBox from 'components/ui/InformationBox'
import { FOREIGN_KEY_CASCADE_ACTION } from 'data/database/database-query-constants'
import { useStore } from 'hooks'
import ActionBar from '../ActionBar'
import { ColumnField } from '../SidePanelEditor.types'
import { FOREIGN_KEY_CASCADE_OPTIONS } from './ForeignKeySelector.constants'
@@ -46,7 +48,7 @@ const ForeignKeySelector = ({
closePanel,
saveChanges,
}: ForeignKeySelectorProps) => {
const { meta } = useStore()
const { project } = useProjectContext()
const [errors, setErrors] = useState<any>({})
const [selectedForeignKey, setSelectedForeignKey] = useState<ForeignKey>({
schema: 'public',
@@ -56,10 +58,15 @@ const ForeignKeySelector = ({
updateAction: FOREIGN_KEY_CASCADE_ACTION.NO_ACTION,
})
const schemas = meta.schemas.list()
const tables = meta.tables.list(
(table: PostgresTable) => table.schema === selectedForeignKey.schema
)
const { data: schemas } = useSchemasQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
})
const { data: tables } = useTablesQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
schema: selectedForeignKey.schema,
})
const foreignKey = column?.foreignKey
const selectedTable: PostgresTable | undefined = find(tables, {
@@ -70,11 +77,6 @@ const ForeignKeySelector = ({
name: selectedForeignKey?.column,
})
useEffect(() => {
// make sure the public schemas are loaded initially
meta.tables.loadBySchema('public')
}, [])
useEffect(() => {
// Reset the state of the side panel
if (visible) {
@@ -101,7 +103,6 @@ const ForeignKeySelector = ({
}, [visible])
const updateSelectedSchema = (schema: string) => {
meta.tables.loadBySchema(schema)
const updatedForeignKey = {
schema,
table: '',
@@ -200,8 +201,6 @@ const ForeignKeySelector = ({
customFooter={
<ActionBar
backButtonLabel="Cancel"
// if the type of the two columns don't match, disable the save button
disableApply={!matchingColumnTypes}
applyButtonLabel="Save"
closePanel={closePanel}
applyFunction={onSaveChanges}
@@ -227,7 +226,7 @@ const ForeignKeySelector = ({
error={errors.schema}
onChange={(value: string) => updateSelectedSchema(value)}
>
{schemas.map((schema: PostgresSchema) => {
{schemas?.map((schema: PostgresSchema) => {
return (
<Listbox.Option
key={schema.id}
@@ -1,22 +1,22 @@
import { PostgresTable } from '@supabase/postgres-meta'
import { useState } from 'react'
import { DndProvider } from 'react-dnd'
import { HTML5Backend } from 'react-dnd-html5-backend'
import { parseSupaTable } from 'components/grid'
import { formatFilterURLParams, formatSortURLParams } from 'components/grid/SupabaseGrid.utils'
import RefreshButton from 'components/grid/components/header/RefreshButton'
import FilterPopover from 'components/grid/components/header/filter'
import SortPopover from 'components/grid/components/header/sort'
import { formatFilterURLParams, formatSortURLParams } from 'components/grid/SupabaseGrid.utils'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import { useTableRowsQuery } from 'data/table-rows/table-rows-query'
import { ForeignKeyConstraint } from 'data/database/foreign-key-constraints-query'
import { useStore } from 'hooks'
import { useTableRowsQuery } from 'data/table-rows/table-rows-query'
import { useTableQuery } from 'data/tables/table-query'
import { IconLoader, SidePanel } from 'ui'
import ActionBar from '../../ActionBar'
import { useEncryptedColumns } from './ForeignRowSelector.utils'
import SelectorGrid from './SelectorGrid'
import { useEffect, useState } from 'react'
import Pagination from './Pagination'
import SelectorGrid from './SelectorGrid'
export interface ForeignRowSelectorProps {
visible: boolean
@@ -31,23 +31,21 @@ const ForeignRowSelector = ({
onSelect,
closePanel,
}: ForeignRowSelectorProps) => {
const { meta } = useStore()
const { project } = useProjectContext()
const {
target_id: tableId,
target_id: _tableId,
target_schema: schemaName,
target_table: tableName,
target_columns: columnName,
} = foreignKey ?? {}
const tableId = _tableId ? Number(_tableId) : undefined
const table = tableId ? meta.tables.byId(tableId) : undefined
useEffect(() => {
if (!table && tableId) {
meta.tables.loadById(tableId)
}
}, [table, tableId])
const { data: table } = useTableQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
id: tableId,
})
const encryptedColumns = useEncryptedColumns({ schemaName, tableName })
@@ -1,23 +1,22 @@
import type { PostgresColumn, PostgresTable } from '@supabase/postgres-meta'
import { QueryKey, useQueryClient } from '@tanstack/react-query'
import { Dictionary } from 'components/grid'
import { find, isEmpty, isUndefined, noop } from 'lodash'
import { isEmpty, isUndefined, noop } from 'lodash'
import { useState } from 'react'
import { Modal } from 'ui'
import { Dictionary } from 'components/grid'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import ConfirmationModal from 'components/ui/ConfirmationModal'
import { entityTypeKeys } from 'data/entity-types/keys'
import { sqlKeys } from 'data/sql/keys'
import { useTableRowCreateMutation } from 'data/table-rows/table-row-create-mutation'
import { useTableRowUpdateMutation } from 'data/table-rows/table-row-update-mutation'
import { tableKeys } from 'data/tables/keys'
import { useStore, useUrlState } from 'hooks'
import { useTableEditorStateSnapshot } from 'state/table-editor'
import { Modal } from 'ui'
import { ColumnEditor, RowEditor, SpreadsheetImport, TableEditor } from '.'
import ForeignRowSelector, {
ForeignRowSelectorProps,
} from './RowEditor/ForeignRowSelector/ForeignRowSelector'
import ForeignRowSelector from './RowEditor/ForeignRowSelector/ForeignRowSelector'
import JsonEdit from './RowEditor/JsonEditor/JsonEditor'
import { JsonEditValue } from './RowEditor/RowEditor.types'
import {
ColumnField,
CreateColumnPayload,
@@ -27,20 +26,7 @@ import {
import { ImportContent } from './TableEditor/TableEditor.types'
export interface SidePanelEditorProps {
selectedSchema: string
selectedTable?: PostgresTable
selectedRowToEdit?: Dictionary<any>
selectedColumnToEdit?: PostgresColumn
selectedTableToEdit?: PostgresTable
selectedValueForJsonEdit?: JsonEditValue
selectedForeignKeyToEdit?: {
foreignKey: NonNullable<ForeignRowSelectorProps['foreignKey']>
row: any
column: any
}
sidePanelKey?: 'row' | 'column' | 'table' | 'json' | 'foreign-row-selector' | 'csv-import'
isDuplicating?: boolean
closePanel: () => void
onRowCreated?: (row: Dictionary<any>) => void
onRowUpdated?: (row: Dictionary<any>, idx: number) => void
@@ -51,21 +37,13 @@ export interface SidePanelEditorProps {
}
const SidePanelEditor = ({
selectedSchema,
selectedTable,
selectedRowToEdit,
selectedColumnToEdit,
selectedTableToEdit,
selectedValueForJsonEdit,
selectedForeignKeyToEdit,
sidePanelKey,
isDuplicating = false,
closePanel,
onRowCreated = noop,
onRowUpdated = noop,
onTableCreated = noop,
onColumnSaved = noop,
}: SidePanelEditorProps) => {
const snap = useTableEditorStateSnapshot()
const [_, setParams] = useUrlState({ arrayKeys: ['filter', 'sort'] })
const { meta, ui } = useStore()
const queryClient = useQueryClient()
@@ -73,7 +51,6 @@ const SidePanelEditor = ({
const [isEdited, setIsEdited] = useState<boolean>(false)
const [isClosingPanel, setIsClosingPanel] = useState<boolean>(false)
const tables = meta.tables.list()
const enumArrayColumns = (selectedTable?.columns ?? [])
.filter((column) => {
return (column?.enums ?? []).length > 0 && column.data_type.toLowerCase() === 'array'
@@ -84,6 +61,8 @@ const SidePanelEditor = ({
const { mutateAsync: createTableRows } = useTableRowCreateMutation()
const { mutateAsync: updateTableRow } = useTableRowUpdateMutation({
async onMutate({ projectRef, table, configuration, payload }) {
snap.closeSidePanel()
const primaryKeyColumns = new Set(Object.keys(configuration.identifiers))
const queryKey = sqlKeys.query(projectRef, [
table.schema,
@@ -192,12 +171,13 @@ const SidePanelEditor = ({
onComplete()
if (!saveRowError) {
setIsEdited(false)
closePanel()
snap.closeSidePanel()
}
}
const onSaveJSON = async (value: string | number) => {
if (selectedTable === undefined || selectedValueForJsonEdit === undefined) return
if (selectedTable === undefined || !(snap.sidePanel?.type === 'json')) return
const selectedValueForJsonEdit = snap.sidePanel.jsonValue
try {
const { row, column } = selectedValueForJsonEdit
@@ -213,7 +193,8 @@ const SidePanelEditor = ({
}
const onSaveForeignRow = async (value: any) => {
if (selectedTable === undefined || selectedForeignKeyToEdit === undefined) return
if (selectedTable === undefined || !(snap.sidePanel?.type === 'foreign-row-selector')) return
const selectedForeignKeyToEdit = snap.sidePanel.foreignKey
try {
const { row, column } = selectedForeignKeyToEdit
@@ -235,6 +216,8 @@ const SidePanelEditor = ({
configuration: { columnId?: string; isEncrypted: boolean; keyId?: string; keyName?: string },
resolve: any
) => {
const selectedColumnToEdit = snap.sidePanel?.type === 'column' && snap.sidePanel.column
const { columnId, ...securityConfig } = configuration
const response = isNewRecord
? await meta.createColumn(
@@ -261,9 +244,10 @@ const SidePanelEditor = ({
) {
reAddRenamedColumnSortAndFilter(selectedColumnToEdit.name, payload.name)
}
queryClient.invalidateQueries(sqlKeys.query(project?.ref, ['foreign-key-constraints']))
await Promise.all([
meta.tables.loadById(selectedTable!.id),
queryClient.invalidateQueries(sqlKeys.query(project?.ref, ['foreign-key-constraints'])),
queryClient.invalidateQueries(tableKeys.table(project?.ref, selectedTable!.id)),
queryClient.invalidateQueries(
sqlKeys.query(project?.ref, [selectedTable!.schema, selectedTable!.name])
),
@@ -278,7 +262,7 @@ const SidePanelEditor = ({
])
onColumnSaved(configuration.isEncrypted)
setIsEdited(false)
closePanel()
snap.closeSidePanel()
}
if (configuration.isEncrypted && selectedTable?.schema) {
@@ -329,8 +313,12 @@ const SidePanelEditor = ({
configuration
try {
if (isDuplicating) {
const duplicateTable = find(tables, { id: tableId }) as PostgresTable
if (
snap.sidePanel?.type === 'table' &&
snap.sidePanel.mode === 'duplicate' &&
selectedTable
) {
const duplicateTable = selectedTable
toastId = ui.setNotification({
category: 'loading',
@@ -344,7 +332,10 @@ const SidePanelEditor = ({
duplicateTable,
})
await queryClient.invalidateQueries(entityTypeKeys.list(project?.ref))
await Promise.all([
queryClient.invalidateQueries(tableKeys.list(project?.ref, table.schema)),
queryClient.invalidateQueries(entityTypeKeys.list(project?.ref)),
])
ui.setNotification({
id: toastId,
@@ -368,7 +359,10 @@ const SidePanelEditor = ({
importContent
)
await queryClient.invalidateQueries(entityTypeKeys.list(project?.ref))
await Promise.all([
queryClient.invalidateQueries(tableKeys.list(project?.ref, table.schema)),
queryClient.invalidateQueries(entityTypeKeys.list(project?.ref)),
])
ui.setNotification({
id: toastId,
@@ -377,15 +371,15 @@ const SidePanelEditor = ({
})
onTableCreated(table)
} else if (selectedTableToEdit) {
} else if (selectedTable) {
toastId = ui.setNotification({
category: 'loading',
message: `Updating table: ${selectedTableToEdit?.name}...`,
message: `Updating table: ${selectedTable?.name}...`,
})
const { table, hasError }: any = await meta.updateTable(
toastId,
selectedTableToEdit,
selectedTable,
payload,
columns,
isRealtimeEnabled
@@ -401,16 +395,17 @@ const SidePanelEditor = ({
queryClient.invalidateQueries(sqlKeys.query(project?.ref, ['foreign-key-constraints']))
await Promise.all([
queryClient.invalidateQueries(
sqlKeys.query(project?.ref, [selectedTableToEdit.schema, selectedTableToEdit.name])
sqlKeys.query(project?.ref, [selectedTable.schema, selectedTable.name])
),
queryClient.invalidateQueries(
sqlKeys.query(project?.ref, [
'table-definition',
selectedTableToEdit.schema,
selectedTableToEdit.name,
selectedTable.schema,
selectedTable.name,
])
),
queryClient.invalidateQueries(entityTypeKeys.list(project?.ref)),
queryClient.invalidateQueries(tableKeys.table(project?.ref, table.schema)),
])
ui.setNotification({
@@ -427,7 +422,7 @@ const SidePanelEditor = ({
if (!saveTableError) {
setIsEdited(false)
closePanel()
snap.closeSidePanel()
}
resolve()
@@ -507,14 +502,14 @@ const SidePanelEditor = ({
message: `Successfully imported ${rowCount} rows of data into ${selectedTable.name}`,
})
resolve()
closePanel()
snap.closeSidePanel()
}
const onClosePanel = () => {
if (isEdited) {
setIsClosingPanel(true)
} else {
closePanel()
snap.closeSidePanel()
}
}
@@ -522,9 +517,9 @@ const SidePanelEditor = ({
<>
{!isUndefined(selectedTable) && (
<RowEditor
row={selectedRowToEdit}
row={snap.sidePanel?.type === 'row' ? snap.sidePanel.row : undefined}
selectedTable={selectedTable}
visible={sidePanelKey === 'row'}
visible={snap.sidePanel?.type === 'row'}
closePanel={onClosePanel}
saveChanges={saveRow}
updateEditorDirty={() => setIsEdited(true)}
@@ -532,55 +527,71 @@ const SidePanelEditor = ({
)}
{!isUndefined(selectedTable) && (
<ColumnEditor
column={selectedColumnToEdit}
column={
snap.sidePanel?.type === 'column'
? (snap.sidePanel.column as PostgresColumn)
: undefined
}
selectedTable={selectedTable}
visible={sidePanelKey === 'column'}
visible={snap.sidePanel?.type === 'column'}
closePanel={onClosePanel}
saveChanges={saveColumn}
updateEditorDirty={() => setIsEdited(true)}
/>
)}
<TableEditor
table={selectedTableToEdit}
selectedSchema={selectedSchema}
isDuplicating={isDuplicating}
visible={sidePanelKey === 'table'}
table={
snap.sidePanel?.type === 'table' &&
(snap.sidePanel.mode === 'edit' || snap.sidePanel.mode === 'duplicate')
? selectedTable
: undefined
}
isDuplicating={snap.sidePanel?.type === 'table' && snap.sidePanel.mode === 'duplicate'}
visible={snap.sidePanel?.type === 'table'}
closePanel={onClosePanel}
saveChanges={saveTable}
updateEditorDirty={() => setIsEdited(true)}
/>
<JsonEdit
visible={sidePanelKey === 'json'}
column={selectedValueForJsonEdit?.column ?? ''}
jsonString={selectedValueForJsonEdit?.jsonString ?? ''}
visible={snap.sidePanel?.type === 'json'}
column={(snap.sidePanel?.type === 'json' && snap.sidePanel.jsonValue.column) || ''}
jsonString={(snap.sidePanel?.type === 'json' && snap.sidePanel.jsonValue.jsonString) || ''}
backButtonLabel="Cancel"
applyButtonLabel="Save changes"
closePanel={onClosePanel}
onSaveJSON={onSaveJSON}
/>
<ForeignRowSelector
key={`foreign-row-selector-${selectedForeignKeyToEdit?.foreignKey?.id ?? 'null'}`}
visible={sidePanelKey === 'foreign-row-selector'}
foreignKey={selectedForeignKeyToEdit?.foreignKey}
key={`foreign-row-selector-${
(snap.sidePanel?.type === 'foreign-row-selector' &&
snap.sidePanel.foreignKey.foreignKey.id) ||
'null'
}`}
visible={snap.sidePanel?.type === 'foreign-row-selector'}
foreignKey={
snap.sidePanel?.type === 'foreign-row-selector'
? snap.sidePanel.foreignKey.foreignKey
: undefined
}
closePanel={onClosePanel}
onSelect={onSaveForeignRow}
/>
<SpreadsheetImport
visible={sidePanelKey === 'csv-import'}
selectedTable={selectedTableToEdit}
visible={snap.sidePanel?.type === 'csv-import'}
selectedTable={selectedTable}
saveContent={onImportData}
closePanel={onClosePanel}
updateEditorDirty={setIsEdited}
/>
<ConfirmationModal
visible={isClosingPanel}
header="Confirm to close"
buttonLabel="Confirm"
header="Discard changes"
buttonLabel="Discard"
onSelectCancel={() => setIsClosingPanel(false)}
onSelectConfirm={() => {
setIsClosingPanel(false)
setIsEdited(false)
closePanel()
snap.closeSidePanel()
}}
>
<Modal.Content>
@@ -2,7 +2,7 @@ import type { PostgresTable } from '@supabase/postgres-meta'
interface HeaderTitleProps {
schema: string
table: PostgresTable
table?: { name: string }
isDuplicating: boolean
}
@@ -17,13 +17,13 @@ const HeaderTitle = ({ schema, table, isDuplicating }: HeaderTitleProps) => {
if (isDuplicating) {
return (
<>
Duplicate table <code className="text-sm">{table.name}</code>
Duplicate table <code className="text-sm">{table?.name}</code>
</>
)
}
return (
<>
Update table <code className="text-sm">{table.name}</code>
Update table <code className="text-sm">{table?.name}</code>
</>
)
}
@@ -2,12 +2,14 @@ import type { PostgresTable, PostgresType } from '@supabase/postgres-meta'
import { isEmpty, isUndefined, noop } from 'lodash'
import Link from 'next/link'
import { useEffect, useState } from 'react'
import { Alert, Badge, Button, Checkbox, IconBookOpen, Input, Modal, SidePanel } from 'ui'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import ConfirmationModal from 'components/ui/ConfirmationModal'
import { useForeignKeyConstraintsQuery } from 'data/database/foreign-key-constraints-query'
import { useStore } from 'hooks'
import { EXCLUDED_SCHEMAS } from 'lib/constants/schemas'
import { useTableEditorStateSnapshot } from 'state/table-editor'
import { Alert, Badge, Button, Checkbox, IconBookOpen, Input, Modal, SidePanel } from 'ui'
import { SpreadsheetImport } from '../'
import ActionBar from '../ActionBar'
import { ColumnField, CreateTablePayload, UpdateTablePayload } from '../SidePanelEditor.types'
@@ -23,9 +25,8 @@ import {
validateFields,
} from './TableEditor.utils'
interface TableEditorProps {
export interface TableEditorProps {
table?: PostgresTable
selectedSchema: string
isDuplicating: boolean
visible: boolean
closePanel: () => void
@@ -47,20 +48,18 @@ interface TableEditorProps {
const TableEditor = ({
table,
selectedSchema,
isDuplicating,
visible = false,
closePanel = noop,
saveChanges = noop,
updateEditorDirty = noop,
}: TableEditorProps) => {
const snap = useTableEditorStateSnapshot()
const { ui, meta } = useStore()
const { project } = useProjectContext()
const isNewRecord = isUndefined(table)
const enumTypes = meta.types.list(
(type: PostgresType) => !meta.excludedSchemas.includes(type.schema)
)
const enumTypes = meta.types.list((type: PostgresType) => !EXCLUDED_SCHEMAS.includes(type.schema))
const publications = meta.publications.list()
const realtimePublication = publications.find(
@@ -135,7 +134,7 @@ const TableEditor = ({
if (isEmpty(errors)) {
const payload: CreateTablePayload | UpdateTablePayload = {
name: tableFields.name,
schema: selectedSchema,
schema: snap.selectedSchemaName,
comment: tableFields.comment,
...(!isNewRecord && { rls_enabled: tableFields.isRLSEnabled }),
}
@@ -170,8 +169,9 @@ const TableEditor = ({
size="large"
key="TableEditor"
visible={visible}
// @ts-ignore
header={<HeaderTitle schema={selectedSchema} table={table} isDuplicating={isDuplicating} />}
header={
<HeaderTitle schema={snap.selectedSchemaName} table={table} isDuplicating={isDuplicating} />
}
className={`transition-all duration-100 ease-in ${isImportingSpreadsheet ? ' mr-32' : ''}`}
onCancel={closePanel}
onConfirm={() => (resolve: () => void) => onSaveChanges(resolve)}
@@ -297,7 +297,7 @@ const TableEditor = ({
<div className="space-y-10 py-6">
{!isDuplicating && (
<ColumnManagement
table={{ name: tableFields.name, schema: selectedSchema }}
table={{ name: tableFields.name, schema: snap.selectedSchemaName }}
columns={tableFields?.columns}
enumTypes={enumTypes}
isNewRecord={isNewRecord}
@@ -1,12 +1,12 @@
import type { PostgresColumn, PostgresRelationship, PostgresTable } from '@supabase/postgres-meta'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { QueryKey, useQueryClient } from '@tanstack/react-query'
import { useParams } from 'common'
import { find, isUndefined, noop } from 'lodash'
import { find, isUndefined } from 'lodash'
import { observer } from 'mobx-react-lite'
import { useRouter } from 'next/router'
import { useEffect, useRef, useState } from 'react'
import { useParams } from 'common'
import {
Dictionary,
SupaTable,
@@ -16,6 +16,7 @@ import {
} from 'components/grid'
import { ERROR_PRIMARY_KEY_NOTFOUND } from 'components/grid/constants'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import Connecting from 'components/ui/Loading/Loading'
import TwoOptionToggle from 'components/ui/TwoOptionToggle'
import { FOREIGN_KEY_CASCADE_ACTION } from 'data/database/database-query-constants'
import {
@@ -26,83 +27,39 @@ import { useProjectJsonSchemaQuery } from 'data/docs/project-json-schema-query'
import { ENTITY_TYPE } from 'data/entity-types/entity-type-constants'
import { sqlKeys } from 'data/sql/keys'
import { useTableRowUpdateMutation } from 'data/table-rows/table-row-update-mutation'
import { useCheckPermissions, useStore, useUrlState } from 'hooks'
import { useCheckPermissions, useLatest, useStore, useUrlState } from 'hooks'
import useEntityType from 'hooks/misc/useEntityType'
import { TableLike } from 'hooks/misc/useTable'
import { EXCLUDED_SCHEMAS } from 'lib/constants/schemas'
import { EMPTY_ARR } from 'lib/void'
import { useTableEditorStateSnapshot } from 'state/table-editor'
import { SchemaView } from 'types'
import APIDocumentationPanel from './APIDocumentationPanel'
import GridHeaderActions from './GridHeaderActions'
import NotFoundState from './NotFoundState'
import SidePanelEditor from './SidePanelEditor'
import { ForeignRowSelectorProps } from './SidePanelEditor/RowEditor/ForeignRowSelector/ForeignRowSelector'
import { JsonEditValue } from './SidePanelEditor/RowEditor/RowEditor.types'
import TableDefinition from './TableDefinition'
export interface TableGridEditorProps {
/** Theme for the editor */
theme?: 'dark' | 'light'
selectedSchema?: string
selectedTable: any // PostgresTable | SchemaView
/** Determines what side panel editor to show */
sidePanelKey?: 'row' | 'column' | 'table' | 'json' | 'foreign-row-selector' | 'csv-import'
/** Toggles if we're duplicating a table */
isDuplicating: boolean
/** Selected entities if we're editing a row, column or table */
selectedRowToEdit?: Dictionary<any>
selectedColumnToEdit?: PostgresColumn
selectedTableToEdit?: PostgresTable
selectedValueForJsonEdit?: JsonEditValue
selectedForeignKeyToEdit?: {
foreignKey: NonNullable<ForeignRowSelectorProps['foreignKey']>
row: any
column: any
}
onAddRow: () => void
onEditRow: (row: Dictionary<any>) => void
onAddColumn: () => void
onEditColumn: (column: PostgresColumn) => void
onDeleteColumn: (column: PostgresColumn) => void
onExpandJSONEditor: (column: string, row: any) => void
onEditForeignKeyColumnValue: (args: {
foreignKey: NonNullable<ForeignRowSelectorProps['foreignKey']>
row: any
column: any
}) => void
onClosePanel: () => void
onImportData: () => void
isLoadingSelectedTable?: boolean
selectedTable?: TableLike
}
const TableGridEditor = ({
theme = 'dark',
selectedSchema,
isLoadingSelectedTable = false,
selectedTable,
sidePanelKey,
isDuplicating,
selectedRowToEdit,
selectedColumnToEdit,
selectedTableToEdit,
selectedValueForJsonEdit,
selectedForeignKeyToEdit,
onAddRow = noop,
onEditRow = noop,
onAddColumn = noop,
onEditColumn = noop,
onDeleteColumn = noop,
onExpandJSONEditor = noop,
onEditForeignKeyColumnValue = noop,
onClosePanel = noop,
onImportData = noop,
}: TableGridEditorProps) => {
const { project } = useProjectContext()
const snap = useTableEditorStateSnapshot()
const { meta, ui, vault } = useStore()
const router = useRouter()
const { ref: projectRef, id } = useParams()
const gridRef = useRef<SupabaseGridRef>(null)
const { project } = useProjectContext()
const [encryptedColumns, setEncryptedColumns] = useState([])
const [apiPreviewPanelOpen, setApiPreviewPanelOpen] = useState(false)
@@ -201,34 +158,38 @@ const TableGridEditor = ({
}, [selectedTable?.id])
const entityType = useEntityType(selectedTable?.id)
const columnsRef = useLatest(selectedTable?.columns ?? EMPTY_ARR)
// NOTE: DO NOT PUT HOOKS AFTER THIS LINE
if (isLoadingSelectedTable) {
return <Connecting />
}
if (isUndefined(selectedTable)) {
return <NotFoundState id={Number(id)} />
}
const tableId = selectedTable?.id
const isViewSelected =
entityType?.type === ENTITY_TYPE.VIEW || entityType?.type === ENTITY_TYPE.MATERIALIZED_VIEW
const isTableSelected = entityType?.type === ENTITY_TYPE.TABLE
const isForeignTableSelected = entityType?.type === ENTITY_TYPE.FOREIGN_TABLE
const isLocked = meta.excludedSchemas.includes(entityType?.schema ?? '')
const isLocked = EXCLUDED_SCHEMAS.includes(entityType?.schema ?? '')
const canEditViaTableEditor = isTableSelected && !isLocked
// [Joshen] We can tweak below to eventually support composite keys as the data
// returned from foreignKeyMeta should be easy to deal with, rather than pg-meta
const formattedRelationships = (selectedTable?.relationships ?? []).map(
(relationship: PostgresRelationship) => {
const relationshipMeta = foreignKeyMeta.find(
(fk: ForeignKeyConstraint) => fk.id === relationship.id
)
return {
...relationship,
deletion_action: relationshipMeta?.deletion_action ?? FOREIGN_KEY_CASCADE_ACTION.NO_ACTION,
}
const formattedRelationships = (
('relationships' in selectedTable && selectedTable.relationships) ||
[]
).map((relationship: PostgresRelationship) => {
const relationshipMeta = foreignKeyMeta.find(
(fk: ForeignKeyConstraint) => fk.id === relationship.id
)
return {
...relationship,
deletion_action: relationshipMeta?.deletion_action ?? FOREIGN_KEY_CASCADE_ACTION.NO_ACTION,
}
)
})
const gridTable =
!isViewSelected && !isForeignTableSelected
@@ -275,27 +236,31 @@ const TableGridEditor = ({
}
}
const onSelectEditColumn = async (name: string) => {
// For some reason, selectedTable here is stale after adding a table
// temporary workaround is to list grab the selected table again
const tables: PostgresTable[] = meta.tables.list()
// @ts-ignore
const table = tables.find((table) => table.id === Number(tableId))
const column = find(table!.columns, { name }) as PostgresColumn
// columns must be accessed via columnsRef.current as these two functions immediately become
// stale as they are accessed via some react-tracked madness
// [TODO]: refactor out all of react-tracked
const onSelectEditColumn = (name: string) => {
const column = find(columnsRef.current, { name }) as PostgresColumn
if (column) {
onEditColumn(column)
snap.onEditColumn(column)
} else {
console.error(`Unable to find column ${name} in ${table?.name}`)
ui.setNotification({
category: 'error',
message: `Unable to find column ${name} in ${selectedTable?.name}`,
})
}
}
const onSelectDeleteColumn = async (name: string) => {
// For some reason, selectedTable here is stale after adding a table
// temporary workaround is to list grab the selected table again
const tables: PostgresTable[] = meta.tables.list()
const table = tables.find((table) => table.id === Number(tableId))
const column = find(table!.columns, { name }) as PostgresColumn
onDeleteColumn(column)
const onSelectDeleteColumn = (name: string) => {
const column = find(columnsRef.current ?? [], { name }) as PostgresColumn
if (column) {
snap.onDeleteColumn(column)
} else {
ui.setNotification({
category: 'error',
message: `Unable to find column ${name} in ${selectedTable?.name}`,
})
}
}
const onError = (error: any) => {
@@ -308,11 +273,14 @@ const TableGridEditor = ({
const updateTableRow = (previousRow: any, updatedData: any) => {
if (!project) return
const enumArrayColumns = selectedTable.columns
.filter((column: any) => {
return (column?.enums ?? []).length > 0 && column.data_type.toLowerCase() === 'array'
})
.map((column: any) => column.name)
const enumArrayColumns =
('columns' in selectedTable &&
selectedTable.columns
?.filter((column) => {
return (column?.enums ?? []).length > 0 && column.data_type.toLowerCase() === 'array'
})
.map((column) => column.name)) ||
[]
const identifiers = {} as Dictionary<any>
;(selectedTable as PostgresTable).primary_keys.forEach(
@@ -384,17 +352,19 @@ const TableGridEditor = ({
</>
) : null
}
onAddColumn={onAddColumn}
onAddColumn={snap.onAddColumn}
onEditColumn={onSelectEditColumn}
onDeleteColumn={onSelectDeleteColumn}
onAddRow={onAddRow}
onAddRow={snap.onAddRow}
updateTableRow={updateTableRow}
onEditRow={onEditRow}
onImportData={onImportData}
onEditRow={snap.onEditRow}
onImportData={snap.onImportData}
onError={onError}
onSqlQuery={onSqlQuery}
onExpandJSONEditor={onExpandJSONEditor}
onEditForeignKeyColumnValue={onEditForeignKeyColumnValue}
onExpandJSONEditor={(column, row) =>
snap.onExpandJSONEditor({ column, row, jsonString: JSON.stringify(row[column]) || '' })
}
onEditForeignKeyColumnValue={snap.onEditForeignKeyColumnValue}
showCustomChildren={(isViewSelected || isTableSelected) && selectedView === 'definition'}
customHeader={
(isViewSelected || isTableSelected) && selectedView === 'definition' ? (
@@ -410,22 +380,13 @@ const TableGridEditor = ({
{(isViewSelected || isTableSelected) && <TableDefinition id={selectedTable?.id} />}
</SupabaseGrid>
{!isUndefined(selectedSchema) && (
{snap.selectedSchemaName !== undefined && (
<SidePanelEditor
selectedSchema={selectedSchema}
isDuplicating={isDuplicating}
selectedTable={selectedTable as PostgresTable}
selectedRowToEdit={selectedRowToEdit}
selectedColumnToEdit={selectedColumnToEdit}
selectedTableToEdit={selectedTableToEdit}
selectedValueForJsonEdit={selectedValueForJsonEdit}
selectedForeignKeyToEdit={selectedForeignKeyToEdit}
sidePanelKey={sidePanelKey}
onRowCreated={onRowCreated}
onRowUpdated={onRowUpdated}
onColumnSaved={onColumnSaved}
onTableCreated={onTableCreated}
closePanel={onClosePanel}
/>
)}
@@ -1,9 +1,8 @@
import { observer } from 'mobx-react-lite'
import { useRouter } from 'next/router'
import { PropsWithChildren, useEffect, useState } from 'react'
import { PropsWithChildren, useEffect } from 'react'
import { useParams } from 'common'
import Error from 'components/ui/Error'
import ProductMenu from 'components/ui/ProductMenu'
import { useAuthConfigPrefetch } from 'data/auth/auth-config-query'
import { useStore, withAuth } from 'hooks'
@@ -15,45 +14,26 @@ export interface AuthLayoutProps {
}
const AuthLayout = ({ title, children }: PropsWithChildren<AuthLayoutProps>) => {
const { ui, meta } = useStore()
const { isInitialized, isLoading, error } = meta.tables
const { ref: projectRef = 'default' } = useParams()
const { ui, meta } = useStore()
useAuthConfigPrefetch({ projectRef })
const router = useRouter()
const page = router.pathname.split('/')[4]
const [loaded, setLoaded] = useState<boolean>(isInitialized)
useEffect(() => {
if (ui.selectedProjectRef) {
meta.policies.load()
meta.tables.load()
meta.roles.load()
meta.schemas.load()
}
}, [ui.selectedProjectRef])
useEffect(() => {
if (!isLoading && !loaded) {
setLoaded(true)
}
}, [isLoading])
if (error) {
return (
<ProjectLayout>
<Error error={error} />
</ProjectLayout>
)
}
return (
<ProjectLayout
isLoading={!loaded}
title={title || 'Authentication'}
product="Authentication"
productMenu={<ProductMenu page={page} menu={generateAuthMenu(projectRef)} />}
productMenu={<ProductMenu page={page} menu={generateAuthMenu(projectRef ?? 'default')} />}
>
<main style={{ maxHeight: '100vh' }} className="flex-1 overflow-y-auto">
{children}
@@ -1,8 +1,7 @@
import { observer } from 'mobx-react-lite'
import { useRouter } from 'next/router'
import { PropsWithChildren, useEffect, useState } from 'react'
import { PropsWithChildren, useEffect } from 'react'
import Error from 'components/ui/Error'
import ProductMenu from 'components/ui/ProductMenu'
import { useSelectedProject, useStore, withAuth } from 'hooks'
import ProjectLayout from '../'
@@ -14,10 +13,6 @@ export interface DatabaseLayoutProps {
const DatabaseLayout = ({ children }: PropsWithChildren<DatabaseLayoutProps>) => {
const { ui, meta, vault } = useStore()
const { isLoading: isSchemasLoading } = meta.schemas
const { isLoading: isVaultLoading } = vault
const { isInitialized, error } = meta.tables
const project = useSelectedProject()
const router = useRouter()
@@ -27,15 +22,8 @@ const DatabaseLayout = ({ children }: PropsWithChildren<DatabaseLayoutProps>) =>
const isVaultEnabled = vaultExtension !== undefined && vaultExtension.installed_version !== null
const pgNetExtensionExists = meta.extensions.byId('pg_net') !== undefined
const isLoading = isSchemasLoading || (isVaultEnabled && isVaultLoading)
const [loaded, setLoaded] = useState<boolean>(isInitialized)
useEffect(() => {
if (ui.selectedProjectRef) {
// Eventually should only load the required stores based on the pages
meta.schemas.load()
meta.tables.load()
meta.roles.load()
meta.triggers.load()
meta.extensions.load()
@@ -49,25 +37,8 @@ const DatabaseLayout = ({ children }: PropsWithChildren<DatabaseLayoutProps>) =>
}
}, [ui.selectedProjectRef, isVaultEnabled])
// Optimization required: load logic should be at the page level
// e.g backups page is waiting for meta.tables to load finish when it doesnt even need that data
useEffect(() => {
if (!isLoading && !loaded) {
setLoaded(true)
}
}, [isLoading])
if (error) {
return (
<ProjectLayout>
<Error error={error} />
</ProjectLayout>
)
}
return (
<ProjectLayout
isLoading={!loaded}
product="Database"
productMenu={
<ProductMenu page={page} menu={generateDatabaseMenu(project, { pgNetExtensionExists })} />
@@ -1,10 +1,12 @@
import { createContext, PropsWithChildren, useContext, useMemo } from 'react'
import { useParams } from 'common/hooks'
import { useProjectDetailQuery } from 'data/projects/project-detail-query'
import { PROJECT_STATUS } from 'lib/constants'
import { PropsWithChildren, createContext, useContext, useMemo } from 'react'
import { TableEditorStateContextProvider } from 'state/table-editor'
import { Project } from 'types'
type ProjectContextType = {
export interface ProjectContextType {
project: Project | undefined
isLoading: boolean
}
@@ -35,7 +37,13 @@ export const ProjectContextProvider = ({
}
}, [selectedProject, isLoading])
return <ProjectContext.Provider value={value}>{children}</ProjectContext.Provider>
return (
<ProjectContext.Provider value={value}>
<TableEditorStateContextProvider key={`table-editor-state-${projectRef}`}>
{children}
</TableEditorStateContextProvider>
</ProjectContext.Provider>
)
}
export const ProjectContextFromParamsProvider = ({ children }: PropsWithChildren<{}>) => {
@@ -6,28 +6,17 @@ import { Entity } from 'data/entity-types/entity-type-query'
import Link from 'next/link'
import { Dropdown, IconEdit, IconCopy, IconLock, IconTrash, IconChevronDown } from 'ui'
import { BASE_PATH } from 'lib/constants'
import { useTableEditorStateSnapshot } from 'state/table-editor'
export interface EntityListItemProps {
id: number
projectRef: string
item: Entity
isLocked: boolean
onEditTable: (table: Entity) => void
onDeleteTable: (table: Entity) => void
onDuplicateTable: (table: Entity) => void
isLoadingTableMetadata?: boolean
}
const EntityListItem = ({
id,
projectRef,
item: entity,
isLocked,
onEditTable,
onDeleteTable,
onDuplicateTable,
isLoadingTableMetadata,
}: EntityListItemProps) => {
const EntityListItem = ({ id, projectRef, item: entity, isLocked }: EntityListItemProps) => {
const snap = useTableEditorStateSnapshot()
const isActive = Number(id) === entity.id
const formatTooltipText = (entityType: string) => {
return Object.entries(ENTITY_TYPE)
@@ -137,9 +126,8 @@ const EntityListItem = ({
icon={<IconEdit size="tiny" />}
onClick={(e) => {
e.stopPropagation()
onEditTable(entity)
snap.onEditTable()
}}
disabled={isLoadingTableMetadata}
>
Edit Table
</Dropdown.Item>,
@@ -148,9 +136,8 @@ const EntityListItem = ({
icon={<IconCopy size="tiny" />}
onClick={(e) => {
e.stopPropagation()
onDuplicateTable(entity)
snap.onDuplicateTable()
}}
disabled={isLoadingTableMetadata}
>
Duplicate Table
</Dropdown.Item>,
@@ -170,9 +157,8 @@ const EntityListItem = ({
icon={<IconTrash size="tiny" />}
onClick={(e) => {
e.stopPropagation()
onDeleteTable(entity)
snap.onDeleteTable()
}}
disabled={isLoadingTableMetadata}
>
Delete Table
</Dropdown.Item>,
@@ -1,49 +1,15 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { noop } from 'lodash'
import { observer } from 'mobx-react-lite'
import { useRouter } from 'next/router'
import { PropsWithChildren, useEffect } from 'react'
import { useParams } from 'common/hooks'
import Connecting from 'components/ui/Loading/Loading'
import NoPermission from 'components/ui/NoPermission'
import { ENTITY_TYPE } from 'data/entity-types/entity-type-constants'
import { Entity } from 'data/entity-types/entity-type-query'
import { useCheckPermissions, useSelectedProject, useStore } from 'hooks'
import useEntityType from 'hooks/misc/useEntityType'
import useLatest from 'hooks/misc/useLatest'
import { useIsTableLoaded, useTableEditorStateSnapshot } from 'state/table-editor'
import ProjectLayout from '../'
import useTableRowsPrefetchWrapper from './TableEditorLayout.utils'
import { useCheckPermissions, useStore } from 'hooks'
import { ProjectLayoutWithAuth } from '../'
import TableEditorMenu from './TableEditorMenu'
export interface TableEditorLayoutProps {
selectedSchema?: string
selectedTable?: string
onSelectSchema: (schema: string) => void
onAddTable: () => void
onEditTable: (table: Entity) => void
onDeleteTable: (table: Entity) => void
onDuplicateTable: (table: Entity) => void
}
const TableEditorLayout = ({ children }: PropsWithChildren<{}>) => {
const { vault, meta, ui } = useStore()
const TableEditorLayout = ({
selectedSchema,
selectedTable,
onSelectSchema = noop,
onAddTable = noop,
onEditTable = noop,
onDeleteTable = noop,
onDuplicateTable = noop,
children,
}: PropsWithChildren<TableEditorLayoutProps>) => {
const { ui, vault, meta } = useStore()
const selectedProject = useSelectedProject()
const router = useRouter()
const { ref, id: _id } = useParams()
const id = _id ? Number(_id) : undefined
const snap = useTableEditorStateSnapshot()
const canReadTables = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_READ, 'tables')
const vaultExtension = meta.extensions.byId('supabase_vault')
@@ -51,7 +17,6 @@ const TableEditorLayout = ({
useEffect(() => {
if (ui.selectedProjectRef) {
meta.schemas.load()
meta.types.load()
meta.policies.load()
meta.publications.load()
@@ -59,87 +24,24 @@ const TableEditorLayout = ({
}
}, [ui.selectedProjectRef])
const isLoaded = useIsTableLoaded(ref, id)
const entity = useEntityType(id, function onNotFound(id) {
if (ref) snap.addLoadedId(ref, id)
})
const prefetch = useLatest(useTableRowsPrefetchWrapper())
useEffect(() => {
let mounted = true
function loadTable() {
if (entity?.type) {
switch (entity.type) {
case ENTITY_TYPE.MATERIALIZED_VIEW:
return meta.materializedViews.loadById(entity.id)
case ENTITY_TYPE.VIEW:
return meta.views.loadById(entity.id)
case ENTITY_TYPE.FOREIGN_TABLE:
return meta.foreignTables.loadById(entity.id)
default:
return meta.tables.loadById(entity.id)
}
}
}
loadTable()
?.then(async (entity: any) => {
await prefetch.current(entity)
return entity
})
.then((entity: any) => {
if (mounted && ref) {
snap.addLoadedId(ref, entity.id)
}
})
.catch(() => {
if (mounted && entity?.id && ref) {
snap.addLoadedId(ref, entity.id)
}
})
return () => {
mounted = false
}
}, [entity?.id])
useEffect(() => {
if (isVaultEnabled) {
vault.load()
}
}, [selectedProject?.ref, isVaultEnabled])
}, [ui.selectedProjectRef, isVaultEnabled])
if (!canReadTables) {
return (
<ProjectLayout>
<ProjectLayoutWithAuth>
<NoPermission isFullPage resourceText="view tables from this project" />
</ProjectLayout>
</ProjectLayoutWithAuth>
)
}
return (
<ProjectLayout
product="Table editor"
selectedTable={selectedTable}
productMenu={
<TableEditorMenu
selectedSchema={selectedSchema}
onSelectSchema={onSelectSchema}
onAddTable={onAddTable}
onEditTable={onEditTable}
onDeleteTable={onDeleteTable}
onDuplicateTable={onDuplicateTable}
/>
}
>
{router.isReady && id !== undefined ? isLoaded ? children : <Connecting /> : children}
</ProjectLayout>
<ProjectLayoutWithAuth product="Table editor" productMenu={<TableEditorMenu />}>
{children}
</ProjectLayoutWithAuth>
)
}
@@ -1,6 +1,15 @@
import * as Tooltip from '@radix-ui/react-tooltip'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { partition } from 'lodash'
import { useMemo, useState } from 'react'
import { noop, partition } from 'lodash'
import { observer } from 'mobx-react-lite'
import { useParams } from 'common/hooks'
import InfiniteList from 'components/ui/InfiniteList'
import { useSchemasQuery } from 'data/database/schemas-query'
import { useEntityTypesQuery } from 'data/entity-types/entity-types-infinite-query'
import { useCheckPermissions, useLocalStorage } from 'hooks'
import { EXCLUDED_SCHEMAS } from 'lib/constants/schemas'
import { useTableEditorStateSnapshot } from 'state/table-editor'
import {
Alert,
Button,
@@ -16,37 +25,12 @@ import {
Listbox,
Menu,
} from 'ui'
import * as Tooltip from '@radix-ui/react-tooltip'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import type { PostgresSchema } from '@supabase/postgres-meta'
import { useParams } from 'common/hooks'
import { useCheckPermissions, useStore, useLocalStorage } from 'hooks'
import { useEntityTypesQuery } from 'data/entity-types/entity-types-infinite-query'
import { Entity } from 'data/entity-types/entity-type-query'
import { useProjectContext } from '../ProjectLayout/ProjectContext'
import InfiniteList from 'components/ui/InfiniteList'
import EntityListItem from './EntityListItem'
export interface TableEditorMenuProps {
selectedSchema?: string
onSelectSchema: (schema: string) => void
onAddTable: () => void
onEditTable: (table: Entity) => void
onDeleteTable: (table: Entity) => void
onDuplicateTable: (table: Entity) => void
}
const TableEditorMenu = ({
selectedSchema,
onSelectSchema = noop,
onAddTable = noop,
onEditTable = noop,
onDeleteTable = noop,
onDuplicateTable = noop,
}: TableEditorMenuProps) => {
const { meta } = useStore()
const TableEditorMenu = () => {
const { id } = useParams()
const snap = useTableEditorStateSnapshot()
const [searchText, setSearchText] = useState<string>('')
const [sort, setSort] = useLocalStorage<'alphabetical' | 'grouped-alphabetical'>(
@@ -68,7 +52,7 @@ const TableEditorMenu = ({
{
projectRef: project?.ref,
connectionString: project?.connectionString,
schema: selectedSchema,
schema: snap.selectedSchemaName,
search: searchText || undefined,
sort,
},
@@ -83,18 +67,27 @@ const TableEditorMenu = ({
[data?.pages]
)
const schemas: PostgresSchema[] = meta.schemas.list()
const schema = schemas.find((schema) => schema.name === selectedSchema)
const canCreateTables = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'tables')
const {
data: schemas,
isLoading: isSchemasLoading,
isSuccess: isSchemasSuccess,
isError: isSchemasError,
error: schemasError,
refetch: refetchSchemas,
} = useSchemasQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
})
const isLoadingTableMetadata = id ? !meta.tables.byId(id) : true
const schema = schemas?.find((schema) => schema.name === snap.selectedSchemaName)
const canCreateTables = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'tables')
const refreshTables = async () => {
await refetch()
}
const [protectedSchemas, openSchemas] = partition(schemas, (schema) =>
meta.excludedSchemas.includes(schema?.name ?? '')
EXCLUDED_SCHEMAS.includes(schema?.name ?? '')
)
const isLocked = protectedSchemas.some((s) => s.id === schema?.id)
@@ -105,25 +98,29 @@ const TableEditorMenu = ({
>
{/* Schema selection dropdown */}
<div className="px-3 mx-4">
{!meta.schemas.isInitialized ? (
{isSchemasLoading && (
<div className="flex h-[26px] items-center space-x-3 rounded border border-gray-500 px-3">
<IconLoader className="animate-spin" size={12} />
<span className="text-xs text-scale-900">Loading schemas...</span>
</div>
) : meta.schemas.hasError ? (
)}
{isSchemasError && (
<Alert variant="warning" title="Failed to load schemas" className="!px-3 !py-3">
<p className="mb-2">Error: {meta.schemas?.error?.message}</p>
<Button type="default" size="tiny" onClick={() => meta.schemas.load()}>
<p className="mb-2">Error: {schemasError.message}</p>
<Button type="default" size="tiny" onClick={() => refetchSchemas()}>
Reload schemas
</Button>
</Alert>
) : (
)}
{isSchemasSuccess && (
<Listbox
size="tiny"
value={selectedSchema}
value={snap.selectedSchemaName}
onChange={(name: string) => {
setSearchText('')
onSelectSchema(name)
snap.setSelectedSchemaName(name)
}}
>
<Listbox.Option
@@ -188,7 +185,7 @@ const TableEditorMenu = ({
}
type="default"
style={{ justifyContent: 'start' }}
onClick={onAddTable}
onClick={snap.onAddTable}
>
<span>New table</span>
</Button>
@@ -352,10 +349,6 @@ const TableEditorMenu = ({
itemProps={{
projectRef: project?.ref,
id: Number(id),
onEditTable,
onDeleteTable,
onDuplicateTable,
isLoadingTableMetadata,
}}
getItemSize={() => 28}
hasNextPage={hasNextPage}
@@ -369,4 +362,4 @@ const TableEditorMenu = ({
)
}
export default observer(TableEditorMenu)
export default TableEditorMenu
+1
View File
@@ -1,4 +1,5 @@
export const databaseKeys = {
schemaList: (projectRef: string | undefined) => ['projects', projectRef, 'schemas'] as const,
backups: (projectRef: string | undefined) => [projectRef, 'database', 'backups'] as const,
poolingConfiguration: (projectRef: string | undefined) =>
[projectRef, 'database', 'pooling-configuration'] as const,
+46 -46
View File
@@ -1,61 +1,61 @@
import { useCallback } from 'react'
import { UseQueryOptions } from '@tanstack/react-query'
import { ExecuteSqlData, useExecuteSqlPrefetch, useExecuteSqlQuery } from '../sql/execute-sql-query'
import { QueryClient, useQuery, UseQueryOptions } from '@tanstack/react-query'
import { components } from 'data/api'
import { get } from 'data/fetchers'
import { ResponseError } from 'types'
export type Schema = {
name: string
}
export const getSchemasQuery = () => {
const sql = /* SQL */ `
SELECT nspname as name
FROM pg_namespace
WHERE
nspname not in ('information_schema', 'pg_catalog', 'pg_toast')
AND nspname not like 'pg_temp_%'
AND nspname not like 'pg_toast_temp_%'
AND has_schema_privilege(oid, 'CREATE, USAGE')
ORDER BY nspname;
`.trim()
return sql
}
import { databaseKeys } from './keys'
export type SchemasVariables = {
projectRef?: string
connectionString?: string
}
export type SchemasData = { result: Schema[] }
export type SchemasError = unknown
export type Schema = components['schemas']['PostgresSchema']
export const useSchemasQuery = <TData extends SchemasData = SchemasData>(
export async function getSchemas(
{ projectRef, connectionString }: SchemasVariables,
options: UseQueryOptions<ExecuteSqlData, SchemasError, TData> = {}
) => {
return useExecuteSqlQuery(
{
projectRef,
connectionString,
sql: getSchemasQuery(),
queryKey: ['schemas'],
signal?: AbortSignal
) {
if (!projectRef) {
throw new Error('projectRef is required')
}
let headers = new Headers()
if (connectionString) headers.set('x-connection-encrypted', connectionString)
const { data, error } = await get('/platform/pg-meta/{ref}/schemas', {
params: {
header: {
'x-connection-encrypted': connectionString!,
},
path: {
ref: projectRef,
},
},
options
)
headers: Object.fromEntries(headers),
signal,
})
if (error) {
throw error
}
return data
}
export const useSchemasPrefetch = () => {
const prefetch = useExecuteSqlPrefetch()
export type SchemasData = Awaited<ReturnType<typeof getSchemas>>
export type SchemasError = ResponseError
return useCallback(
({ projectRef, connectionString }: SchemasVariables) =>
prefetch({
projectRef,
connectionString,
sql: getSchemasQuery(),
queryKey: ['schemas'],
}),
[prefetch]
export const useSchemasQuery = <TData = SchemasData>(
{ projectRef, connectionString }: SchemasVariables,
{ enabled = true, ...options }: UseQueryOptions<SchemasData, SchemasError, TData> = {}
) =>
useQuery<SchemasData, SchemasError, TData>(
databaseKeys.schemaList(projectRef),
({ signal }) => getSchemas({ projectRef, connectionString }, signal),
{ enabled: enabled && typeof projectRef !== 'undefined', ...options }
)
export function invalidateSchemasQuery(client: QueryClient, projectRef: string | undefined) {
return client.invalidateQueries(databaseKeys.schemaList(projectRef))
}
@@ -0,0 +1,57 @@
import { PostgresTable } from '@supabase/postgres-meta'
import { useQuery, UseQueryOptions } from '@tanstack/react-query'
import { get } from 'lib/common/fetch'
import { API_URL } from 'lib/constants'
import { foreignTableKeys } from './keys'
export type ForeignTableVariables = {
id?: number
projectRef?: string
connectionString?: string
}
export type ForeignTable = PostgresTable
export type ForeignTableResponse = ForeignTable | { error?: any }
export async function getForeignTable(
{ id, projectRef, connectionString }: ForeignTableVariables,
signal?: AbortSignal
) {
if (!projectRef) {
throw new Error('projectRef is required')
}
if (!id) {
throw new Error('id is required')
}
let headers = new Headers()
if (connectionString) headers.set('x-connection-encrypted', connectionString)
const response = (await get(`${API_URL}/pg-meta/${projectRef}/foreign-tables?id=${id}`, {
headers: Object.fromEntries(headers),
signal,
})) as ForeignTableResponse
if ('error' in response) {
throw response.error
}
return response as PostgresTable
}
export type ForeignTableData = Awaited<ReturnType<typeof getForeignTable>>
export type ForeignTableError = unknown
export const useForeignTableQuery = <TData = ForeignTableData>(
{ projectRef, id }: ForeignTableVariables,
{ enabled = true, ...options }: UseQueryOptions<ForeignTableData, ForeignTableError, TData> = {}
) =>
useQuery<ForeignTableData, ForeignTableError, TData>(
foreignTableKeys.foreignTable(projectRef, id),
({ signal }) => getForeignTable({ projectRef, id }, signal),
{
enabled: enabled && typeof projectRef !== 'undefined' && typeof id !== 'undefined',
...options,
}
)
@@ -0,0 +1,49 @@
import { PostgresTable } from '@supabase/postgres-meta'
import { useQuery, UseQueryOptions } from '@tanstack/react-query'
import { get } from 'lib/common/fetch'
import { API_URL } from 'lib/constants'
import { ForeignTable } from './foreign-table-query'
import { foreignTableKeys } from './keys'
export type ForeignTablesVariables = {
projectRef?: string
connectionString?: string
}
export type ForeignTablesResponse = ForeignTable[] | { error?: any }
export async function getForeignTables(
{ projectRef, connectionString }: ForeignTablesVariables,
signal?: AbortSignal
) {
if (!projectRef) {
throw new Error('projectRef is required')
}
let headers = new Headers()
if (connectionString) headers.set('x-connection-encrypted', connectionString)
const response = (await get(`${API_URL}/pg-meta/${projectRef}/foreign-tables`, {
headers: Object.fromEntries(headers),
signal,
})) as ForeignTablesResponse
if (!Array.isArray(response) && response.error) {
throw response.error
}
return response as PostgresTable[]
}
export type ForeignTablesData = Awaited<ReturnType<typeof getForeignTables>>
export type ForeignTablesError = unknown
export const useForeignTablesQuery = <TData = ForeignTablesData>(
{ projectRef, connectionString }: ForeignTablesVariables,
{ enabled = true, ...options }: UseQueryOptions<ForeignTablesData, ForeignTablesError, TData> = {}
) =>
useQuery<ForeignTablesData, ForeignTablesError, TData>(
foreignTableKeys.list(projectRef),
({ signal }) => getForeignTables({ projectRef, connectionString }, signal),
{ enabled: enabled && typeof projectRef !== 'undefined', ...options }
)
+5
View File
@@ -0,0 +1,5 @@
export const foreignTableKeys = {
list: (projectRef: string | undefined) => ['projects', projectRef, 'foreignTables'] as const,
foreignTable: (projectRef: string | undefined, id: number | undefined) =>
['projects', projectRef, 'foreignTables', id] as const,
}
+5
View File
@@ -0,0 +1,5 @@
export const materializedViewKeys = {
list: (projectRef: string | undefined) => ['projects', projectRef, 'materializedViews'] as const,
materializedView: (projectRef: string | undefined, id: number | undefined) =>
['projects', projectRef, 'materializedViews', id] as const,
}
@@ -0,0 +1,60 @@
import { PostgresMaterializedView } from '@supabase/postgres-meta'
import { useQuery, UseQueryOptions } from '@tanstack/react-query'
import { get } from 'lib/common/fetch'
import { API_URL } from 'lib/constants'
import { materializedViewKeys } from './keys'
export type MaterializedViewVariables = {
id?: number
projectRef?: string
connectionString?: string
}
export type MaterializedView = PostgresMaterializedView
export type MaterializedViewResponse = MaterializedView | { error?: any }
export async function getMaterializedView(
{ id, projectRef, connectionString }: MaterializedViewVariables,
signal?: AbortSignal
) {
if (!projectRef) {
throw new Error('projectRef is required')
}
if (!id) {
throw new Error('id is required')
}
let headers = new Headers()
if (connectionString) headers.set('x-connection-encrypted', connectionString)
const response = (await get(`${API_URL}/pg-meta/${projectRef}/materialized-views?id=${id}`, {
headers: Object.fromEntries(headers),
signal,
})) as MaterializedViewResponse
if ('error' in response) {
throw response.error
}
return response as PostgresMaterializedView
}
export type MaterializedViewData = Awaited<ReturnType<typeof getMaterializedView>>
export type MaterializedViewError = unknown
export const useMaterializedViewQuery = <TData = MaterializedViewData>(
{ projectRef, connectionString, id }: MaterializedViewVariables,
{
enabled = true,
...options
}: UseQueryOptions<MaterializedViewData, MaterializedViewError, TData> = {}
) =>
useQuery<MaterializedViewData, MaterializedViewError, TData>(
materializedViewKeys.materializedView(projectRef, id),
({ signal }) => getMaterializedView({ projectRef, connectionString, id }, signal),
{
enabled: enabled && typeof projectRef !== 'undefined' && typeof id !== 'undefined',
...options,
}
)
@@ -0,0 +1,52 @@
import { PostgresMaterializedView } from '@supabase/postgres-meta'
import { useQuery, UseQueryOptions } from '@tanstack/react-query'
import { get } from 'lib/common/fetch'
import { API_URL } from 'lib/constants'
import { materializedViewKeys } from './keys'
import { MaterializedView } from './materialized-view-query'
export type MaterializedViewsVariables = {
projectRef?: string
connectionString?: string
}
export type MaterializedViewsResponse = MaterializedView[] | { error?: any }
export async function getMaterializedViews(
{ projectRef, connectionString }: MaterializedViewsVariables,
signal?: AbortSignal
) {
if (!projectRef) {
throw new Error('projectRef is required')
}
let headers = new Headers()
if (connectionString) headers.set('x-connection-encrypted', connectionString)
const response = (await get(`${API_URL}/pg-meta/${projectRef}/materialized-views`, {
headers: Object.fromEntries(headers),
signal,
})) as MaterializedViewsResponse
if (!Array.isArray(response) && response.error) {
throw response.error
}
return response as PostgresMaterializedView[]
}
export type MaterializedViewsData = Awaited<ReturnType<typeof getMaterializedViews>>
export type MaterializedViewsError = unknown
export const useMaterializedViewsQuery = <TData = MaterializedViewsData>(
{ projectRef, connectionString }: MaterializedViewsVariables,
{
enabled = true,
...options
}: UseQueryOptions<MaterializedViewsData, MaterializedViewsError, TData> = {}
) =>
useQuery<MaterializedViewsData, MaterializedViewsError, TData>(
materializedViewKeys.list(projectRef),
({ signal }) => getMaterializedViews({ projectRef, connectionString }, signal),
{ enabled: enabled && typeof projectRef !== 'undefined', ...options }
)
@@ -35,3 +35,16 @@ export const useProjectDetailQuery = <TData = ProjectDetailData>(
export function invalidateProjectDetailsQuery(client: QueryClient, ref: string) {
return client.invalidateQueries(projectKeys.detail(ref))
}
// get the cached value or fallback to fetching it
export async function getCachedProjectDetail(
client: QueryClient,
ref: string | undefined
): Promise<ProjectDetailData | undefined> {
if (!ref) return undefined
const cached = client.getQueryData<ProjectDetailData>(projectKeys.detail(ref))
if (cached) return cached
return await client.fetchQuery<ProjectDetailData, ProjectDetailError>(projectKeys.detail(ref))
}
+6
View File
@@ -0,0 +1,6 @@
export const tableKeys = {
list: (projectRef: string | undefined, schema: string | undefined) =>
['projects', projectRef, 'tables', { schema }] as const,
table: (projectRef: string | undefined, id: number | undefined) =>
['projects', projectRef, 'tables', id] as const,
}
+79
View File
@@ -0,0 +1,79 @@
import { PostgresTable } from '@supabase/postgres-meta'
import { useQuery, useQueryClient, UseQueryOptions } from '@tanstack/react-query'
import { get } from 'lib/common/fetch'
import { API_URL } from 'lib/constants'
import { tableKeys } from './keys'
import { useCallback } from 'react'
export type TableVariables = {
id?: number
projectRef?: string
connectionString?: string
}
export type Table = PostgresTable
export type TableResponse = Table | { error?: any }
export async function getTable(
{ id, projectRef, connectionString }: TableVariables,
signal?: AbortSignal
) {
if (!projectRef) {
throw new Error('projectRef is required')
}
if (!id) {
throw new Error('id is required')
}
let headers = new Headers()
if (connectionString) headers.set('x-connection-encrypted', connectionString)
const response = (await get(`${API_URL}/pg-meta/${projectRef}/tables?id=${id}`, {
headers: Object.fromEntries(headers),
signal,
})) as TableResponse
if ('error' in response) {
throw response.error
}
return response as PostgresTable
}
export type TableData = Awaited<ReturnType<typeof getTable>>
export type TableError = unknown
export const useTableQuery = <TData = TableData>(
{ projectRef, connectionString, id }: TableVariables,
{ enabled = true, ...options }: UseQueryOptions<TableData, TableError, TData> = {}
) =>
useQuery<TableData, TableError, TData>(
tableKeys.table(projectRef, id),
({ signal }) => getTable({ projectRef, connectionString, id }, signal),
{
enabled: enabled && typeof projectRef !== 'undefined' && typeof id !== 'undefined',
...options,
}
)
/**
* useGetTable
* Tries to get a table from the react-query cache, or loads it from the server if it's not cached.
*/
export function useGetTable({
projectRef,
connectionString,
}: Pick<TableVariables, 'projectRef' | 'connectionString'>) {
const queryClient = useQueryClient()
return useCallback(
(id: NonNullable<TableVariables['id']>) => {
return queryClient.fetchQuery({
queryKey: tableKeys.table(projectRef, id),
queryFn: ({ signal }) => getTable({ id, projectRef, connectionString }, signal),
})
},
[connectionString, projectRef, queryClient]
)
}
+80
View File
@@ -0,0 +1,80 @@
import { PostgresTable } from '@supabase/postgres-meta'
import { useQuery, useQueryClient, UseQueryOptions } from '@tanstack/react-query'
import { get } from 'lib/common/fetch'
import { API_URL } from 'lib/constants'
import { useCallback } from 'react'
import { ResponseError } from 'types'
import { tableKeys } from './keys'
import { Table } from './table-query'
export type TablesVariables = {
projectRef?: string
connectionString?: string
schema?: string
}
export type TablesResponse = Table[] | { error?: any }
export async function getTables(
{ projectRef, connectionString, schema }: TablesVariables,
signal?: AbortSignal
) {
if (!projectRef) {
throw new Error('projectRef is required')
}
let headers = new Headers()
if (connectionString) headers.set('x-connection-encrypted', connectionString)
let queryParams = new URLSearchParams()
if (schema) queryParams.set('included_schemas', schema)
const searchStr = queryParams.toString()
const response = (await get(
`${API_URL}/pg-meta/${projectRef}/tables${searchStr ? `?${searchStr}` : ''}`,
{
headers: Object.fromEntries(headers),
signal,
}
)) as TablesResponse
if (!Array.isArray(response) && response.error) {
throw response.error
}
return response as PostgresTable[]
}
export type TablesData = Awaited<ReturnType<typeof getTables>>
export type TablesError = ResponseError
export const useTablesQuery = <TData = TablesData>(
{ projectRef, connectionString, schema }: TablesVariables,
{ enabled = true, ...options }: UseQueryOptions<TablesData, TablesError, TData> = {}
) =>
useQuery<TablesData, TablesError, TData>(
tableKeys.list(projectRef, schema),
({ signal }) => getTables({ projectRef, connectionString, schema }, signal),
{ enabled: enabled && typeof projectRef !== 'undefined', ...options }
)
/**
* useGetTables
* Tries to get tables from the react-query cache, or loads it from the server if it's not cached.
*/
export function useGetTables({
projectRef,
connectionString,
}: Pick<TablesVariables, 'projectRef' | 'connectionString'>) {
const queryClient = useQueryClient()
return useCallback(
(schema?: TablesVariables['schema']) => {
return queryClient.fetchQuery({
queryKey: tableKeys.list(projectRef, schema),
queryFn: ({ signal }) => getTables({ projectRef, connectionString, schema }, signal),
})
},
[connectionString, projectRef, queryClient]
)
}
+5
View File
@@ -0,0 +1,5 @@
export const viewKeys = {
list: (projectRef: string | undefined) => ['projects', projectRef, 'views'] as const,
view: (projectRef: string | undefined, id: number | undefined) =>
['projects', projectRef, 'views', id] as const,
}
+57
View File
@@ -0,0 +1,57 @@
import { PostgresView } from '@supabase/postgres-meta'
import { useQuery, UseQueryOptions } from '@tanstack/react-query'
import { get } from 'lib/common/fetch'
import { API_URL } from 'lib/constants'
import { viewKeys } from './keys'
export type ViewVariables = {
id?: number
projectRef?: string
connectionString?: string
}
export type View = PostgresView
export type ViewResponse = View | { error?: any }
export async function getView(
{ id, projectRef, connectionString }: ViewVariables,
signal?: AbortSignal
) {
if (!projectRef) {
throw new Error('projectRef is required')
}
if (!id) {
throw new Error('id is required')
}
let headers = new Headers()
if (connectionString) headers.set('x-connection-encrypted', connectionString)
const response = (await get(`${API_URL}/pg-meta/${projectRef}/views?id=${id}`, {
headers: Object.fromEntries(headers),
signal,
})) as ViewResponse
if ('error' in response) {
throw response.error
}
return response as PostgresView
}
export type ViewData = Awaited<ReturnType<typeof getView>>
export type ViewError = unknown
export const useViewQuery = <TData = ViewData>(
{ projectRef, connectionString, id }: ViewVariables,
{ enabled = true, ...options }: UseQueryOptions<ViewData, ViewError, TData> = {}
) =>
useQuery<ViewData, ViewError, TData>(
viewKeys.view(projectRef, id),
({ signal }) => getView({ projectRef, connectionString, id }, signal),
{
enabled: enabled && typeof projectRef !== 'undefined' && typeof id !== 'undefined',
...options,
}
)
+49
View File
@@ -0,0 +1,49 @@
import { PostgresView } from '@supabase/postgres-meta'
import { useQuery, UseQueryOptions } from '@tanstack/react-query'
import { get } from 'lib/common/fetch'
import { API_URL } from 'lib/constants'
import { viewKeys } from './keys'
import { View } from './view-query'
export type ViewsVariables = {
projectRef?: string
connectionString?: string
}
export type ViewsResponse = View[] | { error?: any }
export async function getViews(
{ projectRef, connectionString }: ViewsVariables,
signal?: AbortSignal
) {
if (!projectRef) {
throw new Error('projectRef is required')
}
let headers = new Headers()
if (connectionString) headers.set('x-connection-encrypted', connectionString)
const response = (await get(`${API_URL}/pg-meta/${projectRef}/views`, {
headers: Object.fromEntries(headers),
signal,
})) as ViewsResponse
if (!Array.isArray(response) && response.error) {
throw response.error
}
return response as PostgresView[]
}
export type ViewsData = Awaited<ReturnType<typeof getViews>>
export type ViewsError = unknown
export const useViewsQuery = <TData = ViewsData>(
{ projectRef, connectionString }: ViewsVariables,
{ enabled = true, ...options }: UseQueryOptions<ViewsData, ViewsError, TData> = {}
) =>
useQuery<ViewsData, ViewsError, TData>(
viewKeys.list(projectRef),
({ signal }) => getViews({ projectRef, connectionString }, signal),
{ enabled: enabled && typeof projectRef !== 'undefined', ...options }
)
+81
View File
@@ -0,0 +1,81 @@
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import { ENTITY_TYPE } from 'data/entity-types/entity-type-constants'
import { ForeignTable, useForeignTableQuery } from 'data/foreign-tables/foreign-table-query'
import {
MaterializedView,
useMaterializedViewQuery,
} from 'data/materialized-views/materialized-view-query'
import { Table, useTableQuery } from 'data/tables/table-query'
import { View, useViewQuery } from 'data/views/view-query'
import useEntityType from './useEntityType'
export type TableLike = Table | View | MaterializedView | ForeignTable
/**
* A hook that loads all table-like objects. e.g. tables, views, materialized views, etc...
*/
function useTable(id?: number) {
const { project } = useProjectContext()
const entity = useEntityType(id)
const tableResult = useTableQuery(
{
projectRef: project?.ref,
connectionString: project?.connectionString,
id: entity?.id,
},
{
enabled: entity?.type === ENTITY_TYPE.TABLE,
}
)
const viewResult = useViewQuery(
{
projectRef: project?.ref,
connectionString: project?.connectionString,
id: entity?.id,
},
{
enabled: entity?.type === ENTITY_TYPE.VIEW,
}
)
const materializedViewResult = useMaterializedViewQuery(
{
projectRef: project?.ref,
connectionString: project?.connectionString,
id: entity?.id,
},
{
enabled: entity?.type === ENTITY_TYPE.MATERIALIZED_VIEW,
}
)
const foreignTableResult = useForeignTableQuery(
{
projectRef: project?.ref,
connectionString: project?.connectionString,
id: entity?.id,
},
{
enabled: entity?.type === ENTITY_TYPE.FOREIGN_TABLE,
}
)
switch (entity?.type) {
case ENTITY_TYPE.TABLE:
return tableResult
case ENTITY_TYPE.VIEW:
return viewResult
case ENTITY_TYPE.MATERIALIZED_VIEW:
return materializedViewResult
case ENTITY_TYPE.FOREIGN_TABLE:
return foreignTableResult
default:
// While entity is loading, we can fallback
// to the loading state of the table query.
return tableResult
}
}
export default useTable
+20
View File
@@ -0,0 +1,20 @@
/**
* A list of system schemas that users should not interact with
*/
export const EXCLUDED_SCHEMAS = [
'auth',
'extensions',
'information_schema',
'net',
'pgsodium',
'pgsodium_masks',
'pgbouncer',
'pgtle',
'realtime',
'storage',
'supabase_functions',
'supabase_migrations',
'vault',
'graphql',
'graphql_public',
]
@@ -34,7 +34,6 @@ const GraphiQLPage: NextPageWithLayout = () => {
useEffect(() => {
if (ui.selectedProjectRef) {
// Schemas may be needed when enabling the GraphQL extension
meta.schemas.load()
meta.extensions.load()
}
}, [ui.selectedProjectRef])
+96 -56
View File
@@ -1,16 +1,23 @@
import { useState, useEffect } from 'react'
import { partition } from 'lodash'
import { Button, Listbox, IconSearch, Input, IconExternalLink, IconLock } from 'ui'
import { observer } from 'mobx-react-lite'
import { PostgresTable, PostgresPolicy } from '@supabase/postgres-meta'
import { PostgresPolicy, PostgresTable } from '@supabase/postgres-meta'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { partition } from 'lodash'
import { observer } from 'mobx-react-lite'
import { useEffect, useState } from 'react'
import { NextPageWithLayout } from 'types'
import { useCheckPermissions, useStore } from 'hooks'
import { useParams } from 'common/hooks'
import { AuthLayout } from 'components/layouts'
import { Policies } from 'components/interfaces/Auth/Policies'
import { AuthLayout } from 'components/layouts'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import AlertError from 'components/ui/AlertError'
import NoPermission from 'components/ui/NoPermission'
import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader'
import { useSchemasQuery } from 'data/database/schemas-query'
import { useTablesQuery } from 'data/tables/tables-query'
import { useCheckPermissions, useStore } from 'hooks'
import { EXCLUDED_SCHEMAS } from 'lib/constants/schemas'
import { useTableEditorStateSnapshot } from 'state/table-editor'
import { NextPageWithLayout } from 'types'
import { Button, IconExternalLink, IconLock, IconSearch, Input, Listbox } from 'ui'
/**
* Filter tables by table name and policy name
@@ -48,27 +55,47 @@ const onFilterTables = (
}
const AuthPoliciesPage: NextPageWithLayout = () => {
const { project } = useProjectContext()
const snap = useTableEditorStateSnapshot()
const { meta } = useStore()
const { search } = useParams()
const [selectedSchema, setSelectedSchema] = useState<string>('public')
const [searchString, setSearchString] = useState<string>('')
useEffect(() => {
if (search) setSearchString(search)
}, [search])
const schemas = meta.schemas.list()
const {
data: schemas,
isLoading: isLoadingSchemas,
isSuccess: isSuccessSchemas,
isError: isErrorSchemas,
error: errorSchemas,
} = useSchemasQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
})
const [protectedSchemas, openSchemas] = partition(schemas, (schema) =>
meta.excludedSchemas.includes(schema?.name ?? '')
EXCLUDED_SCHEMAS.includes(schema?.name ?? '')
)
// @ts-ignore
const schema = schemas.find((schema) => schema.name === selectedSchema)
const schema = schemas?.find((schema) => schema.name === snap.selectedSchemaName)
const isLocked = protectedSchemas.some((s) => s.id === schema?.id)
const policies = meta.policies.list()
const tables = meta.tables.list((table: { schema: string }) => table.schema === selectedSchema)
const filteredTables = onFilterTables(tables, policies, searchString)
const {
data: tables,
isLoading,
isSuccess,
isError,
error,
} = useTablesQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
schema: snap.selectedSchemaName,
})
const filteredTables = onFilterTables(tables ?? [], policies, searchString)
const canReadPolicies = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_READ, 'policies')
@@ -82,53 +109,62 @@ const AuthPoliciesPage: NextPageWithLayout = () => {
<div className="flex items-center justify-between">
<div className="flex items-center space-x-4">
<div className="w-[230px]">
<Listbox
size="small"
value={selectedSchema}
onChange={(schema: string) => {
setSelectedSchema(schema)
setSearchString('')
}}
icon={isLocked && <IconLock size={14} strokeWidth={2} />}
>
<Listbox.Option
disabled
key="normal-schemas"
value="normal-schemas"
label="Schemas"
{isLoadingSchemas && (
<div className="h-[34px] w-full bg-scale-1000 rounded shimmering-loader" />
)}
{isErrorSchemas && (
<AlertError error={errorSchemas} subject="Failed to retrieve schemas" />
)}
{isSuccessSchemas && (
<Listbox
size="small"
value={snap.selectedSchemaName}
onChange={(schema: string) => {
snap.setSelectedSchemaName(schema)
setSearchString('')
}}
icon={isLocked && <IconLock size={14} strokeWidth={2} />}
>
<p className="text-sm">Schemas</p>
</Listbox.Option>
{/* @ts-ignore */}
{openSchemas.map((schema) => (
<Listbox.Option
key={schema.id}
value={schema.name}
label={schema.name}
addOnBefore={() => <span className="text-scale-900">schema</span>}
disabled
key="normal-schemas"
value="normal-schemas"
label="Schemas"
>
<span className="text-scale-1200 text-sm">{schema.name}</span>
<p className="text-sm">Schemas</p>
</Listbox.Option>
))}
<Listbox.Option
disabled
key="protected-schemas"
value="protected-schemas"
label="Protected schemas"
>
<p className="text-sm">Protected schemas</p>
</Listbox.Option>
{protectedSchemas.map((schema) => (
{openSchemas.map((schema) => (
<Listbox.Option
key={schema.id}
value={schema.name}
label={schema.name}
addOnBefore={() => <span className="text-scale-900">schema</span>}
>
<span className="text-scale-1200 text-sm">{schema.name}</span>
</Listbox.Option>
))}
<Listbox.Option
key={schema.id}
value={schema.name}
label={schema.name}
addOnBefore={() => <span className="text-scale-900">schema</span>}
disabled
key="protected-schemas"
value="protected-schemas"
label="Protected schemas"
>
<span className="text-scale-1200 text-sm">{schema.name}</span>
<p className="text-sm">Protected schemas</p>
</Listbox.Option>
))}
</Listbox>
{protectedSchemas.map((schema) => (
<Listbox.Option
key={schema.id}
value={schema.name}
label={schema.name}
addOnBefore={() => <span className="text-scale-900">schema</span>}
>
<span className="text-scale-1200 text-sm">{schema.name}</span>
</Listbox.Option>
))}
</Listbox>
)}
</div>
<Input
size="small"
@@ -150,7 +186,11 @@ const AuthPoliciesPage: NextPageWithLayout = () => {
</a>
</div>
</div>
<Policies tables={filteredTables} hasTables={tables.length > 0} isLocked={isLocked} />
{isLoading && <GenericSkeletonLoader />}
{isError && <AlertError error={error} subject="Failed to retrieve tables" />}
{isSuccess && (
<Policies tables={filteredTables} hasTables={tables.length > 0} isLocked={isLocked} />
)}
</div>
)
}
@@ -5,15 +5,13 @@ import { useEffect, useState } from 'react'
import { CreateFunction, DeleteFunction } from 'components/interfaces/Database'
import FunctionsList from 'components/interfaces/Database/Functions/FunctionsList/FunctionsList'
import { DatabaseLayout } from 'components/layouts'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import { ScaffoldContainer, ScaffoldSection } from 'components/layouts/Scaffold'
import NoPermission from 'components/ui/NoPermission'
import { useCheckPermissions, useStore } from 'hooks'
import { NextPageWithLayout } from 'types'
import { ScaffoldContainer, ScaffoldSection } from 'components/layouts/Scaffold'
const FunctionsPage: NextPageWithLayout = () => {
const { meta } = useStore()
const { project } = useProjectContext()
const { ui, meta } = useStore()
const [selectedFunction, setSelectedFunction] = useState<any>()
const [showCreateFunctionForm, setShowCreateFunctionForm] = useState<boolean>(false)
const [showDeleteFunctionForm, setShowDeleteFunctionForm] = useState<boolean>(false)
@@ -21,10 +19,10 @@ const FunctionsPage: NextPageWithLayout = () => {
const canReadFunctions = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_READ, 'functions')
useEffect(() => {
if (project?.ref) {
if (ui.selectedProjectRef) {
fetchFunctions()
}
}, [project?.ref])
}, [ui.selectedProjectRef])
const fetchFunctions = async () => {
meta.functions.load()
+20 -11
View File
@@ -1,39 +1,48 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import clsx from 'clsx'
import { useParams } from 'common'
import { observer } from 'mobx-react-lite'
import { useEffect, useState } from 'react'
import { IconLoader } from 'ui'
import { useParams } from 'common'
import DeleteHookModal from 'components/interfaces/Database/Hooks/DeleteHookModal'
import EditHookPanel from 'components/interfaces/Database/Hooks/EditHookPanel'
import HooksList from 'components/interfaces/Database/Hooks/HooksList/HooksList'
import { DatabaseLayout } from 'components/layouts'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import { ScaffoldContainer, ScaffoldSection } from 'components/layouts/Scaffold'
import ProductEmptyState from 'components/to-be-cleaned/ProductEmptyState'
import NoPermission from 'components/ui/NoPermission'
import { useHooksEnableMutation } from 'data/database/hooks-enable-mutation'
import { useSchemasQuery } from 'data/database/schemas-query'
import { useCheckPermissions, useStore } from 'hooks'
import { NextPageWithLayout } from 'types'
import { ScaffoldContainer, ScaffoldSection } from 'components/layouts/Scaffold'
import { IconLoader } from 'ui'
const HooksPage: NextPageWithLayout = () => {
const { project } = useProjectContext()
const { meta, ui } = useStore()
const { ref } = useParams()
const schemas = meta.schemas.list()
const { isLoading: isLoadingSchemas } = meta.schemas
const { ref: projectRef } = useParams()
const {
data: schemas,
isLoading: isLoadingSchemas,
refetch,
} = useSchemasQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
})
const [selectedHook, setSelectedHook] = useState<any>()
const [showCreateHookForm, setShowCreateHookForm] = useState<boolean>(false)
const [showDeleteHookForm, setShowDeleteHookForm] = useState<boolean>(false)
const isHooksEnabled = schemas.some((schema: any) => schema.name === 'supabase_functions')
const isHooksEnabled = schemas?.some((schema) => schema.name === 'supabase_functions')
const canReadWebhooks = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_READ, 'triggers')
const canCreateWebhooks = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'triggers')
const { mutate: enableHooks, isLoading: isEnablingHooks } = useHooksEnableMutation({
onSuccess: () => {
meta.schemas.load()
refetch()
ui.setNotification({
category: 'success',
message: `Successfully enabled webhooks`,
@@ -46,8 +55,8 @@ const HooksPage: NextPageWithLayout = () => {
}, [ui.selectedProjectRef])
const enableHooksForProject = async () => {
if (!ref) return console.error('Project ref is required')
enableHooks({ ref })
if (!projectRef) return console.error('Project ref is required')
enableHooks({ ref: projectRef })
}
const createHook = () => {
+63 -44
View File
@@ -1,66 +1,85 @@
import { observer } from 'mobx-react-lite'
import { partition } from 'lodash'
import { useState } from 'react'
import SchemaGraph from 'components/interfaces/Database/Schemas/SchemaGraph'
import { DatabaseLayout } from 'components/layouts'
import { useStore } from 'hooks'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import AlertError from 'components/ui/AlertError'
import { useSchemasQuery } from 'data/database/schemas-query'
import { EXCLUDED_SCHEMAS } from 'lib/constants/schemas'
import { NextPageWithLayout } from 'types'
import { IconLock, Listbox } from 'ui'
const SchemasPage: NextPageWithLayout = () => {
const { meta } = useStore()
const schemas = meta.schemas.list()
const { project } = useProjectContext()
const {
data: schemas,
isSuccess,
isLoading,
isError,
error,
} = useSchemasQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
})
const [selectedSchema, setSelectedSchema] = useState<string>('public')
const [protectedSchemas, openSchemas] = partition(schemas, (schema) =>
meta.excludedSchemas.includes(schema?.name ?? '')
EXCLUDED_SCHEMAS.includes(schema?.name ?? '')
)
const schema = schemas.find((schema) => schema.name === selectedSchema)
const schema = schemas?.find((schema) => schema.name === selectedSchema)
const isLocked = protectedSchemas.some((s) => s.id === schema?.id)
return (
<>
<div className="flex w-full h-full flex-col">
<div className="p-4 border-b border-scale-500">
<Listbox
className="w-[260px]"
size="small"
value={selectedSchema}
onChange={setSelectedSchema}
icon={isLocked && <IconLock size={14} strokeWidth={2} />}
>
<Listbox.Option disabled key="normal-schemas" value="normal-schemas" label="Schemas">
<p className="text-sm">Schemas</p>
</Listbox.Option>
{openSchemas.map((schema) => (
<Listbox.Option
key={schema.id}
value={schema.name}
label={schema.name}
addOnBefore={() => <span className="text-scale-900">schema</span>}
>
<span className="text-scale-1200 text-sm">{schema.name}</span>
</Listbox.Option>
))}
<Listbox.Option
disabled
key="protected-schemas"
value="protected-schemas"
label="Protected schemas"
{isLoading && (
<div className="h-[34px] w-[260px] bg-scale-1000 rounded shimmering-loader" />
)}
{isError && <AlertError error={error} subject="Failed to retrieve schemas" />}
{isSuccess && (
<Listbox
className="w-[260px]"
size="small"
value={selectedSchema}
onChange={setSelectedSchema}
icon={isLocked && <IconLock size={14} strokeWidth={2} />}
>
<p className="text-sm">Protected schemas</p>
</Listbox.Option>
{protectedSchemas.map((schema) => (
<Listbox.Option
key={schema.id}
value={schema.name}
label={schema.name}
addOnBefore={() => <span className="text-scale-900">schema</span>}
>
<span className="text-scale-1200 text-sm">{schema.name}</span>
<Listbox.Option disabled key="normal-schemas" value="normal-schemas" label="Schemas">
<p className="text-sm">Schemas</p>
</Listbox.Option>
))}
</Listbox>
{openSchemas.map((schema) => (
<Listbox.Option
key={schema.id}
value={schema.name}
label={schema.name}
addOnBefore={() => <span className="text-scale-900">schema</span>}
>
<span className="text-scale-1200 text-sm">{schema.name}</span>
</Listbox.Option>
))}
<Listbox.Option
disabled
key="protected-schemas"
value="protected-schemas"
label="Protected schemas"
>
<p className="text-sm">Protected schemas</p>
</Listbox.Option>
{protectedSchemas.map((schema) => (
<Listbox.Option
key={schema.id}
value={schema.name}
label={schema.name}
addOnBefore={() => <span className="text-scale-900">schema</span>}
>
<span className="text-scale-1200 text-sm">{schema.name}</span>
</Listbox.Option>
))}
</Listbox>
)}
</div>
<SchemaGraph schema={selectedSchema}></SchemaGraph>
@@ -71,4 +90,4 @@ const SchemasPage: NextPageWithLayout = () => {
SchemasPage.getLayout = (page) => <DatabaseLayout title="Database">{page}</DatabaseLayout>
export default observer(SchemasPage)
export default SchemasPage
+26 -177
View File
@@ -1,166 +1,56 @@
import type { PostgresColumn, PostgresTable } from '@supabase/postgres-meta'
import { useQueryClient } from '@tanstack/react-query'
import { isUndefined } from 'lodash'
import { observer } from 'mobx-react-lite'
import { useEffect, useState } from 'react'
import { Modal } from 'ui'
import { useParams } from 'common'
import { ColumnList, TableList } from 'components/interfaces/Database'
import { SidePanelEditor } from 'components/interfaces/TableGridEditor'
import DeleteConfirmationDialogs from 'components/interfaces/TableGridEditor/DeleteConfirmationDialogs'
import { DatabaseLayout } from 'components/layouts'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import { ScaffoldContainer, ScaffoldSection } from 'components/layouts/Scaffold'
import ConfirmationModal from 'components/ui/ConfirmationModal'
import { sqlKeys } from 'data/sql/keys'
import { Table } from 'data/tables/table-query'
import { useStore } from 'hooks'
import { useTableEditorStateSnapshot } from 'state/table-editor'
import { NextPageWithLayout } from 'types'
const DatabaseTables: NextPageWithLayout = () => {
const { meta, ui } = useStore()
const { project } = useProjectContext()
const snap = useTableEditorStateSnapshot()
const { ui, meta } = useStore()
const queryClient = useQueryClient()
const [isDeleting, setIsDeleting] = useState<boolean>(false)
const [selectedSchema, setSelectedSchema] = useState('public')
const [selectedTable, setSelectedTable] = useState<any>()
const [sidePanelKey, setSidePanelKey] = useState<'column' | 'table'>()
const [selectedColumnToEdit, setSelectedColumnToEdit] = useState<PostgresColumn>()
const [selectedTableToEdit, setSelectedTableToEdit] = useState<PostgresTable>()
const [selectedColumnToDelete, setSelectedColumnToDelete] = useState<PostgresColumn>()
const [selectedTableToDelete, setSelectedTableToDelete] = useState<PostgresTable>()
const { ref: projectRef } = useParams()
useEffect(() => {
if (project?.ref) {
if (ui.selectedProjectRef) {
meta.types.load()
}
}, [project?.ref])
}, [ui.selectedProjectRef])
const onAddTable = () => {
setSidePanelKey('table')
setSelectedTableToEdit(undefined)
}
const onEditTable = (table: PostgresTable) => {
setSidePanelKey('table')
setSelectedTableToEdit(table)
}
const onDeleteTable = (table: PostgresTable) => {
setIsDeleting(true)
setSelectedTableToDelete(table)
}
const onAddColumn = () => {
setSidePanelKey('column')
setSelectedColumnToEdit(undefined)
}
const onEditColumn = (column: PostgresColumn) => {
setSidePanelKey('column')
setSelectedColumnToEdit(column)
}
const onDeleteColumn = (column: PostgresColumn) => {
setIsDeleting(true)
setSelectedColumnToDelete(column)
}
const onColumnUpdated = async () => {
if (selectedTable === undefined) return
const updatedTable = await meta.tables.loadById(selectedTable.id)
setSelectedTable(updatedTable)
}
const onClosePanel = () => setSidePanelKey(undefined)
const onConfirmDeleteTable = async () => {
try {
if (isUndefined(selectedTableToDelete)) return
const response: any = await meta.tables.del(selectedTableToDelete.id)
if (response.error) {
throw response.error
} else {
ui.setNotification({
category: 'success',
message: `Successfully removed ${selectedTableToDelete.name}.`,
})
}
} catch (error: any) {
ui.setNotification({
category: 'error',
message: `Failed to delete ${selectedTableToDelete?.name}: ${error.message}`,
})
} finally {
setIsDeleting(false)
setSelectedTableToDelete(undefined)
}
}
const onConfirmDeleteColumn = async () => {
try {
if (isUndefined(selectedColumnToDelete)) return
const response: any = await meta.columns.del(selectedColumnToDelete.id)
if (response.error) {
throw response.error
} else {
onColumnUpdated()
ui.setNotification({
category: 'success',
message: `Successfully removed ${selectedColumnToDelete.name}.`,
})
queryClient.invalidateQueries(sqlKeys.query(project?.ref, ['foreign-key-constraints']))
await Promise.all([
meta.tables.loadById(selectedColumnToDelete!.table_id),
queryClient.invalidateQueries(
sqlKeys.query(project?.ref, [selectedTable!.schema, selectedTable!.name])
),
queryClient.invalidateQueries(
sqlKeys.query(project?.ref, [
'table-definition',
selectedTable!.schema,
selectedTable!.name,
])
),
])
}
} catch (error: any) {
ui.setNotification({
category: 'error',
message: `Failed to delete ${selectedColumnToDelete?.name}: ${error.message}`,
})
} finally {
setIsDeleting(false)
setSelectedColumnToDelete(undefined)
}
}
const [selectedTable, setSelectedTable] = useState<Table | undefined>(undefined)
const [selectedTableToEdit, setSelectedTableToEdit] = useState<Table | undefined>(undefined)
return (
<>
<ScaffoldContainer>
<ScaffoldSection>
<div className="col-span-12">
{isUndefined(selectedTable) ? (
{selectedTable === undefined ? (
<TableList
selectedSchema={selectedSchema}
onSelectSchema={setSelectedSchema}
onAddTable={onAddTable}
onEditTable={onEditTable}
onDeleteTable={onDeleteTable}
onAddTable={snap.onAddTable}
onEditTable={(table) => {
setSelectedTableToEdit(table)
snap.onEditTable()
}}
onDeleteTable={(table) => {
setSelectedTableToEdit(table)
snap.onDeleteTable()
}}
onOpenTable={setSelectedTable}
/>
) : (
<ColumnList
selectedTable={selectedTable}
onAddColumn={onAddColumn}
onEditColumn={onEditColumn}
onDeleteColumn={onDeleteColumn}
onAddColumn={snap.onAddColumn}
onEditColumn={snap.onEditColumn}
onDeleteColumn={snap.onDeleteColumn}
onSelectBack={() => setSelectedTable(undefined)}
/>
)}
@@ -168,49 +58,8 @@ const DatabaseTables: NextPageWithLayout = () => {
</ScaffoldSection>
</ScaffoldContainer>
<ConfirmationModal
danger
visible={isDeleting && !isUndefined(selectedTableToDelete)}
header={
<span className="break-words">{`Confirm deletion of table "${selectedTableToDelete?.name}"`}</span>
}
buttonLabel="Delete"
buttonLoadingLabel="Deleting"
onSelectCancel={() => setIsDeleting(false)}
onSelectConfirm={onConfirmDeleteTable}
>
<Modal.Content>
<p className="py-4 text-sm text-scale-1100">
Are you sure you want to delete the selected table? This action cannot be undone.
</p>
</Modal.Content>
</ConfirmationModal>
<ConfirmationModal
danger
visible={isDeleting && !isUndefined(selectedColumnToDelete)}
header={`Confirm deletion of column "${selectedColumnToDelete?.name}"`}
buttonLabel="Delete"
buttonLoadingLabel="Deleting"
onSelectCancel={() => setIsDeleting(false)}
onSelectConfirm={onConfirmDeleteColumn}
>
<Modal.Content>
<p className="py-4 text-sm text-scale-1100">
Are you sure you want to delete the selected column? This action cannot be undone.
</p>
</Modal.Content>
</ConfirmationModal>
<SidePanelEditor
sidePanelKey={sidePanelKey}
selectedSchema={selectedSchema}
selectedTable={selectedTable}
onColumnSaved={onColumnUpdated}
closePanel={onClosePanel}
selectedColumnToEdit={selectedColumnToEdit}
selectedTableToEdit={selectedTableToEdit}
/>
<DeleteConfirmationDialogs projectRef={projectRef} selectedTable={selectedTableToEdit} />
<SidePanelEditor selectedTable={selectedTableToEdit} />
</>
)
}
+28 -382
View File
@@ -1,402 +1,48 @@
import type { PostgresColumn, PostgresTable } from '@supabase/postgres-meta'
import { useQueryClient } from '@tanstack/react-query'
import { useParams, useTheme } from 'common'
import { isNaN, isUndefined } from 'lodash'
import { observer } from 'mobx-react-lite'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { useEffect, useState } from 'react'
import { Alert, Button, Checkbox, IconExternalLink, Modal } from 'ui'
import { Dictionary } from 'components/grid'
import { useTheme } from 'common'
import { useParams } from 'common/hooks'
import { TableGridEditor } from 'components/interfaces'
import { ForeignRowSelectorProps } from 'components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/ForeignRowSelector/ForeignRowSelector'
import { JsonEditValue } from 'components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/RowEditor.types'
import DeleteConfirmationDialogs from 'components/interfaces/TableGridEditor/DeleteConfirmationDialogs'
import { TableEditorLayout } from 'components/layouts'
import {
ProjectContextFromParamsProvider,
useProjectContext,
} from 'components/layouts/ProjectLayout/ProjectContext'
import ConfirmationModal from 'components/ui/ConfirmationModal'
import { Entity } from 'data/entity-types/entity-type-query'
import { entityTypeKeys } from 'data/entity-types/keys'
import { sqlKeys } from 'data/sql/keys'
import { useStore, useUrlState, withAuth } from 'hooks'
import { NextPageWithLayout, SchemaView } from 'types'
import { ProjectContextFromParamsProvider } from 'components/layouts/ProjectLayout/ProjectContext'
import useTable from 'hooks/misc/useTable'
import { useRouter } from 'next/router'
import { NextPageWithLayout } from 'types'
const TableEditorPage: NextPageWithLayout = () => {
const router = useRouter()
const { isDarkMode } = useTheme()
const { id, ref: projectRef } = useParams()
const [_, setParams] = useUrlState({ arrayKeys: ['filter', 'sort'] })
const { id: _id, ref: projectRef } = useParams()
const id = _id ? Number(_id) : undefined
const { project } = useProjectContext()
const queryClient = useQueryClient()
const { meta, ui } = useStore()
const [selectedSchema, setSelectedSchema] = useState<string>()
const [isDeleting, setIsDeleting] = useState<boolean>(false)
const [isDuplicating, setIsDuplicating] = useState<boolean>(false)
const [isDeleteWithCascade, setIsDeleteWithCascade] = useState(false)
const [selectedColumnToDelete, setSelectedColumnToDelete] = useState<PostgresColumn>()
const [selectedTableToDelete, setSelectedTableToDelete] = useState<PostgresTable>()
const [sidePanelKey, setSidePanelKey] = useState<
'row' | 'column' | 'table' | 'json' | 'foreign-row-selector' | 'csv-import'
>()
const [selectedRowToEdit, setSelectedRowToEdit] = useState<Dictionary<any>>()
const [selectedColumnToEdit, setSelectedColumnToEdit] = useState<PostgresColumn>()
const [selectedTableToEdit, setSelectedTableToEdit] = useState<PostgresTable>()
const [selectedValueForJsonEdit, setSelectedValueForJsonEdit] = useState<JsonEditValue>()
const [selectedForeignKeyToEdit, setSelectedForeignKeyToEdit] = useState<{
foreignKey: NonNullable<ForeignRowSelectorProps['foreignKey']>
row: any
column: any
}>()
const tables: PostgresTable[] = meta.tables.list()
const views: SchemaView[] = meta.views.list()
const materializedViews = meta.materializedViews.list()
const foreignTables: Partial<PostgresTable>[] = meta.foreignTables.list()
const selectedTable = !isNaN(Number(id))
? // @ts-ignore
tables
// @ts-ignore
.concat(views)
// @ts-ignore
.concat(materializedViews)
// @ts-ignore
.concat(foreignTables)
.find((table) => table.id === Number(id))
: undefined
useEffect(() => {
if (selectedTable && 'schema' in selectedTable) {
setSelectedSchema(selectedTable.schema)
}
}, [selectedTable?.name])
const onAddRow = () => {
setSidePanelKey('row')
setSelectedRowToEdit(undefined)
}
const onEditRow = (row: Dictionary<any>) => {
setSidePanelKey('row')
setSelectedRowToEdit(row)
}
const onAddColumn = () => {
setSidePanelKey('column')
setSelectedColumnToEdit(undefined)
}
const onEditColumn = (column: PostgresColumn) => {
setSidePanelKey('column')
setSelectedColumnToEdit(column)
}
const onDeleteColumn = (column: PostgresColumn) => {
setIsDeleting(true)
setSelectedColumnToDelete(column)
setIsDeleteWithCascade(false)
}
const onAddTable = () => {
setSidePanelKey('table')
setIsDuplicating(false)
setSelectedTableToEdit(undefined)
}
const onEditTable = (entity: Entity) => {
setSidePanelKey('table')
setIsDuplicating(false)
const table = meta.tables.byId(entity.id)
setSelectedTableToEdit(table)
}
const onDeleteTable = (entity: Entity) => {
setIsDeleting(true)
const table = meta.tables.byId(entity.id)
setSelectedTableToDelete(table)
setIsDeleteWithCascade(false)
}
const onDuplicateTable = (entity: Entity) => {
setSidePanelKey('table')
setIsDuplicating(true)
const table = meta.tables.byId(entity.id)
setSelectedTableToEdit(table)
}
const onExpandJSONEditor = (column: string, row: any) => {
setSidePanelKey('json')
setSelectedValueForJsonEdit({ column, row, jsonString: JSON.stringify(row[column]) || '' })
}
const onImportData = () => {
if (id) {
setSidePanelKey('csv-import')
const table = meta.tables.byId(id)
setSelectedTableToEdit(table)
} else {
console.error('Table ID not found')
}
}
const onEditForeignKeyColumnValue = ({
foreignKey,
row,
column,
}: {
foreignKey: NonNullable<ForeignRowSelectorProps['foreignKey']>
row: any
column: any
}) => {
setSidePanelKey('foreign-row-selector')
setSelectedForeignKeyToEdit({
foreignKey,
row,
column,
})
}
const onClosePanel = () => {
setSidePanelKey(undefined)
}
const removeDeletedColumnFromFiltersAndSorts = (columnName: string) => {
setParams((prevParams) => {
const existingFilters = (prevParams?.filter ?? []) as string[]
const existingSorts = (prevParams?.sort ?? []) as string[]
return {
...prevParams,
filter: existingFilters.filter((filter: string) => {
const [column] = filter.split(':')
if (column !== columnName) return filter
}),
sort: existingSorts.filter((sort: string) => {
const [column] = sort.split(':')
if (column !== columnName) return sort
}),
}
})
}
const onConfirmDeleteColumn = async () => {
try {
if (selectedColumnToDelete === undefined) return
const response: any = await meta.columns.del(selectedColumnToDelete.id, isDeleteWithCascade)
if (response.error) throw response.error
removeDeletedColumnFromFiltersAndSorts(selectedColumnToDelete.name)
ui.setNotification({
category: 'success',
message: `Successfully deleted column "${selectedColumnToDelete.name}"`,
})
queryClient.invalidateQueries(sqlKeys.query(project?.ref, ['foreign-key-constraints']))
await Promise.all([
meta.tables.loadById(selectedColumnToDelete!.table_id),
queryClient.invalidateQueries(
sqlKeys.query(project?.ref, [selectedTable!.schema, selectedTable!.name])
),
queryClient.invalidateQueries(
sqlKeys.query(project?.ref, [
'table-definition',
selectedTable!.schema,
selectedTable!.name,
])
),
])
if (selectedSchema) await meta.views.loadBySchema(selectedSchema)
} catch (error: any) {
ui.setNotification({
category: 'error',
message: `Failed to delete ${selectedColumnToDelete!.name}: ${error.message}`,
})
} finally {
setIsDeleting(false)
setSelectedColumnToDelete(undefined)
}
}
const onConfirmDeleteTable = async () => {
try {
if (selectedTableToDelete === undefined) return
const response: any = await meta.tables.del(selectedTableToDelete.id, isDeleteWithCascade)
if (response.error) throw response.error
const tables = meta.tables.list((table: PostgresTable) => table.schema === selectedSchema)
await queryClient.invalidateQueries(entityTypeKeys.list(projectRef))
// For simplicity for now, we just open the first table within the same schema
if (tables.length > 0) {
router.push(`/project/${projectRef}/editor/${tables[0].id}`)
} else {
router.push(`/project/${projectRef}/editor/`)
}
ui.setNotification({
category: 'success',
message: `Successfully deleted table "${selectedTableToDelete.name}"`,
})
if (selectedSchema) await meta.views.loadBySchema(selectedSchema)
} catch (error: any) {
ui.setNotification({
error,
category: 'error',
message: `Failed to delete ${selectedTableToDelete?.name}: ${error.message}`,
})
} finally {
setIsDeleting(false)
setSelectedTableToDelete(undefined)
}
}
const { data: selectedTable, isLoading } = useTable(id)
return (
<TableEditorLayout
selectedSchema={selectedSchema}
selectedTable={selectedTable?.name}
onSelectSchema={setSelectedSchema}
onAddTable={onAddTable}
onEditTable={onEditTable}
onDeleteTable={onDeleteTable}
onDuplicateTable={onDuplicateTable}
>
<>
<TableGridEditor
selectedSchema={selectedSchema}
isLoadingSelectedTable={isLoading}
selectedTable={selectedTable}
sidePanelKey={sidePanelKey}
isDuplicating={isDuplicating}
selectedRowToEdit={selectedRowToEdit}
selectedColumnToEdit={selectedColumnToEdit}
selectedTableToEdit={selectedTableToEdit}
selectedValueForJsonEdit={selectedValueForJsonEdit}
selectedForeignKeyToEdit={selectedForeignKeyToEdit}
onAddRow={onAddRow}
onEditRow={onEditRow}
onAddColumn={onAddColumn}
onEditColumn={onEditColumn}
onDeleteColumn={onDeleteColumn}
onExpandJSONEditor={onExpandJSONEditor}
onEditForeignKeyColumnValue={onEditForeignKeyColumnValue}
onClosePanel={onClosePanel}
onImportData={onImportData}
theme={isDarkMode ? 'dark' : 'light'}
/>
<ConfirmationModal
danger
size="small"
visible={isDeleting && !isUndefined(selectedColumnToDelete)}
header={`Confirm deletion of column "${selectedColumnToDelete?.name}"`}
buttonLabel="Delete"
buttonLoadingLabel="Deleting"
onSelectCancel={() => {
setIsDeleting(false)
setSelectedColumnToDelete(undefined)
<DeleteConfirmationDialogs
projectRef={projectRef}
selectedTable={selectedTable}
onAfterDeleteTable={(tables) => {
// For simplicity for now, we just open the first table within the same schema
if (tables.length > 0) {
router.push(`/project/${projectRef}/editor/${tables[0].id}`)
} else {
router.push(`/project/${projectRef}/editor/`)
}
}}
onSelectConfirm={onConfirmDeleteColumn}
>
<Modal.Content>
<div className="py-4 space-y-4">
<p className="text-sm text-scale-1100">
Are you sure you want to delete the selected column? This action cannot be undone.
</p>
<Checkbox
label="Drop column with cascade?"
description="Deletes the column and its dependent objects"
checked={isDeleteWithCascade}
onChange={() => setIsDeleteWithCascade(!isDeleteWithCascade)}
/>
{isDeleteWithCascade && (
<Alert
withIcon
variant="warning"
title="Warning: Dropping with cascade may result in unintended consequences"
>
<p className="mb-4">
All dependent objects will be removed, as will any objects that depend on them,
recursively.
</p>
<Link href="https://www.postgresql.org/docs/current/ddl-depend.html">
<a target="_blank" rel="noreferrer">
<Button size="tiny" type="default" icon={<IconExternalLink />}>
About dependency tracking
</Button>
</a>
</Link>
</Alert>
)}
</div>
</Modal.Content>
</ConfirmationModal>
<ConfirmationModal
danger
size="small"
visible={isDeleting && !isUndefined(selectedTableToDelete)}
header={
<span className="break-words">{`Confirm deletion of table "${selectedTableToDelete?.name}"`}</span>
}
buttonLabel="Delete"
buttonLoadingLabel="Deleting"
onSelectCancel={() => {
setIsDeleting(false)
setSelectedTableToDelete(undefined)
}}
onSelectConfirm={onConfirmDeleteTable}
>
<Modal.Content>
<div className="py-4 space-y-4">
<p className="text-sm text-scale-1100">
Are you sure you want to delete the selected table? This action cannot be undone.
</p>
<Checkbox
label="Drop table with cascade?"
description="Deletes the table and its dependent objects"
checked={isDeleteWithCascade}
onChange={() => setIsDeleteWithCascade(!isDeleteWithCascade)}
/>
{isDeleteWithCascade && (
<Alert
withIcon
variant="warning"
title="Warning: Dropping with cascade may result in unintended consequences"
>
<p className="mb-4">
All dependent objects will be removed, as will any objects that depend on them,
recursively.
</p>
<Link href="https://www.postgresql.org/docs/current/ddl-depend.html">
<a target="_blank" rel="noreferrer">
<Button size="tiny" type="default" icon={<IconExternalLink />}>
About dependency tracking
</Button>
</a>
</Link>
</Alert>
)}
</div>
</Modal.Content>
</ConfirmationModal>
</TableEditorLayout>
/>
</>
)
}
TableEditorPage.getLayout = (page) => (
<ProjectContextFromParamsProvider>{page}</ProjectContextFromParamsProvider>
<ProjectContextFromParamsProvider>
<TableEditorLayout>{page}</TableEditorLayout>
</ProjectContextFromParamsProvider>
)
export default withAuth(observer(TableEditorPage))
export default TableEditorPage
+16 -103
View File
@@ -1,119 +1,32 @@
import type { PostgresTable } from '@supabase/postgres-meta'
import { isUndefined } from 'lodash'
import { observer } from 'mobx-react-lite'
import router from 'next/router'
import { useState } from 'react'
import { useRouter } from 'next/router'
import { useParams } from 'common'
import { EmptyState, SidePanelEditor } from 'components/interfaces/TableGridEditor'
import { TableEditorLayout } from 'components/layouts'
import {
ProjectContextFromParamsProvider,
useProjectContext,
} from 'components/layouts/ProjectLayout/ProjectContext'
import ConfirmationModal from 'components/ui/ConfirmationModal'
import { Entity } from 'data/entity-types/entity-type-query'
import { useStore, withAuth } from 'hooks'
import { ProjectContextFromParamsProvider } from 'components/layouts/ProjectLayout/ProjectContext'
import { Table } from 'data/tables/table-query'
import { NextPageWithLayout } from 'types'
const TableEditorPage: NextPageWithLayout = () => {
const { meta, ui } = useStore()
const { project } = useProjectContext()
const projectRef = project?.ref
const { ref: projectRef } = useParams()
const router = useRouter()
const [sidePanelKey, setSidePanelKey] = useState<'row' | 'column' | 'table'>()
const [isDeleting, setIsDeleting] = useState<boolean>(false)
const [isDuplicating, setIsDuplicating] = useState<boolean>(false)
const [selectedSchema, setSelectedSchema] = useState<string>('public')
const [selectedTableToEdit, setSelectedTableToEdit] = useState<PostgresTable>()
const [selectedTableToDelete, setSelectedTableToDelete] = useState<PostgresTable>()
const onAddTable = () => {
setSidePanelKey('table')
setIsDuplicating(false)
setSelectedTableToEdit(undefined)
}
const onEditTable = (entity: Entity) => {
setSidePanelKey('table')
setIsDuplicating(false)
const table = meta.tables.byId(entity.id)
setSelectedTableToEdit(table)
}
const onDeleteTable = (entity: Entity) => {
setIsDeleting(true)
const table = meta.tables.byId(entity.id)
setSelectedTableToDelete(table)
}
const onDuplicateTable = (entity: Entity) => {
setSidePanelKey('table')
setIsDuplicating(true)
const table = meta.tables.byId(entity.id)
setSelectedTableToEdit(table)
}
const onClosePanel = () => {
setSidePanelKey(undefined)
}
const onConfirmDeleteTable = async () => {
try {
await meta.tables.del(selectedTableToDelete!.id)
setIsDeleting(false)
ui.setNotification({
category: 'success',
message: `Successfully deleted ${selectedTableToDelete!.name}`,
})
await meta.views.loadBySchema(selectedSchema)
} catch (error: any) {
ui.setNotification({
category: 'error',
message: `Failed to delete ${selectedTableToDelete!.name}`,
})
}
const onTableCreated = (table: Table) => {
router.push(`/project/${projectRef}/editor/${table.id}`)
}
return (
<TableEditorLayout
selectedSchema={selectedSchema}
onSelectSchema={setSelectedSchema}
onAddTable={onAddTable}
onEditTable={onEditTable}
onDeleteTable={onDeleteTable}
onDuplicateTable={onDuplicateTable}
>
<EmptyState selectedSchema={selectedSchema} onAddTable={onAddTable} />
{/* On this page it'll only handle tables */}
<ConfirmationModal
danger
visible={isDeleting && !isUndefined(selectedTableToDelete)}
header={
<span className="break-words">{`Confirm deletion of table "${selectedTableToDelete?.name}"`}</span>
}
description={`Are you sure you want to delete the selected table? This action cannot be undone`}
buttonLabel="Delete"
buttonLoadingLabel="Deleting"
onSelectCancel={() => setIsDeleting(false)}
onSelectConfirm={onConfirmDeleteTable}
/>
<SidePanelEditor
selectedSchema={selectedSchema}
isDuplicating={isDuplicating}
selectedTableToEdit={selectedTableToEdit}
sidePanelKey={sidePanelKey}
closePanel={onClosePanel}
onTableCreated={(table: any) => router.push(`/project/${projectRef}/editor/${table.id}`)}
/>
</TableEditorLayout>
<>
<EmptyState />
<SidePanelEditor onTableCreated={onTableCreated} />
</>
)
}
TableEditorPage.getLayout = (page) => (
<ProjectContextFromParamsProvider>{page}</ProjectContextFromParamsProvider>
<ProjectContextFromParamsProvider>
<TableEditorLayout>{page}</TableEditorLayout>
</ProjectContextFromParamsProvider>
)
export default withAuth(observer(TableEditorPage))
export default TableEditorPage
+1 -7
View File
@@ -1,5 +1,5 @@
import { observer, useLocalObservable } from 'mobx-react-lite'
import { createContext, useEffect } from 'react'
import { createContext } from 'react'
import { useParams } from 'common/hooks'
import ServiceList from 'components/interfaces/Settings/API/ServiceList'
@@ -31,12 +31,6 @@ const ApiSettings: NextPageWithLayout = () => {
PageContext.project = project
if (meta) PageState.meta = meta
useEffect(() => {
if (ui.selectedProjectRef) {
meta.schemas.load()
}
}, [ui.selectedProjectRef])
return (
<PageContext.Provider value={PageState}>
<div className="flex flex-col gap-8 px-5 py-6 mx-auto 1xl:px-28 lg:px-16 xl:px-24 2xl:px-32">
+1 -1
View File
@@ -67,7 +67,7 @@ const SqlEditor: NextPageWithLayout = () => {
pgInfoRef.current = {}
}
pgInfoRef.current.tableColumns = tableColumns?.result
pgInfoRef.current.schemas = schemas?.result
pgInfoRef.current.schemas = schemas
pgInfoRef.current.keywords = keywords?.result
pgInfoRef.current.functions = functions?.result
}
+28
View File
@@ -0,0 +1,28 @@
import { proxy, snapshot, useSnapshot } from 'valtio'
import { proxySet } from 'valtio/utils'
export const tableEditorGlobalState = proxy({
projectRef: undefined as undefined | string,
loadedIds: proxySet<number>([]),
addLoadedId: (ref: string, id: number) => {
if (ref !== tableEditorGlobalState.projectRef) {
tableEditorGlobalState.loadedIds.clear()
tableEditorGlobalState.projectRef = ref
}
tableEditorGlobalState.loadedIds.add(id)
},
})
export const getTableEditorGlobalStateSnapshot = () => snapshot(tableEditorGlobalState)
export const useTableEditorGlobalStateSnapshot = (options?: Parameters<typeof useSnapshot>[1]) =>
useSnapshot(tableEditorGlobalState, options)
export const useIsTableLoaded = (ref?: string, id?: number) => {
const snap = useTableEditorGlobalStateSnapshot()
if (!ref || !id) return false
if (ref !== snap.projectRef) return false
return snap.loadedIds.has(id)
}
-28
View File
@@ -1,28 +0,0 @@
import { proxy, snapshot, useSnapshot } from 'valtio'
import { proxySet } from 'valtio/utils'
export const tableEditorState = proxy({
projectRef: undefined as undefined | string,
loadedIds: proxySet<number>([]),
addLoadedId: (ref: string, id: number) => {
if (ref !== tableEditorState.projectRef) {
tableEditorState.loadedIds.clear()
tableEditorState.projectRef = ref
}
tableEditorState.loadedIds.add(id)
},
})
export const getTableEditorStateSnapshot = () => snapshot(tableEditorState)
export const useTableEditorStateSnapshot = (options?: Parameters<typeof useSnapshot>[1]) =>
useSnapshot(tableEditorState, options)
export const useIsTableLoaded = (ref?: string, id?: number) => {
const snap = useTableEditorStateSnapshot()
if (!ref || !id) return false
if (ref !== snap.projectRef) return false
return snap.loadedIds.has(id)
}
+171
View File
@@ -0,0 +1,171 @@
import { PostgresColumn } from '@supabase/postgres-meta'
import { Dictionary } from 'components/grid'
import { ForeignRowSelectorProps } from 'components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/ForeignRowSelector/ForeignRowSelector'
import { JsonEditValue } from 'components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/RowEditor.types'
import { PropsWithChildren, createContext, useContext, useRef } from 'react'
import { proxy, useSnapshot } from 'valtio'
type ForeignKey = {
foreignKey: NonNullable<ForeignRowSelectorProps['foreignKey']>
row: Dictionary<any>
column: PostgresColumn
}
export type SidePanel =
| { type: 'row'; row?: Dictionary<any> }
| { type: 'column'; column?: PostgresColumn }
| { type: 'table'; mode: 'new' | 'edit' | 'duplicate' }
| { type: 'json'; jsonValue: JsonEditValue }
| {
type: 'foreign-row-selector'
foreignKey: ForeignKey
}
| { type: 'csv-import' }
export type ConfirmationDialog =
| { type: 'table'; isDeleteWithCascade: boolean }
| { type: 'column'; column: PostgresColumn; isDeleteWithCascade: boolean }
export type UIState =
| {
open: 'none'
}
| {
open: 'side-panel'
sidePanel: SidePanel
}
| {
open: 'confirmation-dialog'
confirmationDialog: ConfirmationDialog
}
export const createTableEditorState = () => {
const state = proxy({
selectedSchemaName: 'public',
setSelectedSchemaName: (schemaName: string) => {
state.selectedSchemaName = schemaName
},
ui: { open: 'none' } as UIState,
get sidePanel() {
return state.ui.open === 'side-panel' ? state.ui.sidePanel : undefined
},
get confirmationDialog() {
return state.ui.open === 'confirmation-dialog' ? state.ui.confirmationDialog : undefined
},
closeSidePanel: () => {
state.ui = { open: 'none' }
},
closeConfirmationDialog: () => {
state.ui = { open: 'none' }
},
/* Tables */
onAddTable: () => {
state.ui = {
open: 'side-panel',
sidePanel: { type: 'table', mode: 'new' },
}
},
onEditTable: () => {
state.ui = {
open: 'side-panel',
sidePanel: { type: 'table', mode: 'edit' },
}
},
onDuplicateTable: () => {
state.ui = {
open: 'side-panel',
sidePanel: { type: 'table', mode: 'duplicate' },
}
},
onDeleteTable: () => {
state.ui = {
open: 'confirmation-dialog',
confirmationDialog: { type: 'table', isDeleteWithCascade: false },
}
},
/* Columns */
onAddColumn: () => {
state.ui = {
open: 'side-panel',
sidePanel: { type: 'column' },
}
},
onEditColumn: (column: PostgresColumn) => {
state.ui = {
open: 'side-panel',
sidePanel: { type: 'column', column },
}
},
onDeleteColumn: (column: PostgresColumn) => {
state.ui = {
open: 'confirmation-dialog',
confirmationDialog: { type: 'column', column, isDeleteWithCascade: false },
}
},
/* Rows */
onAddRow: () => {
state.ui = {
open: 'side-panel',
sidePanel: { type: 'row' },
}
},
onEditRow: (row: Dictionary<any>) => {
state.ui = {
open: 'side-panel',
sidePanel: { type: 'row', row },
}
},
/* Misc */
onExpandJSONEditor: (jsonValue: JsonEditValue) => {
state.ui = {
open: 'side-panel',
sidePanel: { type: 'json', jsonValue },
}
},
onEditForeignKeyColumnValue: (foreignKey: ForeignKey) => {
state.ui = {
open: 'side-panel',
sidePanel: { type: 'foreign-row-selector', foreignKey },
}
},
onImportData: () => {
state.ui = {
open: 'side-panel',
sidePanel: { type: 'csv-import' },
}
},
/* Utils */
toggleConfirmationIsWithCascade: (overrideIsDeleteWithCascade?: boolean) => {
if (state.ui.open === 'confirmation-dialog') {
state.ui.confirmationDialog.isDeleteWithCascade =
overrideIsDeleteWithCascade ?? !state.ui.confirmationDialog.isDeleteWithCascade
}
},
})
return state
}
export type TableEditorState = ReturnType<typeof createTableEditorState>
export const TableEditorStateContext = createContext<TableEditorState>(createTableEditorState())
export const TableEditorStateContextProvider = ({ children }: PropsWithChildren<{}>) => {
const state = useRef(createTableEditorState()).current
return (
<TableEditorStateContext.Provider value={state}>{children}</TableEditorStateContext.Provider>
)
}
export const useTableEditorStateSnapshot = (options?: Parameters<typeof useSnapshot>[1]) => {
const state = useContext(TableEditorStateContext)
return useSnapshot(state, options)
}
+65 -61
View File
@@ -1,62 +1,62 @@
import Papa from 'papaparse'
import { makeObservable, observable } from 'mobx'
import { find, isUndefined, isEqual, isEmpty, chunk } from 'lodash'
import { Query } from 'components/grid/query/Query'
import { chunk, find, isEmpty, isEqual, isUndefined } from 'lodash'
import { makeObservable } from 'mobx'
import Papa from 'papaparse'
import type {
PostgresColumn,
PostgresTable,
PostgresRelationship,
PostgresPrimaryKey,
PostgresSchema,
PostgresRelationship,
PostgresTable,
} from '@supabase/postgres-meta'
import { IS_PLATFORM, API_URL } from 'lib/constants'
import { post } from 'lib/common/fetch'
import { API_URL, IS_PLATFORM } from 'lib/constants'
import { timeout, tryParseJson } from 'lib/helpers'
import { ResponseError } from 'types'
import { IRootStore } from '../RootStore'
import ColumnStore from './ColumnStore'
import SchemaStore from './SchemaStore'
import TableStore, { ITableStore } from './TableStore'
import OpenApiStore, { IOpenApiStore } from './OpenApiStore'
import { IPostgresMetaInterface } from '../common/PostgresMetaInterface'
import ColumnStore from './ColumnStore'
import OpenApiStore, { IOpenApiStore } from './OpenApiStore'
import TableStore, { ITableStore } from './TableStore'
import {
generateCreateColumnPayload,
generateUpdateColumnPayload,
} from 'components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/ColumnEditor.utils'
import {
ColumnField,
CreateColumnPayload,
ExtendedPostgresRelationship,
UpdateColumnPayload,
} from 'components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.types'
import {
generateCreateColumnPayload,
generateUpdateColumnPayload,
} from 'components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/ColumnEditor.utils'
import { ImportContent } from 'components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableEditor.types'
import RolesStore, { IRolesStore } from './RolesStore'
import PoliciesStore from './PoliciesStore'
import TriggersStore from './TriggersStore'
import PublicationStore from './PublicationStore'
import { FOREIGN_KEY_CASCADE_ACTION } from 'data/database/database-query-constants'
import { getCachedProjectDetail } from 'data/projects/project-detail-query'
import { getQueryClient } from 'data/query-client'
import { tableKeys } from 'data/tables/keys'
import { getTable } from 'data/tables/table-query'
import { getTables } from 'data/tables/tables-query'
import ExtensionsStore from './ExtensionsStore'
import ForeignTableStore, { IForeignTableStore } from './ForeignTableStore'
import FunctionsStore from './FunctionsStore'
import HooksStore from './HooksStore'
import ExtensionsStore from './ExtensionsStore'
import TypesStore from './TypesStore'
import ForeignTableStore, { IForeignTableStore } from './ForeignTableStore'
import ViewStore, { IViewStore } from './ViewStore'
import MaterializedViewStore, { IMaterializedViewStore } from './MaterializedViewStore'
import { FOREIGN_KEY_CASCADE_ACTION } from 'data/database/database-query-constants'
import PoliciesStore from './PoliciesStore'
import PublicationStore from './PublicationStore'
import RolesStore, { IRolesStore } from './RolesStore'
import TriggersStore from './TriggersStore'
import TypesStore from './TypesStore'
import ViewStore, { IViewStore } from './ViewStore'
const BATCH_SIZE = 1000
const CHUNK_SIZE = 1024 * 1024 * 0.1 // 0.1MB
export interface IMetaStore {
excludedSchemas: string[]
openApi: IOpenApiStore
tables: ITableStore
columns: IPostgresMetaInterface<PostgresColumn>
schemas: IPostgresMetaInterface<PostgresSchema>
views: IViewStore
materializedViews: IMaterializedViewStore
foreignTables: IForeignTableStore
@@ -150,7 +150,6 @@ export default class MetaStore implements IMetaStore {
openApi: OpenApiStore
tables: TableStore
columns: ColumnStore
schemas: SchemaStore
views: ViewStore
materializedViews: MaterializedViewStore
foreignTables: ForeignTableStore
@@ -169,25 +168,6 @@ export default class MetaStore implements IMetaStore {
baseUrl: string
headers: { [prop: string]: any }
// [Joshen] I'm going to treat this as a list of system schemas
excludedSchemas = [
'auth',
'extensions',
'information_schema',
'net',
'pgsodium',
'pgsodium_masks',
'pgbouncer',
'pgtle',
'realtime',
'storage',
'supabase_functions',
'supabase_migrations',
'vault',
'graphql',
'graphql_public',
]
constructor(rootStore: IRootStore, options: { projectRef: string; connectionString?: string }) {
const { projectRef, connectionString } = options
this.rootStore = rootStore
@@ -206,7 +186,6 @@ export default class MetaStore implements IMetaStore {
)
this.tables = new TableStore(this.rootStore, `${this.baseUrl}/tables`, this.headers)
this.columns = new ColumnStore(this.rootStore, `${this.baseUrl}/columns`, this.headers)
this.schemas = new SchemaStore(this.rootStore, `${this.baseUrl}/schemas`, this.headers)
this.views = new ViewStore(this.rootStore, `${this.baseUrl}/views`, this.headers)
this.materializedViews = new MaterializedViewStore(
this.rootStore,
@@ -239,9 +218,7 @@ export default class MetaStore implements IMetaStore {
)
this.types = new TypesStore(this.rootStore, `${this.baseUrl}/types`, this.headers)
makeObservable(this, {
excludedSchemas: observable,
})
makeObservable(this, {})
}
/**
@@ -349,7 +326,16 @@ export default class MetaStore implements IMetaStore {
async updateTableRealtime(table: PostgresTable, enable: boolean) {
let publicationUpdateError
const publications = this.publications.list()
const publicTables = this.tables.list((table: PostgresTable) => table.schema === 'public')
const queryClient = getQueryClient()
const project = await getCachedProjectDetail(queryClient, this.rootStore.ui.selectedProjectRef)
const projectRef = project?.ref
const connectionString = project?.connectionString
const publicTables = await queryClient.fetchQuery({
queryKey: tableKeys.list(projectRef, 'public'),
queryFn: ({ signal }) =>
getTables({ projectRef, connectionString, schema: 'public' }, signal),
})
let realtimePublication = publications.find((pub) => pub.name === 'supabase_realtime')
if (realtimePublication === undefined) {
@@ -596,8 +582,15 @@ export default class MetaStore implements IMetaStore {
})
}
await this.tables.load()
const tables = this.tables.list()
const queryClient = getQueryClient()
const project = await getCachedProjectDetail(queryClient, this.rootStore.ui.selectedProjectRef)
const projectRef = project?.ref
const connectionString = project?.connectionString
const tables = await queryClient.fetchQuery({
queryKey: tableKeys.list(projectRef, 'public'),
queryFn: ({ signal }) => getTables({ projectRef, connectionString }, signal),
})
const duplicatedTable = find(tables, { schema: sourceTableSchema, name: duplicatedTableName })
if (isRLSEnabled) {
@@ -621,8 +614,8 @@ export default class MetaStore implements IMetaStore {
importContent?: ImportContent
) {
// Create the table first
const table: any = await this.tables.create(payload)
if (table.error) throw table.error
const table = await this.tables.create(payload)
if ('error' in table) throw table.error
// If we face any errors during this process after the actual table creation
// We'll delete the table as a way to clean up and not leave behind bits that
@@ -745,7 +738,7 @@ export default class MetaStore implements IMetaStore {
}
// Finally, return the created table
return await this.tables.loadById(table.id)
return table
} catch (error: any) {
this.tables.del(table.id)
throw error
@@ -865,7 +858,21 @@ export default class MetaStore implements IMetaStore {
// Update table's realtime configuration
await this.updateTableRealtime(table, isRealtimeEnabled)
return { table: await this.tables.loadById(table.id), hasError }
const queryClient = getQueryClient()
const project = await getCachedProjectDetail(queryClient, this.rootStore.ui.selectedProjectRef)
const projectRef = project?.ref
const connectionString = project?.connectionString
queryClient.invalidateQueries(tableKeys.table(projectRef, table.id))
return {
table: await getTable({
projectRef,
connectionString,
id: table.id,
}),
hasError,
}
}
async insertRowsViaSpreadsheet(
@@ -1000,9 +1007,6 @@ export default class MetaStore implements IMetaStore {
this.columns.setUrl(`${this.baseUrl}/columns`)
this.columns.setHeaders(this.headers)
this.schemas.setUrl(`${this.baseUrl}/schemas`)
this.schemas.setHeaders(this.headers)
this.views.setUrl(`${this.baseUrl}/views`)
this.views.setHeaders(this.headers)
-24
View File
@@ -1,24 +0,0 @@
import type { PostgresSchema } from '@supabase/postgres-meta'
import PostgresMetaInterface from '../common/PostgresMetaInterface'
import { IRootStore } from '../RootStore'
export default class SchemaStore extends PostgresMetaInterface<PostgresSchema> {
constructor(
rootStore: IRootStore,
dataUrl: string,
headers?: {
[prop: string]: any
},
options?: { identifier: string }
) {
super(rootStore, dataUrl, headers, options)
}
// Dashboard to hide schemas with pg_ prefix
list(filter: any) {
const schemasFilter = (schema: PostgresSchema) =>
!schema.name.startsWith('pg_') && (typeof filter === 'function' ? filter(schema) : true)
return super.list(schemasFilter)
}
}