mirror of
https://github.com/supabase/supabase.git
synced 2026-10-09 19:35:06 +03:00
Merge branch 'master' into patch-3
This commit is contained in:
90 files changed
+2892
-1974
No files matched your search
+36
-2
@@ -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).
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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} />
|
||||
})}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -60,6 +60,7 @@ Sreyas Udayavarman
|
||||
Stanislav M
|
||||
Steve Chavez
|
||||
Stojan Dimitrovski
|
||||
Supun Sudaraka Kalidasa
|
||||
Terry Sutton
|
||||
Thor Schaeff
|
||||
Tyler Shukert
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
@@ -33,3 +33,5 @@ yarn-error.log*
|
||||
.env.production.local
|
||||
|
||||
.vercel
|
||||
|
||||
/public/dashboard
|
||||
@@ -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
|
||||
|
||||
@@ -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} />}
|
||||
|
||||
@@ -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} />
|
||||
|
||||
+1
-1
@@ -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
|
||||
+5
-6
@@ -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'
|
||||
|
||||
+18
-19
@@ -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}
|
||||
|
||||
+13
-15
@@ -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>
|
||||
|
||||
+3
-3
@@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
+11
-11
@@ -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,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,
|
||||
|
||||
@@ -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 }
|
||||
)
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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]
|
||||
)
|
||||
}
|
||||
@@ -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]
|
||||
)
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
)
|
||||
@@ -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 }
|
||||
)
|
||||
@@ -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
|
||||
@@ -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])
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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
|
||||
@@ -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} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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,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">
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user