mirror of
https://github.com/supabase/supabase.git
synced 2026-10-07 02:15:05 +03:00
38 files changed
+781
-472
No files matched your search
@@ -5,25 +5,39 @@ description: Convenience helpers for implementing user authentication in SvelteK
|
||||
sidebar_label: SvelteKit
|
||||
---
|
||||
|
||||
import Tabs from '@theme/Tabs'
|
||||
import TabItem from '@theme/TabItem'
|
||||
|
||||
This submodule provides convenience helpers for implementing user authentication in [SvelteKit](https://kit.svelte.dev/) applications.
|
||||
|
||||
## Installation
|
||||
|
||||
Using [npm](https://npmjs.org):
|
||||
This library supports Node.js `^16.15.0`.
|
||||
|
||||
<Tabs
|
||||
groupId="install"
|
||||
defaultValue="npm"
|
||||
values={[
|
||||
{label: 'npm', value: 'npm'},
|
||||
{label: 'Yarn', value: 'yarn'},
|
||||
]}>
|
||||
|
||||
<TabItem value="npm">
|
||||
|
||||
```sh
|
||||
npm install @supabase/auth-helpers-sveltekit
|
||||
```
|
||||
|
||||
Using [yarn](https://yarnpkg.com/):
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="yarn">
|
||||
|
||||
```sh
|
||||
yarn add @supabase/auth-helpers-sveltekit
|
||||
```
|
||||
|
||||
This library supports the following tooling versions:
|
||||
|
||||
- Node.js: `^16.15.0`
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Getting Started
|
||||
|
||||
@@ -37,12 +51,11 @@ PUBLIC_SUPABASE_URL=https://your-project.supabase.co
|
||||
PUBLIC_SUPABASE_ANON_KEY=your-anon-key
|
||||
```
|
||||
|
||||
### SupabaseClient setup
|
||||
### Set up the Supabase client
|
||||
|
||||
We will start off by creating a `db.ts` file inside of our `src/lib` directory. Now lets instantiate our `supabaseClient`.
|
||||
Start off by creating a `db.ts` file inside of the `src/lib` directory and instantiate the `supabaseClient`.
|
||||
|
||||
```ts
|
||||
// src/lib/db.ts
|
||||
```ts title="src/lib/db.ts"
|
||||
import { createClient } from '@supabase/supabase-js';
|
||||
import { setupSupabaseHelpers } from '@supabase/auth-helpers-sveltekit';
|
||||
import { dev } from '$app/environment';
|
||||
@@ -65,12 +78,11 @@ setupSupabaseHelpers({
|
||||
|
||||
### Initialize the client
|
||||
|
||||
Edit your `+layout.svelte` file and set up the client side.
|
||||
Edit your `+layout.svelte` file and set up the client-side.
|
||||
|
||||
```html
|
||||
<!-- src/routes/+layout.svelte -->
|
||||
```html title="src/routes/+layout.svelte"
|
||||
<script lang="ts">
|
||||
// we need to make sure the supabase instance is initialized on the client
|
||||
// make sure the supabase instance is initialized on the client
|
||||
import '$lib/db';
|
||||
import { startSupabaseSessionSync } from '@supabase/auth-helpers-sveltekit';
|
||||
import { page } from '$app/stores';
|
||||
@@ -86,14 +98,12 @@ Edit your `+layout.svelte` file and set up the client side.
|
||||
<slot />
|
||||
```
|
||||
|
||||
### Hooks setup
|
||||
### Set up hooks
|
||||
|
||||
Our `hooks.ts` file is where the heavy lifting of this library happens:
|
||||
The `hooks.ts` file is where the heavy lifting of this library happens:
|
||||
|
||||
```ts
|
||||
// src/hooks.server.ts
|
||||
|
||||
// we need to make sure the supabase instance is initialized on the server
|
||||
```ts title="src/hooks.server.ts"
|
||||
// make sure the supabase instance is initialized on the server
|
||||
import '$lib/db';
|
||||
import { dev } from '$app/environment';
|
||||
import { auth } from '@supabase/auth-helpers-sveltekit/server';
|
||||
@@ -109,18 +119,17 @@ export const handle = sequence(auth(), yourHandler);
|
||||
There are three handle methods available:
|
||||
|
||||
- `callback()`:
|
||||
This will create a handler for `/api/auth/callback`. The `client` forwards the session details here every time `onAuthStateChange` fires on the client side. This is needed to set up the cookies for your application so that SSR works seamlessly.
|
||||
This creates a handler for `/api/auth/callback`. The `client` forwards the session details here every time `onAuthStateChange` fires on the client-side. This is needed to set up the cookies for your application so that SSR works seamlessly.
|
||||
- `session()`:
|
||||
This will parse the session from the cookie and populate it in locals
|
||||
This parses the session from the cookie and populate it in locals
|
||||
- `auth()`:
|
||||
a shorthand for `sequence(callback(), session())` that uses both handlers
|
||||
|
||||
### Send session to client
|
||||
|
||||
In order to make the session available to the UI (pages, layouts) we need to pass the session in the root layout load function:
|
||||
In order to make the session available to the UI (e.g., pages and layouts), pass the session in the root layout load function:
|
||||
|
||||
```ts
|
||||
// src/routes/+layout.server.ts
|
||||
```ts title="src/routes/+layout.server.ts"
|
||||
import type { LayoutServerLoad } from './$types';
|
||||
|
||||
export const load: LayoutServerLoad = async ({ locals }) => {
|
||||
@@ -132,10 +141,9 @@ export const load: LayoutServerLoad = async ({ locals }) => {
|
||||
|
||||
### Typings
|
||||
|
||||
In order to get the most out of TypeScript and it´s intellisense, you should import our types into the `app.d.ts` type definition file that comes with your SvelteKit project.
|
||||
In order to get the most out of TypeScript and IntelliSense, import the types into the `app.d.ts` type definition file that comes with your SvelteKit project.
|
||||
|
||||
```ts
|
||||
// src/app.d.ts
|
||||
```ts title="src/app.d.ts"
|
||||
/// <reference types="@sveltejs/kit" />
|
||||
|
||||
// See https://kit.svelte.dev/docs/types#app
|
||||
@@ -159,8 +167,7 @@ This library has provided a method to delete the session cookie.
|
||||
|
||||
Create a server only page with an action to delete the cookie
|
||||
|
||||
```ts
|
||||
// src/routes/logout/+page.server.ts
|
||||
```ts title="src/routes/logout/+page.server.ts"
|
||||
import { deleteSession } from '@supabase/auth-helpers-sveltekit/server';
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import type { Actions } from './$types';
|
||||
@@ -184,12 +191,11 @@ Then make a **POST** request with a form to the server only page above.
|
||||
</form>
|
||||
```
|
||||
|
||||
### Basic Setup
|
||||
### Basic setup
|
||||
|
||||
You can now determine if a user is authenticated on the client-side by checking that the `user` object in `$page.data.session` is defined.
|
||||
|
||||
```html
|
||||
<!-- src/routes/+page.svelte -->
|
||||
```html title="src/routes/+page.svelte"
|
||||
<script>
|
||||
import { page } from '$app/stores';
|
||||
</script>
|
||||
@@ -230,8 +236,7 @@ For [row level security](https://supabase.com/docs/learn/auth-deep-dive/auth-row
|
||||
|
||||
## Server-side data fetching with RLS
|
||||
|
||||
```html
|
||||
<!-- src/routes/profile/+page.svelte -->
|
||||
```html title="src/routes/profile/+page.svelte"
|
||||
<script>
|
||||
/** @type {import('./$types').PageData} */
|
||||
export let data;
|
||||
@@ -245,8 +250,7 @@ For [row level security](https://supabase.com/docs/learn/auth-deep-dive/auth-row
|
||||
|
||||
For [row level security](https://supabase.com/docs/learn/auth-deep-dive/auth-row-level-security) to work in a server environment, you need to use the `withAuth` helper to check if the user is authenticated. The helper extends the event with `session` and `getSupabaseClient()`:
|
||||
|
||||
```ts
|
||||
// src/routes/profile/+page.ts
|
||||
```ts title="src/routes/profile/+page.ts"
|
||||
import type { PageLoad } from './$types';
|
||||
import { withAuth } from '@supabase/auth-helpers-sveltekit';
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
@@ -291,8 +295,7 @@ await getSupabaseClient().from('table2').select();
|
||||
|
||||
Wrap an API Route to check that the user has a valid session. If they're not logged in the session is `null`.
|
||||
|
||||
```ts
|
||||
// src/routes/api/protected-route/+server.ts
|
||||
```ts title="src/routes/api/protected-route/+server.ts"
|
||||
import type { RequestHandler } from './$types';
|
||||
import { withAuth } from '@supabase/auth-helpers-sveltekit';
|
||||
import { json, redirect } from '@sveltejs/kit';
|
||||
@@ -318,10 +321,9 @@ If you visit `/api/protected-route` without a valid session cookie, you will get
|
||||
|
||||
## Protecting Actions
|
||||
|
||||
Wrap an Action to check that the user has a valid session. If they're not logged in the session is `null`.
|
||||
Wrap an Action to check that the user has a valid session. If they're not logged in, the session is `null`.
|
||||
|
||||
```ts
|
||||
// src/routes/posts/+page.server.ts
|
||||
```ts title="src/routes/posts/+page.server.ts"
|
||||
import type { Actions } from './$types';
|
||||
import { withAuth } from '@supabase/auth-helpers-sveltekit';
|
||||
import { error, invalid } from '@sveltejs/kit';
|
||||
@@ -419,10 +421,9 @@ export const actions: Actions = {
|
||||
|
||||
## Custom session namespace
|
||||
|
||||
If you wan´t to use something else than `locals.session` and `$page.data.session` you can do so by updating the types and creating three helper functions:
|
||||
If you want to use something other than `locals.session` and `$page.data.session`, update the types and create three helper functions:
|
||||
|
||||
```ts
|
||||
// src/app.d.ts
|
||||
```ts title="src/app.d.ts"
|
||||
declare namespace App {
|
||||
interface Locals {
|
||||
mySupabaseSession: import('@supabase/auth-helpers-sveltekit').SupabaseSession;
|
||||
@@ -431,8 +432,9 @@ declare namespace App {
|
||||
mySupabaseSession: import('@supabase/auth-helpers-sveltekit').SupabaseSession;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
// src/hooks.server.ts
|
||||
```ts title="src/hooks.server.ts"
|
||||
setupSupabaseServer({
|
||||
supabaseClient,
|
||||
cookieOptions: {
|
||||
@@ -442,8 +444,9 @@ setupSupabaseServer({
|
||||
getSessionFromLocals: (locals) => locals.mySupabaseSession,
|
||||
setSessionToLocals: (locals, session) => (locals.mySupabaseSession = session)
|
||||
});
|
||||
```
|
||||
|
||||
// src/lib/db.ts
|
||||
```ts title="src/lib/db.ts"
|
||||
setupSupabaseClient({
|
||||
supabaseClient,
|
||||
// --- change location within pageData ---
|
||||
@@ -451,23 +454,26 @@ setupSupabaseClient({
|
||||
});
|
||||
```
|
||||
|
||||
## Migration from 0.6.11 and below to 0.7.0
|
||||
## Migrate from 0.6.11 and below to 0.7.0 {#migration}
|
||||
|
||||
There are quite a lot of breaking changes in the latest 0.7.0 version of this library.
|
||||
There are numerous breaking changes in the latest 0.7.0 version of this library.
|
||||
|
||||
### Environment variable prefix has changed
|
||||
### Environment variable prefix
|
||||
|
||||
0.6.11 and below: `VITE_`
|
||||
The environment variable prefix is now `PUBLIC_` instead of `VITE_` (e.g., `VITE_SUPABASE_URL` is now `PUBLIC_SUPABASE_URL`).
|
||||
|
||||
0.7.0: `PUBLIC_`
|
||||
### Set up the Supabase client {#migration-set-up-supabase-client}
|
||||
|
||||
e.g. `VITE_SUPABASE_URL` is now `PUBLIC_SUPABASE_URL`
|
||||
<Tabs
|
||||
groupId="version"
|
||||
values={[
|
||||
{label: '0.6.11 and below', value: 'older'},
|
||||
{label: '0.7.0', value: 'latest'},
|
||||
]}>
|
||||
|
||||
### Setup Supabase client
|
||||
<TabItem value="older">
|
||||
|
||||
0.6.11 and below:
|
||||
```js
|
||||
// src/lib/db.ts
|
||||
```js title="src/lib/db.ts"
|
||||
import { createSupabaseClient } from '@supabase/auth-helpers-sveltekit';
|
||||
|
||||
const { supabaseClient } = createSupabaseClient(
|
||||
@@ -478,9 +484,11 @@ const { supabaseClient } = createSupabaseClient(
|
||||
export { supabaseClient };
|
||||
```
|
||||
|
||||
0.7.0:
|
||||
```js
|
||||
// src/lib/db.ts
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="latest">
|
||||
|
||||
```js title="src/lib/db.ts"
|
||||
import { createClient } from '@supabase/supabase-js';
|
||||
import { setupSupabaseHelpers } from '@supabase/auth-helpers-sveltekit';
|
||||
import { dev } from '$app/environment';
|
||||
@@ -502,11 +510,21 @@ setupSupabaseHelpers({
|
||||
});
|
||||
```
|
||||
|
||||
### Initialize client side code
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
0.6.11 and below:
|
||||
```html
|
||||
<!-- src/routes/__layout.svelte -->
|
||||
### Initialize the client {#migration-initialize-client}
|
||||
|
||||
<Tabs
|
||||
groupId="version"
|
||||
values={[
|
||||
{label: '0.6.11 and below', value: 'older'},
|
||||
{label: '0.7.0', value: 'latest'},
|
||||
]}>
|
||||
|
||||
<TabItem value="older">
|
||||
|
||||
```html title="src/routes/__layout.svelte"
|
||||
<script>
|
||||
import { session } from '$app/stores';
|
||||
import { supabaseClient } from '$lib/db';
|
||||
@@ -518,11 +536,15 @@ import { SupaAuthHelper } from '@supabase/auth-helpers-svelte';
|
||||
</SupaAuthHelper>
|
||||
```
|
||||
|
||||
0.7.0:
|
||||
```html
|
||||
<!-- src/routes/+layout.svelte -->
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="latest">
|
||||
|
||||
The `@supabase/auth-helpers-svelte` library is no longer required as the `@supabase/auth-helpers-sveltekit` library handles all the client-side code.
|
||||
|
||||
```html title="src/routes/+layout.svelte"
|
||||
<script lang="ts">
|
||||
// we need to make sure the supabase instance is initialized on the client
|
||||
// make sure the supabase instance is initialized on the client
|
||||
import '$lib/db';
|
||||
import { startSupabaseSessionSync } from '@supabase/auth-helpers-sveltekit';
|
||||
import { page } from '$app/stores';
|
||||
@@ -538,13 +560,21 @@ import { SupaAuthHelper } from '@supabase/auth-helpers-svelte';
|
||||
<slot />
|
||||
```
|
||||
|
||||
> We no longer require the @supabase/auth-helpers-svelte library as the @supabase/auth-helpers-sveltekit library handles all the client side code too
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
### Hooks setup
|
||||
### Set up hooks {#migration-set-up-hooks}
|
||||
|
||||
0.6.11 and below:
|
||||
```ts
|
||||
// src/hooks.ts
|
||||
<Tabs
|
||||
groupId="version"
|
||||
values={[
|
||||
{label: '0.6.11 and below', value: 'older'},
|
||||
{label: '0.7.0', value: 'latest'},
|
||||
]}>
|
||||
|
||||
<TabItem value="older">
|
||||
|
||||
```ts title="src/hooks.ts"
|
||||
import { handleAuth } from '@supabase/auth-helpers-sveltekit';
|
||||
import type { GetSession, Handle } from '@sveltejs/kit';
|
||||
import { sequence } from '@sveltejs/kit/hooks';
|
||||
@@ -561,10 +591,12 @@ export const getSession: GetSession = async (event) => {
|
||||
}
|
||||
```
|
||||
|
||||
0.7.0:
|
||||
```ts
|
||||
// src/hooks.server.ts
|
||||
// we need to make sure the supabase instance is initialized on the server
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="latest">
|
||||
|
||||
```ts title="src/hooks.server.ts"
|
||||
// make sure the supabase instance is initialized on the server
|
||||
import '$lib/db';
|
||||
import { dev } from '$app/environment';
|
||||
import { auth } from '@supabase/auth-helpers-sveltekit/server';
|
||||
@@ -573,9 +605,9 @@ export const handle = auth();
|
||||
```
|
||||
|
||||
**Optional** *if using additional handle methods*
|
||||
```ts
|
||||
// src/hooks.server.ts
|
||||
// we need to make sure the supabase instance is initialized on the server
|
||||
|
||||
```ts title="src/hooks.server.ts"
|
||||
// make sure the supabase instance is initialized on the server
|
||||
import '$lib/db';
|
||||
import { dev } from '$app/environment';
|
||||
import { auth } from '@supabase/auth-helpers-sveltekit/server';
|
||||
@@ -584,10 +616,21 @@ import { sequence } from '@sveltejs/kit/hooks';
|
||||
export const handle = sequence(auth(), yourHandler);
|
||||
```
|
||||
|
||||
### Typings
|
||||
0.6.11 and below:
|
||||
```ts
|
||||
// src/app.d.ts
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
### Typings {#migration-typings}
|
||||
|
||||
<Tabs
|
||||
groupId="version"
|
||||
values={[
|
||||
{label: '0.6.11 and below', value: 'older'},
|
||||
{label: '0.7.0', value: 'latest'},
|
||||
]}>
|
||||
|
||||
<TabItem value="older">
|
||||
|
||||
```ts title="src/app.d.ts"
|
||||
/// <reference types="@sveltejs/kit" />
|
||||
// See https://kit.svelte.dev/docs/types#app
|
||||
// for information about these interfaces
|
||||
@@ -608,9 +651,11 @@ declare namespace App {
|
||||
}
|
||||
```
|
||||
|
||||
0.7.0:
|
||||
```ts
|
||||
// src/app.d.ts
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="latest">
|
||||
|
||||
```ts title="src/app.d.ts"
|
||||
/// <reference types="@sveltejs/kit" />
|
||||
|
||||
// See https://kit.svelte.dev/docs/types#app
|
||||
@@ -630,11 +675,21 @@ declare namespace App {
|
||||
}
|
||||
```
|
||||
|
||||
### Checking user on the client
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
0.6.11 and below:
|
||||
```html
|
||||
<!-- src/routes/index.svelte -->
|
||||
### Check the user on the client
|
||||
|
||||
<Tabs
|
||||
groupId="version"
|
||||
values={[
|
||||
{label: '0.6.11 and below', value: 'older'},
|
||||
{label: '0.7.0', value: 'latest'},
|
||||
]}>
|
||||
|
||||
<TabItem value="older">
|
||||
|
||||
```html title="src/routes/index.svelte"
|
||||
<script>
|
||||
import { session } from '$app/stores';
|
||||
</script>
|
||||
@@ -647,9 +702,11 @@ declare namespace App {
|
||||
{/if}
|
||||
```
|
||||
|
||||
0.7.0:
|
||||
```html
|
||||
<!-- src/routes/+page.svelte -->
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="latest">
|
||||
|
||||
```html title="src/routes/+page.svelte"
|
||||
<script>
|
||||
import { page } from '$app/stores';
|
||||
</script>
|
||||
@@ -662,11 +719,21 @@ declare namespace App {
|
||||
{/if}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
### withPageAuth
|
||||
|
||||
0.6.11 and below:
|
||||
```html
|
||||
<!-- // src/routes/protected-route.svelte -->
|
||||
<Tabs
|
||||
groupId="version"
|
||||
values={[
|
||||
{label: '0.6.11 and below', value: 'older'},
|
||||
{label: '0.7.0', value: 'latest'},
|
||||
]}>
|
||||
|
||||
<TabItem value="older">
|
||||
|
||||
```html title="src/routes/protected-route.svelte"
|
||||
<script lang="ts" context="module">
|
||||
import { supabaseServerClient, withPageAuth } from '@supabase/auth-helpers-sveltekit';
|
||||
import type { Load } from './__types/protected-page';
|
||||
@@ -696,9 +763,11 @@ export let user;
|
||||
<pre>{JSON.stringify(user, null, 2)}</pre>
|
||||
```
|
||||
|
||||
0.7.0:
|
||||
```html
|
||||
<!-- src/routes/protected-route/+page.svelte -->
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="latest">
|
||||
|
||||
```html title="src/routes/protected-route/+page.svelte"
|
||||
<script lang="ts">
|
||||
import type { PageData } from './$types';
|
||||
|
||||
@@ -714,8 +783,7 @@ export let user;
|
||||
<pre>{JSON.stringify(user, null, 2)}</pre>
|
||||
```
|
||||
|
||||
```ts
|
||||
// src/routes/protected-route/+page.ts
|
||||
```ts title="src/routes/protected-route/+page.ts"
|
||||
import { withAuth } from '@supabase/auth-helpers-sveltekit';
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import type { PageLoad } from './$types';
|
||||
@@ -730,11 +798,21 @@ export const load: PageLoad = withAuth(async ({ session, getSupabaseClient }) =>
|
||||
});
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
### withApiAuth
|
||||
|
||||
0.6.11 and below:
|
||||
```ts
|
||||
// src/routes/api/protected-route.ts
|
||||
<Tabs
|
||||
groupId="version"
|
||||
values={[
|
||||
{label: '0.6.11 and below', value: 'older'},
|
||||
{label: '0.7.0', value: 'latest'},
|
||||
]}>
|
||||
|
||||
<TabItem value="older">
|
||||
|
||||
```ts title="src/routes/api/protected-route.ts"
|
||||
import { supabaseServerClient, withApiAuth } from "@supabase/auth-helpers-sveltekit";
|
||||
import type { RequestHandler } from "./__types/protected-route";
|
||||
|
||||
@@ -759,10 +837,11 @@ export const GET: RequestHandler<GetOutput> = async ({ locals, request }) =>
|
||||
});
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
|
||||
0.7.0:
|
||||
```ts
|
||||
// src/routes/api/protected-route/+server.ts
|
||||
<TabItem value="latest">
|
||||
|
||||
```ts title="src/routes/api/protected-route/+server.ts"
|
||||
import type { RequestHandler } from './$types';
|
||||
import { withAuth } from '@supabase/auth-helpers-sveltekit';
|
||||
import { json, redirect } from '@sveltejs/kit';
|
||||
@@ -785,6 +864,9 @@ export const GET: RequestHandler = withAuth(async ({ session, getSupabaseClient
|
||||
);
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Additional Links
|
||||
|
||||
- [Auth Helpers Source code](https://github.com/supabase/auth-helpers)
|
||||
|
||||
@@ -70,5 +70,8 @@ ENABLE_PHONE_AUTOCONFIRM=true
|
||||
# Studio - Configuration for the Dashboard
|
||||
############
|
||||
|
||||
STUDIO_ORGANIZATION_NAME=Default Organization
|
||||
STUDIO_PROJECT_NAME=Default Project
|
||||
|
||||
STUDIO_PORT=3000
|
||||
PUBLIC_REST_URL=http://localhost:8000/rest/v1/ # replace if you intend to use Studio outside of localhost
|
||||
@@ -17,6 +17,9 @@ services:
|
||||
STUDIO_PG_META_URL: http://meta:8080
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
|
||||
DEFAULT_ORGANIZATION: ${STUDIO_DEFAULT_ORGANIZATION}
|
||||
DEFAULT_PROJECT: ${STUDIO_DEFAULT_PROJECT}
|
||||
|
||||
SUPABASE_URL: http://kong:8000
|
||||
SUPABASE_REST_URL: ${PUBLIC_REST_URL}
|
||||
SUPABASE_ANON_KEY: ${ANON_KEY}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
STUDIO_PG_META_URL=http://localhost:8000/pg
|
||||
POSTGRES_PASSWORD=your-super-secret-and-long-postgres-password
|
||||
|
||||
DEFAULT_ORGANIZATION_NAME=Default Organization
|
||||
DEFAULT_PROJECT_NAME=Default Project
|
||||
|
||||
SUPABASE_URL=http://localhost:8000
|
||||
SUPABASE_REST_URL=http://localhost:8000/rest/v1/
|
||||
SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE
|
||||
|
||||
@@ -67,3 +67,10 @@ Then run the following commands to install dependencies and start the dashboard.
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
If you would like to configure different defaults for "Default Organization" and "Default Project", you will need to update the `.env` in the studio folder with the corresponding values.
|
||||
|
||||
```
|
||||
DEFAULT_ORGANIZATION_NAME=
|
||||
DEFAULT_PROJECT_NAME=
|
||||
```
|
||||
@@ -79,10 +79,20 @@ export function initTable(
|
||||
? onLoadStorage(props.storageRef, table.name, table.schema)
|
||||
: undefined
|
||||
|
||||
// Load sort and filters via URL param only if given
|
||||
// Check for saved state on initial load and also, load sort and filters via URL param only if given
|
||||
// Otherwise load from local storage to resume user session
|
||||
if (sort === undefined && filter === undefined && (savedState?.sorts || savedState?.filters)) {
|
||||
return { savedState: { sorts: savedState.sorts, filters: savedState.filters } }
|
||||
if (
|
||||
!state.isInitialComplete &&
|
||||
sort === undefined &&
|
||||
filter === undefined &&
|
||||
(savedState?.sorts || savedState?.filters)
|
||||
) {
|
||||
return {
|
||||
savedState: {
|
||||
sorts: savedState.sorts,
|
||||
filters: savedState.filters,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const gridColumns = getGridColumns(table, {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import * as React from 'react'
|
||||
import { FC, useCallback, KeyboardEvent, ReactNode } from 'react'
|
||||
|
||||
type BlockKeysProps = {
|
||||
interface BlockKeysProps {
|
||||
value: string | null
|
||||
children: ReactNode
|
||||
onEscape?: (value: string | null) => void
|
||||
onEnter?: (value: string | null) => void
|
||||
value: string | null
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -11,9 +12,9 @@ type BlockKeysProps = {
|
||||
* We use this with cell editor to allow editor component to handle keys.
|
||||
* Example: press enter to add newline on textEditor
|
||||
*/
|
||||
export const BlockKeys: React.FC<BlockKeysProps> = ({ onEscape, onEnter, value, children }) => {
|
||||
const handleKeyDown = React.useCallback(
|
||||
(ev: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
export const BlockKeys: FC<BlockKeysProps> = ({ value, children, onEscape, onEnter }) => {
|
||||
const handleKeyDown = useCallback(
|
||||
(ev: KeyboardEvent<HTMLDivElement>) => {
|
||||
switch (ev.key) {
|
||||
case 'Escape':
|
||||
ev.stopPropagation()
|
||||
@@ -21,7 +22,10 @@ export const BlockKeys: React.FC<BlockKeysProps> = ({ onEscape, onEnter, value,
|
||||
break
|
||||
case 'Enter':
|
||||
ev.stopPropagation()
|
||||
if (onEnter) onEnter(value)
|
||||
if (!ev.shiftKey && onEnter) {
|
||||
ev.preventDefault()
|
||||
onEnter(value)
|
||||
}
|
||||
break
|
||||
}
|
||||
},
|
||||
@@ -29,7 +33,7 @@ export const BlockKeys: React.FC<BlockKeysProps> = ({ onEscape, onEnter, value,
|
||||
)
|
||||
|
||||
function onBlur() {
|
||||
if (onEscape) onEscape(value)
|
||||
if (onEnter) onEnter(value)
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as React from 'react'
|
||||
import { useState, useCallback } from 'react'
|
||||
|
||||
import { EditorProps } from '@supabase/react-data-grid'
|
||||
import { useTrackedState } from '../../store'
|
||||
@@ -11,18 +11,22 @@ export function JsonEditor<TRow, TSummaryRow = unknown>({
|
||||
onRowChange,
|
||||
}: EditorProps<TRow, TSummaryRow>) {
|
||||
const state = useTrackedState()
|
||||
const [isPopoverOpen, setIsPopoverOpen] = React.useState(true)
|
||||
const [isPopoverOpen, setIsPopoverOpen] = useState(true)
|
||||
const gridColumn = state.gridColumns.find((x) => x.name == column.key)
|
||||
const initialValue = row[column.key as keyof TRow] as unknown
|
||||
const jsonString = initialValue ? JSON.stringify(initialValue) : ''
|
||||
const prettyJsonValue = prettifyJSON(jsonString)
|
||||
const [value, setValue] = React.useState<string | null>(prettyJsonValue)
|
||||
const [value, setValue] = useState<string | null>(prettyJsonValue)
|
||||
|
||||
const onEscape = React.useCallback((newValue: string | null) => {
|
||||
commitChange(newValue)
|
||||
const cancelChanges = useCallback(() => {
|
||||
onRowChange(row, true)
|
||||
setIsPopoverOpen(false)
|
||||
}, [])
|
||||
|
||||
const saveChanges = useCallback((newValue: string | null) => {
|
||||
commitChange(newValue)
|
||||
}, [])
|
||||
|
||||
function onChange(_value: string | undefined) {
|
||||
if (!_value || _value == '') setValue(null)
|
||||
else setValue(_value)
|
||||
@@ -31,12 +35,14 @@ export function JsonEditor<TRow, TSummaryRow = unknown>({
|
||||
function commitChange(newValue: string | null) {
|
||||
if (!newValue) {
|
||||
onRowChange({ ...row, [column.key]: null }, true)
|
||||
setIsPopoverOpen(false)
|
||||
} else if (verifyJSON(newValue)) {
|
||||
const jsonValue = JSON.parse(newValue)
|
||||
onRowChange({ ...row, [column.key]: jsonValue }, true)
|
||||
setIsPopoverOpen(false)
|
||||
} else {
|
||||
const { onError } = state
|
||||
if (onError) onError(Error('invalid input'))
|
||||
if (onError) onError(Error('Please enter a valid JSON'))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,13 +54,17 @@ export function JsonEditor<TRow, TSummaryRow = unknown>({
|
||||
sideOffset={-35}
|
||||
className="rounded-none"
|
||||
overlay={
|
||||
<BlockKeys value={value} onEscape={onEscape}>
|
||||
<BlockKeys value={value} onEscape={cancelChanges} onEnter={saveChanges}>
|
||||
<MonacoEditor
|
||||
width={`${gridColumn?.width || column.width}px`}
|
||||
value={value ?? ''}
|
||||
language="json"
|
||||
onChange={onChange}
|
||||
/>
|
||||
<div className="flex items-center justify-end p-2 bg-scale-400 space-x-2">
|
||||
<p className="text-xs text-scale-1100">Save changes</p>
|
||||
<code className="text-xs">⏎</code>
|
||||
</div>
|
||||
</BlockKeys>
|
||||
}
|
||||
>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as React from 'react'
|
||||
import { useState, useCallback } from 'react'
|
||||
import { EditorProps } from '@supabase/react-data-grid'
|
||||
import { useTrackedState } from '../../store'
|
||||
import { BlockKeys, MonacoEditor, NullValue, EmptyValue } from '../common'
|
||||
@@ -10,12 +10,17 @@ export function TextEditor<TRow, TSummaryRow = unknown>({
|
||||
onRowChange,
|
||||
}: EditorProps<TRow, TSummaryRow>) {
|
||||
const state = useTrackedState()
|
||||
const [isPopoverOpen, setIsPopoverOpen] = React.useState(true)
|
||||
const [isPopoverOpen, setIsPopoverOpen] = useState(true)
|
||||
const gridColumn = state.gridColumns.find((x) => x.name == column.key)
|
||||
const initialValue = row[column.key as keyof TRow] as unknown as string
|
||||
const [value, setValue] = React.useState<string | null>(initialValue)
|
||||
const [value, setValue] = useState<string | null>(initialValue)
|
||||
|
||||
const onEscape = React.useCallback((newValue: string | null) => {
|
||||
const cancelChanges = useCallback(() => {
|
||||
onRowChange(row, true)
|
||||
setIsPopoverOpen(false)
|
||||
}, [])
|
||||
|
||||
const saveChanges = useCallback((newValue: string | null) => {
|
||||
onRowChange({ ...row, [column.key]: newValue }, true)
|
||||
setIsPopoverOpen(false)
|
||||
}, [])
|
||||
@@ -33,12 +38,16 @@ export function TextEditor<TRow, TSummaryRow = unknown>({
|
||||
sideOffset={-35}
|
||||
className="rounded-none"
|
||||
overlay={
|
||||
<BlockKeys value={value} onEscape={onEscape}>
|
||||
<BlockKeys value={value} onEscape={cancelChanges} onEnter={saveChanges}>
|
||||
<MonacoEditor
|
||||
width={`${gridColumn?.width || column.width}px`}
|
||||
value={value ?? ''}
|
||||
onChange={onChange}
|
||||
/>
|
||||
<div className="flex items-center justify-end p-2 bg-scale-400 space-x-2">
|
||||
<p className="text-xs text-scale-1100">Save changes</p>
|
||||
<code className="text-xs">⏎</code>
|
||||
</div>
|
||||
</BlockKeys>
|
||||
}
|
||||
>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FC, ReactNode, useState } from 'react'
|
||||
import { FC, useState } from 'react'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import {
|
||||
Input,
|
||||
@@ -10,66 +10,40 @@ import {
|
||||
IconTrash,
|
||||
} from '@supabase/ui'
|
||||
|
||||
import Table from '../../../to-be-cleaned/Table'
|
||||
import { useStore } from 'hooks'
|
||||
import Table from 'components/to-be-cleaned/Table'
|
||||
import NoSearchResults from 'components/to-be-cleaned/NoSearchResults'
|
||||
import { PostgresTable } from '@supabase/postgres-meta'
|
||||
|
||||
const Header: FC<{
|
||||
filterString: string
|
||||
filterPlaceholder: string
|
||||
leftComponents: ReactNode
|
||||
rightComponents: ReactNode
|
||||
setFilterString: (value: string) => void
|
||||
}> = ({
|
||||
filterString,
|
||||
setFilterString,
|
||||
filterPlaceholder = 'Filter',
|
||||
leftComponents,
|
||||
rightComponents,
|
||||
}) => (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
<div>{leftComponents}</div>
|
||||
<div>
|
||||
<Input
|
||||
size="small"
|
||||
placeholder={filterPlaceholder}
|
||||
value={filterString}
|
||||
onChange={(e: any) => setFilterString(e.target.value)}
|
||||
icon={<IconSearch size="tiny" />}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="">{rightComponents}</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
const ColumnList: FC<{
|
||||
selectedTable: any
|
||||
interface Props {
|
||||
selectedTable: PostgresTable
|
||||
onSelectBack: () => void
|
||||
onAddColumn: () => void
|
||||
onEditColumn: (column: any) => void
|
||||
onDeleteColumn: (column: any) => void
|
||||
}> = ({
|
||||
}
|
||||
|
||||
const ColumnList: FC<Props> = ({
|
||||
selectedTable,
|
||||
onSelectBack = () => {},
|
||||
onAddColumn = () => {},
|
||||
onEditColumn = () => {},
|
||||
onDeleteColumn = () => {},
|
||||
}) => {
|
||||
const { meta } = useStore()
|
||||
const [filterString, setFilterString] = useState<string>('')
|
||||
const columns =
|
||||
filterString.length === 0
|
||||
? selectedTable.columns
|
||||
: selectedTable.columns.filter((column: any) => column.name.includes(filterString))
|
||||
|
||||
const isLocked = meta.excludedSchemas.includes(selectedTable?.schema ?? '')
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mb-4">
|
||||
<Header
|
||||
filterPlaceholder="Filter columns"
|
||||
filterString={filterString}
|
||||
setFilterString={setFilterString}
|
||||
leftComponents={
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
<div className="flex items-center mr-4">
|
||||
<Button
|
||||
type="outline"
|
||||
@@ -80,13 +54,24 @@ const ColumnList: FC<{
|
||||
/>
|
||||
<code>{selectedTable.name}</code>
|
||||
</div>
|
||||
}
|
||||
rightComponents={
|
||||
<Button icon={<IconPlus />} onClick={() => onAddColumn()}>
|
||||
New
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<div>
|
||||
<Input
|
||||
size="small"
|
||||
placeholder="Filter columns"
|
||||
value={filterString}
|
||||
onChange={(e: any) => setFilterString(e.target.value)}
|
||||
icon={<IconSearch size="tiny" />}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{!isLocked && (
|
||||
<div>
|
||||
<Button icon={<IconPlus />} onClick={() => onAddColumn()}>
|
||||
New column
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{columns.length === 0 ? (
|
||||
<NoSearchResults />
|
||||
@@ -111,8 +96,12 @@ const ColumnList: FC<{
|
||||
<Table.td>
|
||||
<p>{x.name}</p>
|
||||
</Table.td>
|
||||
<Table.td>
|
||||
<p>{x.comment}</p>
|
||||
<Table.td className="break-all whitespace-normal">
|
||||
{x.comment !== null ? (
|
||||
<p title={x.comment}>{x.comment}</p>
|
||||
) : (
|
||||
<p className="text-scale-800">No description</p>
|
||||
)}
|
||||
</Table.td>
|
||||
<Table.td>
|
||||
<code className="text-sm">{x.data_type}</code>
|
||||
@@ -127,12 +116,14 @@ const ColumnList: FC<{
|
||||
icon={<IconEdit3 />}
|
||||
style={{ padding: 5 }}
|
||||
type="text"
|
||||
disabled={isLocked}
|
||||
/>
|
||||
<Button
|
||||
onClick={() => onDeleteColumn(x)}
|
||||
icon={<IconTrash />}
|
||||
style={{ padding: 5 }}
|
||||
type="text"
|
||||
disabled={isLocked}
|
||||
/>
|
||||
</div>
|
||||
</Table.td>
|
||||
|
||||
@@ -1,155 +1,195 @@
|
||||
import { FC, ReactNode, useState } from 'react'
|
||||
import { FC, useState } from 'react'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import {
|
||||
Button,
|
||||
IconPlus,
|
||||
Input,
|
||||
Listbox,
|
||||
IconSearch,
|
||||
IconTrash,
|
||||
IconEdit3,
|
||||
IconColumns,
|
||||
IconLock,
|
||||
} from '@supabase/ui'
|
||||
import { partition } from 'lodash'
|
||||
|
||||
import { useStore } from 'hooks'
|
||||
import Table from 'components/to-be-cleaned/Table'
|
||||
import NoSearchResults from 'components/to-be-cleaned/NoSearchResults'
|
||||
import { PostgresTable, PostgresSchema } from '@supabase/postgres-meta'
|
||||
|
||||
const Header: FC<{
|
||||
filterString: string
|
||||
filterPlaceholder: string
|
||||
leftComponents: ReactNode
|
||||
rightComponents: ReactNode
|
||||
setFilterString: (value: string) => void
|
||||
}> = ({
|
||||
filterString,
|
||||
setFilterString,
|
||||
filterPlaceholder = 'Filter',
|
||||
leftComponents,
|
||||
rightComponents,
|
||||
}) => (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
<div>{leftComponents}</div>
|
||||
<div>
|
||||
<Input
|
||||
size="small"
|
||||
placeholder={filterPlaceholder}
|
||||
value={filterString}
|
||||
onChange={(e: any) => setFilterString(e.target.value)}
|
||||
icon={<IconSearch size="tiny" />}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="">{rightComponents}</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
const TableList: FC<{
|
||||
interface Props {
|
||||
selectedSchema: string
|
||||
onSelectSchema: (schema: string) => void
|
||||
onAddTable: () => void
|
||||
onEditTable: (table: any) => void
|
||||
onDeleteTable: (table: any) => void
|
||||
onOpenTable: (table: any) => void
|
||||
}> = observer(
|
||||
({
|
||||
onAddTable = () => {},
|
||||
onEditTable = () => {},
|
||||
onDeleteTable = () => {},
|
||||
onOpenTable = () => {},
|
||||
}) => {
|
||||
const { meta } = useStore()
|
||||
const [filterString, setFilterString] = useState<string>('')
|
||||
const tables =
|
||||
filterString.length === 0
|
||||
? meta.tables.list((table: any) => table.schema === 'public')
|
||||
: meta.tables.list(
|
||||
(table: any) => table.schema === 'public' && table.name.includes(filterString)
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
{/* @ts-ignore */}
|
||||
<Header
|
||||
filterString={filterString}
|
||||
setFilterString={setFilterString}
|
||||
rightComponents={
|
||||
<Button icon={<IconPlus />} onClick={() => onAddTable()}>
|
||||
New
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
{tables.length === 0 ? (
|
||||
<NoSearchResults />
|
||||
) : (
|
||||
<div className="my-4 w-full">
|
||||
<Table
|
||||
head={[
|
||||
<Table.th key="name">Name</Table.th>,
|
||||
<Table.th key="schema">Schema</Table.th>,
|
||||
<Table.th key="description" className="hidden lg:table-cell">
|
||||
Description
|
||||
</Table.th>,
|
||||
<Table.th key="rows" className="hidden xl:table-cell">
|
||||
Rows (Estimated)
|
||||
</Table.th>,
|
||||
<Table.th key="size" className="hidden xl:table-cell">
|
||||
Size (Estimated)
|
||||
</Table.th>,
|
||||
<Table.th key="buttons"></Table.th>,
|
||||
]}
|
||||
body={tables.map((x: any, i: any) => (
|
||||
<Table.tr key={x.id} hoverable>
|
||||
<Table.td>
|
||||
<p>{x.name}</p>
|
||||
</Table.td>
|
||||
<Table.td>
|
||||
<p>{x.schema}</p>
|
||||
</Table.td>
|
||||
<Table.td className=" hidden max-w-sm truncate lg:table-cell">
|
||||
<p>{x.comment}</p>
|
||||
</Table.td>
|
||||
<Table.td className=" hidden xl:table-cell">
|
||||
<code className="text-sm">{x.live_rows_estimate ?? x.live_row_count}</code>
|
||||
</Table.td>
|
||||
<Table.td className=" hidden xl:table-cell">
|
||||
<code className="text-sm">{x.size}</code>
|
||||
</Table.td>
|
||||
<Table.td>
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button
|
||||
iconRight={<IconColumns />}
|
||||
type="default"
|
||||
className="whitespace-nowrap hover:border-gray-500"
|
||||
style={{ paddingTop: 3, paddingBottom: 3 }}
|
||||
onClick={() => {
|
||||
onOpenTable(x)
|
||||
}}
|
||||
>
|
||||
{x.columns.length} columns
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => onEditTable(x)}
|
||||
icon={<IconEdit3 />}
|
||||
style={{ padding: 5 }}
|
||||
type="text"
|
||||
/>
|
||||
<Button
|
||||
onClick={() => onDeleteTable(x)}
|
||||
icon={<IconTrash />}
|
||||
style={{ padding: 5 }}
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
</Table.td>
|
||||
</Table.tr>
|
||||
const TableList: FC<Props> = ({
|
||||
selectedSchema,
|
||||
onSelectSchema = () => {},
|
||||
onAddTable = () => {},
|
||||
onEditTable = () => {},
|
||||
onDeleteTable = () => {},
|
||||
onOpenTable = () => {},
|
||||
}) => {
|
||||
const { meta } = useStore()
|
||||
|
||||
const [filterString, setFilterString] = useState<string>('')
|
||||
|
||||
const schemas: PostgresSchema[] = meta.schemas.list()
|
||||
const [protectedSchemas, openSchemas] = partition(schemas, (schema) =>
|
||||
meta.excludedSchemas.includes(schema?.name ?? '')
|
||||
)
|
||||
|
||||
const allTables: PostgresTable[] = meta.tables.list(
|
||||
(table: PostgresTable) => table.schema === selectedSchema
|
||||
)
|
||||
const tables =
|
||||
filterString.length === 0
|
||||
? allTables
|
||||
: // @ts-ignore
|
||||
allTables.filter((table: PostgresTable) => table.name.includes(filterString))
|
||||
|
||||
// @ts-ignore
|
||||
const schema = schemas.find((schema) => schema.name === selectedSchema)
|
||||
const isLocked = protectedSchemas.some((s) => s.id === schema?.id)
|
||||
|
||||
return (
|
||||
<>
|
||||
<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={onSelectSchema}
|
||||
icon={isLocked && <IconLock size={16} 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}
|
||||
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>
|
||||
<div>
|
||||
<Input
|
||||
size="small"
|
||||
placeholder="Filter tables"
|
||||
value={filterString}
|
||||
onChange={(e: any) => setFilterString(e.target.value)}
|
||||
icon={<IconSearch size="tiny" />}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{!isLocked && (
|
||||
<div>
|
||||
<Button icon={<IconPlus />} onClick={() => onAddTable()}>
|
||||
New table
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
)
|
||||
</div>
|
||||
{tables.length === 0 ? (
|
||||
<NoSearchResults />
|
||||
) : (
|
||||
<div className="my-4 w-full">
|
||||
<Table
|
||||
head={[
|
||||
<Table.th key="name">Name</Table.th>,
|
||||
<Table.th key="description" className="hidden lg:table-cell">
|
||||
Description
|
||||
</Table.th>,
|
||||
<Table.th key="rows" className="hidden xl:table-cell">
|
||||
Rows (Estimated)
|
||||
</Table.th>,
|
||||
<Table.th key="size" className="hidden xl:table-cell">
|
||||
Size (Estimated)
|
||||
</Table.th>,
|
||||
<Table.th key="buttons"></Table.th>,
|
||||
]}
|
||||
body={tables.map((x: any, i: any) => (
|
||||
<Table.tr key={x.id}>
|
||||
<Table.td>
|
||||
<p title={x.name}>{x.name}</p>
|
||||
</Table.td>
|
||||
<Table.td className="hidden max-w-sm truncate lg:table-cell break-all whitespace-normal">
|
||||
{x.comment !== null ? (
|
||||
<p title={x.comment}>{x.comment}</p>
|
||||
) : (
|
||||
<p className="text-scale-800">No description</p>
|
||||
)}
|
||||
</Table.td>
|
||||
<Table.td className="hidden xl:table-cell">
|
||||
<code className="text-sm">{x.live_rows_estimate ?? x.live_row_count}</code>
|
||||
</Table.td>
|
||||
<Table.td className="hidden xl:table-cell">
|
||||
<code className="text-sm">{x.size}</code>
|
||||
</Table.td>
|
||||
<Table.td>
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button
|
||||
type="default"
|
||||
iconRight={<IconColumns />}
|
||||
className="whitespace-nowrap hover:border-gray-500"
|
||||
style={{ paddingTop: 3, paddingBottom: 3 }}
|
||||
onClick={() => onOpenTable(x)}
|
||||
>
|
||||
{x.columns.length} columns
|
||||
</Button>
|
||||
<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>
|
||||
))}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default TableList
|
||||
export default observer(TableList)
|
||||
@@ -30,7 +30,7 @@ const ExampleProject: FC<Props> = ({ framework, title, description, url }) => {
|
||||
<img
|
||||
className="transition-all group-hover:scale-110"
|
||||
src={`/img/libraries/${framework.toLowerCase()}${
|
||||
framework.toLowerCase() == 'nextjs' ? (isDarkTheme ? '-dark' : '') : ''
|
||||
['expo', 'nextjs'].includes(framework.toLowerCase()) ? (isDarkTheme ? '-dark' : '') : ''
|
||||
}-icon.svg`}
|
||||
alt={`${framework} logo`}
|
||||
width={26}
|
||||
|
||||
@@ -17,7 +17,6 @@ const APIKeys = () => {
|
||||
|
||||
const availableLanguages = [
|
||||
{ name: 'Javascript', key: 'js' },
|
||||
{ name: 'Python', key: 'python' },
|
||||
{ name: 'Dart', key: 'dart' },
|
||||
]
|
||||
const [selectedLanguage, setSelectedLanguage] = useState(availableLanguages[0])
|
||||
|
||||
@@ -2,7 +2,14 @@ import Link from 'next/link'
|
||||
import { FC } from 'react'
|
||||
import SVG from 'react-inlinesvg'
|
||||
import { useRouter } from 'next/router'
|
||||
import { Button, IconKey, IconArchive, IconExternalLink, IconCode } from '@supabase/ui'
|
||||
import {
|
||||
Button,
|
||||
IconKey,
|
||||
IconArchive,
|
||||
IconExternalLink,
|
||||
IconCode,
|
||||
IconActivity,
|
||||
} from '@supabase/ui'
|
||||
|
||||
import Panel from 'components/ui/Panel'
|
||||
import APIKeys from './APIKeys'
|
||||
@@ -18,7 +25,7 @@ const NewProjectPanel: FC<Props> = ({}) => {
|
||||
<div className="grid grid-cols-12 gap-4 lg:gap-20">
|
||||
<div className="col-span-12">
|
||||
<div className="flex flex-col space-y-20">
|
||||
<div className="flex flex-col justify-between h-full">
|
||||
<div className="flex h-full flex-col justify-between">
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-xl text-scale-1200">Welcome to your new project</h3>
|
||||
<p className="text-base text-scale-1100">
|
||||
@@ -29,7 +36,7 @@ const NewProjectPanel: FC<Props> = ({}) => {
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-12 gap-4">
|
||||
<div className="col-span-12 lg:col-span-7 space-y-8 flex flex-col justify-center">
|
||||
<div className="col-span-12 flex flex-col justify-center space-y-8 lg:col-span-7">
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-xl text-scale-1200">
|
||||
Get started by building out your database
|
||||
@@ -40,7 +47,7 @@ const NewProjectPanel: FC<Props> = ({}) => {
|
||||
you need something more.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Link href={`/project/${ref}/editor`}>
|
||||
<a>
|
||||
<Button
|
||||
@@ -91,7 +98,7 @@ const NewProjectPanel: FC<Props> = ({}) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col justify-between h-full space-y-6">
|
||||
<div className="flex h-full flex-col justify-between space-y-6">
|
||||
<div className="max-w-2xl space-y-2">
|
||||
<h3 className="text-xl text-scale-1200">Explore our other products</h3>
|
||||
<p className="text-base text-scale-1100">
|
||||
@@ -99,21 +106,21 @@ const NewProjectPanel: FC<Props> = ({}) => {
|
||||
it completely, or just the features you need.
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 lg:gap-8">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 md:gap-4 md:gap-y-0 xl:grid-cols-4">
|
||||
<Panel>
|
||||
<Panel.Content className="flex flex-col space-y-4 lg:h-40">
|
||||
<Panel.Content className="flex flex-col space-y-4 md:px-3">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="bg-scale-600 text-scale-1000 shadow-sm rounded p-1.5">
|
||||
<div className="rounded bg-scale-600 p-1.5 text-scale-1000 shadow-sm">
|
||||
<IconKey strokeWidth={2} size={16} />
|
||||
</div>
|
||||
<h5>Authentication</h5>
|
||||
</div>
|
||||
<div className="flex flex-grow">
|
||||
<p className="text-scale-1000 text-sm">
|
||||
<div className="flex flex-grow md:min-h-[50px] xl:min-h-[75px]">
|
||||
<p className="text-sm text-scale-1000">
|
||||
A complete user management system that works without any additional tools.
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-x-2 flex items-center">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Link href={`/project/${ref}/auth/users`}>
|
||||
<a>
|
||||
<Button type="default">Explore Auth</Button>
|
||||
@@ -135,19 +142,19 @@ const NewProjectPanel: FC<Props> = ({}) => {
|
||||
</Panel>
|
||||
|
||||
<Panel>
|
||||
<Panel.Content className="flex flex-col space-y-4 lg:h-40">
|
||||
<Panel.Content className="flex flex-col space-y-4 md:px-3">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="bg-scale-600 text-scale-1000 shadow-sm rounded p-1.5">
|
||||
<div className="rounded bg-scale-600 p-1.5 text-scale-1000 shadow-sm">
|
||||
<IconArchive strokeWidth={2} size={16} />
|
||||
</div>
|
||||
<h5>Storage</h5>
|
||||
</div>
|
||||
<div className="flex flex-grow">
|
||||
<p className="text-scale-1000 text-sm">
|
||||
<div className="flex md:min-h-[50px] xl:min-h-[75px]">
|
||||
<p className="text-sm text-scale-1000">
|
||||
Store, organize, and serve any file types of any size from multiple buckets.
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-x-2 flex items-center">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Link href={`/project/${ref}/storage/buckets`}>
|
||||
<a>
|
||||
<Button type="default">Explore Storage</Button>
|
||||
@@ -169,20 +176,20 @@ const NewProjectPanel: FC<Props> = ({}) => {
|
||||
</Panel>
|
||||
|
||||
<Panel>
|
||||
<Panel.Content className="flex flex-col space-y-4 lg:h-40">
|
||||
<Panel.Content className="flex flex-col space-y-4 md:px-3">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="bg-scale-600 text-scale-1000 shadow-sm rounded p-1.5">
|
||||
<div className="rounded bg-scale-600 p-1.5 text-scale-1000 shadow-sm">
|
||||
<IconCode strokeWidth={2} size={16} />
|
||||
</div>
|
||||
<h5>Edge Functions</h5>
|
||||
</div>
|
||||
<div className="flex flex-grow">
|
||||
<p className="text-scale-1000 text-sm">
|
||||
<div className="flex md:min-h-[50px] xl:min-h-[75px]">
|
||||
<p className="text-sm text-scale-1000">
|
||||
Write custom code without deploying or scaling servers, with fast deploy times
|
||||
and low latency.
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-x-2 flex items-center">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Link href={`/project/${ref}/functions`}>
|
||||
<a>
|
||||
<Button type="default">Explore Functions</Button>
|
||||
@@ -202,6 +209,34 @@ const NewProjectPanel: FC<Props> = ({}) => {
|
||||
</div>
|
||||
</Panel.Content>
|
||||
</Panel>
|
||||
<Panel>
|
||||
<Panel.Content className="flex flex-col space-y-4 md:px-3">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="rounded bg-scale-600 p-1.5 text-scale-1000 shadow-sm">
|
||||
<IconActivity strokeWidth={2} size={16} />
|
||||
</div>
|
||||
<h5>Realtime</h5>
|
||||
</div>
|
||||
<div className="flex md:min-h-[50px] xl:min-h-[75px]">
|
||||
<p className="text-sm text-scale-1000">
|
||||
Listen to your to PostgreSQL database in realtime via websockets.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Link href="https://supabase.com/docs/guides/realtime">
|
||||
<a>
|
||||
<Button
|
||||
className="translate-y-[1px]"
|
||||
icon={<IconExternalLink size={14} />}
|
||||
type="default"
|
||||
>
|
||||
About Realtime
|
||||
</Button>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
</Panel.Content>
|
||||
</Panel>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -211,14 +246,14 @@ const NewProjectPanel: FC<Props> = ({}) => {
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-xl text-scale-1200">Connecting to your new project</h3>
|
||||
<p className="text-base lg:max-w-sm text-scale-1100">
|
||||
<p className="text-base text-scale-1100 lg:max-w-sm">
|
||||
Interact with your database through the{' '}
|
||||
<Link href="https://supabase.com/docs/reference">
|
||||
<a className="text-brand-900">Supabase client libraries</a>
|
||||
</Link>{' '}
|
||||
with your API keys.
|
||||
</p>
|
||||
<p className="text-base lg:max-w-sm text-scale-1100">
|
||||
<p className="text-base text-scale-1100 lg:max-w-sm">
|
||||
More information about your project's keys can be found in your project's API
|
||||
settings.
|
||||
</p>
|
||||
|
||||
+17
-15
@@ -122,7 +122,7 @@ const ColumnEditor: FC<Props> = ({
|
||||
if (isEmpty(errors)) {
|
||||
const payload = isNewRecord
|
||||
? generateCreateColumnPayload(selectedTable.id, columnFields)
|
||||
: generateUpdateColumnPayload(column!, columnFields)
|
||||
: generateUpdateColumnPayload(column!, selectedTable, columnFields)
|
||||
const foreignKey = columnFields.foreignKey
|
||||
? { ...columnFields.foreignKey, source_column_name: columnFields.name }
|
||||
: undefined
|
||||
@@ -177,6 +177,16 @@ const ColumnEditor: FC<Props> = ({
|
||||
value={columnFields?.comment ?? ''}
|
||||
onChange={(event: any) => onUpdateField({ comment: event.target.value })}
|
||||
/>
|
||||
<div className="grid grid-cols-12 gap-4">
|
||||
<div className="col-span-12 md:col-span-8 md:col-start-5">
|
||||
<Checkbox
|
||||
label="Is Primary Key"
|
||||
description="A primary key indicates that a column or group of columns can be used as a unique identifier for rows in the table."
|
||||
checked={columnFields?.isPrimaryKey ?? false}
|
||||
onChange={() => onUpdateField({ isPrimaryKey: !columnFields?.isPrimaryKey })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SidePanel.Content>
|
||||
|
||||
@@ -184,18 +194,6 @@ const ColumnEditor: FC<Props> = ({
|
||||
|
||||
<SidePanel.Content>
|
||||
<div className="space-y-10 py-6">
|
||||
{isNewRecord && !hasPrimaryKey && (
|
||||
<div className="grid grid-cols-12 gap-4">
|
||||
<div className="col-span-12 md:col-span-8 md:col-start-5">
|
||||
<Checkbox
|
||||
label={hasPrimaryKey ? 'Add to composite primary key' : 'Is Primary Key'}
|
||||
description="A primary key indicates that a column or group of columns can be used as a unique identifier for rows in the table."
|
||||
checked={columnFields?.isPrimaryKey ?? false}
|
||||
onChange={() => onUpdateField({ isPrimaryKey: !columnFields?.isPrimaryKey })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<ColumnForeignKey
|
||||
column={columnFields}
|
||||
originalForeignKey={originalForeignKey}
|
||||
@@ -216,8 +214,12 @@ const ColumnEditor: FC<Props> = ({
|
||||
/>
|
||||
|
||||
<div className="text-sm grid md:grid-cols-12 md:gap-x-4 ">
|
||||
<p className='text-scale-1100 col-span-7 col-start-5 -mt-4'>
|
||||
<a href="https://supabase.com/docs/guides/database/tables#data-types" target="_blank" className="underline inline-block gap-1 p-2 rounded-sm">
|
||||
<p className="text-scale-1100 col-span-7 col-start-5 -mt-4">
|
||||
<a
|
||||
href="https://supabase.com/docs/guides/database/tables#data-types"
|
||||
target="_blank"
|
||||
className="underline inline-block gap-1 p-2 rounded-sm"
|
||||
>
|
||||
Learn more about data types
|
||||
</a>
|
||||
</p>
|
||||
|
||||
+8
@@ -103,8 +103,13 @@ export const generateCreateColumnPayload = (
|
||||
|
||||
export const generateUpdateColumnPayload = (
|
||||
originalColumn: PostgresColumn,
|
||||
table: PostgresTable,
|
||||
field: ColumnField
|
||||
): Partial<UpdateColumnPayload> => {
|
||||
// @ts-ignore
|
||||
const primaryKeyColumns = table.primary_keys.map((key) => key.name)
|
||||
const isOriginallyPrimaryKey = primaryKeyColumns.includes(originalColumn.name)
|
||||
|
||||
// Only append the properties which are getting updated
|
||||
const defaultValue = field.defaultValue
|
||||
const type = field.isArray ? `${field.format}[]` : field.format
|
||||
@@ -136,6 +141,9 @@ export const generateUpdateColumnPayload = (
|
||||
if (!isEqual(originalColumn.is_unique, field.isUnique)) {
|
||||
payload.isUnique = field.isUnique
|
||||
}
|
||||
if (!isEqual(isOriginallyPrimaryKey, field.isPrimaryKey)) {
|
||||
payload.isPrimaryKey = field.isPrimaryKey
|
||||
}
|
||||
|
||||
return payload
|
||||
}
|
||||
|
||||
+7
-7
@@ -28,7 +28,7 @@ const InputField: FC<Props> = ({
|
||||
<Select
|
||||
size="medium"
|
||||
layout="horizontal"
|
||||
value={field.value}
|
||||
value={field.value ?? ''}
|
||||
label={field.name}
|
||||
labelOptional={field.format}
|
||||
descriptionText={field.comment}
|
||||
@@ -51,7 +51,7 @@ const InputField: FC<Props> = ({
|
||||
<Input
|
||||
layout="horizontal"
|
||||
label={field.name}
|
||||
value={field.value}
|
||||
value={field.value ?? ''}
|
||||
// @ts-ignore This is creating some validateDOMNesting errors
|
||||
// because descriptionText is a <p> element as a parent
|
||||
descriptionText={
|
||||
@@ -73,7 +73,7 @@ const InputField: FC<Props> = ({
|
||||
onChange={(event: any) => onUpdateField({ [field.name]: event.target.value })}
|
||||
actions={
|
||||
<Button
|
||||
disabled={field.value.length === 0}
|
||||
disabled={field.value === null || field.value?.length === 0}
|
||||
type="default"
|
||||
htmlType="button"
|
||||
onClick={onViewForeignKey}
|
||||
@@ -98,7 +98,7 @@ const InputField: FC<Props> = ({
|
||||
disabled={!isEditable}
|
||||
error={errors[field.name]}
|
||||
rows={5}
|
||||
value={field.value}
|
||||
value={field.value ?? ''}
|
||||
placeholder={
|
||||
typeof field.defaultValue === 'string' && field.defaultValue.length === 0
|
||||
? 'Default: Empty string'
|
||||
@@ -114,7 +114,7 @@ const InputField: FC<Props> = ({
|
||||
return (
|
||||
<Input
|
||||
layout="horizontal"
|
||||
value={field.value}
|
||||
value={field.value ?? ''}
|
||||
label={field.name}
|
||||
descriptionText={field.comment}
|
||||
labelOptional={field.format}
|
||||
@@ -141,7 +141,7 @@ const InputField: FC<Props> = ({
|
||||
<DateTimeInput
|
||||
name={field.name}
|
||||
format={field.format}
|
||||
value={field.value}
|
||||
value={field.value ?? ''}
|
||||
description={field.comment}
|
||||
onChange={(value: any) => onUpdateField({ [field.name]: value })}
|
||||
/>
|
||||
@@ -155,7 +155,7 @@ const InputField: FC<Props> = ({
|
||||
descriptionText={field.comment}
|
||||
labelOptional={field.format}
|
||||
error={errors[field.name]}
|
||||
value={field.value}
|
||||
value={field.value ?? ''}
|
||||
placeholder={field.isIdentity ? 'Automatically generated as identity' : field.defaultValue}
|
||||
disabled={!isEditable}
|
||||
onChange={(event: any) => onUpdateField({ [field.name]: event.target.value })}
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@ export interface RowField {
|
||||
comment: string
|
||||
format: string
|
||||
enums: string[]
|
||||
value: string
|
||||
value: string | null
|
||||
defaultValue: string
|
||||
foreignKey?: PostgresRelationship
|
||||
isNullable: boolean
|
||||
|
||||
+4
-4
@@ -60,16 +60,16 @@ export const generateRowFields = (
|
||||
export const validateFields = (fields: RowField[]) => {
|
||||
const errors = {} as any
|
||||
fields.forEach((field) => {
|
||||
if (field.format.startsWith('_') && field.value?.length > 0) {
|
||||
if (field.format.startsWith('_') && (field.value?.length ?? 0) > 0) {
|
||||
try {
|
||||
minifyJSON(field.value)
|
||||
minifyJSON(field.value ?? '')
|
||||
} catch {
|
||||
errors[field.name] = 'Invalid array'
|
||||
}
|
||||
}
|
||||
if (field.format.includes('json') && field.value?.length > 0) {
|
||||
if (field.format.includes('json') && (field.value?.length ?? 0) > 0) {
|
||||
try {
|
||||
minifyJSON(field.value)
|
||||
minifyJSON(field.value ?? '')
|
||||
} catch {
|
||||
errors[field.name] = 'Invalid JSON'
|
||||
}
|
||||
|
||||
@@ -120,7 +120,11 @@ const SidePanelEditor: FC<Props> = ({
|
||||
resolve: any
|
||||
) => {
|
||||
const response = isNewRecord
|
||||
? await meta.createColumn(payload as CreateColumnPayload, foreignKey)
|
||||
? await meta.createColumn(
|
||||
payload as CreateColumnPayload,
|
||||
selectedTable as PostgresTable,
|
||||
foreignKey
|
||||
)
|
||||
: await meta.updateColumn(
|
||||
configuration.columnId as string,
|
||||
payload as UpdateColumnPayload,
|
||||
|
||||
@@ -25,6 +25,7 @@ export interface UpdateColumnPayload {
|
||||
isIdentity?: boolean
|
||||
isNullable?: boolean
|
||||
isUnique?: boolean
|
||||
isPrimaryKey?: boolean
|
||||
identityGeneration?: 'BY DEFAULT' | 'ALWAYS'
|
||||
}
|
||||
|
||||
|
||||
@@ -231,9 +231,9 @@ const Column: FC<Props> = ({
|
||||
</div>
|
||||
{!hasImportContent && (
|
||||
<div className="flex w-[5%] justify-end">
|
||||
<div className="cursor-pointer" onClick={() => onRemoveColumn()}>
|
||||
<button className="cursor-pointer" onClick={() => onRemoveColumn()}>
|
||||
<IconX strokeWidth={1} />
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
+21
-6
@@ -1,6 +1,15 @@
|
||||
import Link from 'next/link'
|
||||
import React, { FC, useState } from 'react'
|
||||
import { partition, isEmpty, isUndefined } from 'lodash'
|
||||
import { Alert, Button, IconEdit, IconHelpCircle, IconKey, IconTrash } from '@supabase/ui'
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
IconEdit,
|
||||
IconExternalLink,
|
||||
IconHelpCircle,
|
||||
IconKey,
|
||||
IconTrash,
|
||||
} from '@supabase/ui'
|
||||
import {
|
||||
PostgresTable,
|
||||
PostgresColumn,
|
||||
@@ -41,7 +50,7 @@ const ColumnManagement: FC<Props> = ({
|
||||
tables = [],
|
||||
columns = [],
|
||||
enumTypes = [],
|
||||
importContent = {},
|
||||
importContent,
|
||||
isNewRecord,
|
||||
onColumnsUpdated = () => {},
|
||||
onSelectImportData = () => {},
|
||||
@@ -316,11 +325,17 @@ const ColumnManagement: FC<Props> = ({
|
||||
Add column
|
||||
</Button>
|
||||
)}
|
||||
<p className='text-sm text-scale-1100'>
|
||||
<a href="https://supabase.com/docs/guides/database/tables#data-types" target="_blank" className="underline flex items-center gap-1">
|
||||
Learn more about data types
|
||||
<Link href="https://supabase.com/docs/guides/database/tables#data-types">
|
||||
<a>
|
||||
<Button
|
||||
type="text"
|
||||
className="text-scale-1000 hover:text-scale-1200"
|
||||
icon={<IconExternalLink size={12} strokeWidth={2} />}
|
||||
>
|
||||
Learn more about data types
|
||||
</Button>
|
||||
</a>
|
||||
</p>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<ForeignKeySelector
|
||||
|
||||
@@ -39,76 +39,75 @@ export const generateDatabaseMenu = (project?: Project): ProductMenuGroup[] => {
|
||||
},
|
||||
...(IS_PLATFORM
|
||||
? [
|
||||
{
|
||||
title: 'Logs and Usage',
|
||||
items: [
|
||||
{
|
||||
name: 'API logs',
|
||||
key: 'api-logs',
|
||||
url: `/project/${ref}/database/api-logs`,
|
||||
items: [],
|
||||
},
|
||||
...(reportsOverview
|
||||
? [
|
||||
{
|
||||
name: 'API usage',
|
||||
key: 'api-usage',
|
||||
url: `/project/${ref}/database/api-usage`,
|
||||
items: [],
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
name: 'Postgres logs',
|
||||
key: 'postgres-logs',
|
||||
url: `/project/${ref}/database/postgres-logs`,
|
||||
items: [],
|
||||
},
|
||||
...(logsRealtime
|
||||
? [
|
||||
{
|
||||
name: 'Realtime logs',
|
||||
key: 'realtime-logs',
|
||||
url: `/project/${ref}/database/realtime-logs`,
|
||||
items: [],
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
},
|
||||
]
|
||||
{
|
||||
title: 'Logs and Usage',
|
||||
items: [
|
||||
{
|
||||
name: 'API logs',
|
||||
key: 'api-logs',
|
||||
url: `/project/${ref}/database/api-logs`,
|
||||
items: [],
|
||||
},
|
||||
...(reportsOverview
|
||||
? [
|
||||
{
|
||||
name: 'API usage',
|
||||
key: 'api-usage',
|
||||
url: `/project/${ref}/database/api-usage`,
|
||||
items: [],
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
name: 'Postgres logs',
|
||||
key: 'postgres-logs',
|
||||
url: `/project/${ref}/database/postgres-logs`,
|
||||
items: [],
|
||||
},
|
||||
...(logsRealtime
|
||||
? [
|
||||
{
|
||||
name: 'Realtime logs',
|
||||
key: 'realtime-logs',
|
||||
url: `/project/${ref}/database/realtime-logs`,
|
||||
items: [],
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(IS_PLATFORM
|
||||
? [
|
||||
{
|
||||
title: 'Alpha Preview',
|
||||
isPreview: true,
|
||||
items: [
|
||||
{
|
||||
name: 'Triggers',
|
||||
key: 'triggers',
|
||||
url: `/project/${ref}/database/triggers`,
|
||||
items: [],
|
||||
},
|
||||
{
|
||||
name: 'Functions',
|
||||
key: 'functions',
|
||||
url: `/project/${ref}/database/functions`,
|
||||
items: [],
|
||||
},
|
||||
...(showHooksRoute
|
||||
? [
|
||||
{
|
||||
name: 'Database Webhooks',
|
||||
key: 'hooks',
|
||||
url: `/project/${ref}/database/hooks`,
|
||||
items: [],
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
},
|
||||
]
|
||||
{
|
||||
title: 'Alpha Preview',
|
||||
items: [
|
||||
{
|
||||
name: 'Database Triggers',
|
||||
key: 'triggers',
|
||||
url: `/project/${ref}/database/triggers`,
|
||||
items: [],
|
||||
},
|
||||
{
|
||||
name: 'Database Functions',
|
||||
key: 'functions',
|
||||
url: `/project/${ref}/database/functions`,
|
||||
items: [],
|
||||
},
|
||||
...(showHooksRoute
|
||||
? [
|
||||
{
|
||||
name: 'Database Webhooks',
|
||||
key: 'hooks',
|
||||
url: `/project/${ref}/database/hooks`,
|
||||
items: [],
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
},
|
||||
]
|
||||
: []),
|
||||
]
|
||||
}
|
||||
@@ -104,8 +104,8 @@ const TableEditorMenu: FC<Props> = ({
|
||||
<div className="my-6 mx-4 flex flex-grow flex-col space-y-6">
|
||||
{/* Schema selection dropdown */}
|
||||
<div className="px-3">
|
||||
{meta.schemas.isLoading ? (
|
||||
<div className="flex h-[30px] items-center space-x-3 rounded border border-gray-500 px-3">
|
||||
{!meta.schemas.isInitialized ? (
|
||||
<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-scale-900 text-xs">Loading schemas...</span>
|
||||
</div>
|
||||
@@ -121,7 +121,7 @@ const TableEditorMenu: FC<Props> = ({
|
||||
}}
|
||||
>
|
||||
<Listbox.Option disabled key="normal-schemas" value="normal-schemas" label="Schemas">
|
||||
<p className="text-sm">Schemas</p>
|
||||
<p className="text-xs text-scale-1100">Schemas</p>
|
||||
</Listbox.Option>
|
||||
{/* @ts-ignore */}
|
||||
{openSchemas.map((schema) => (
|
||||
@@ -129,9 +129,9 @@ const TableEditorMenu: FC<Props> = ({
|
||||
key={schema.id}
|
||||
value={schema.name}
|
||||
label={schema.name}
|
||||
addOnBefore={() => <span className="text-scale-900">schema</span>}
|
||||
addOnBefore={() => <span className="text-scale-900 text-xs">schema</span>}
|
||||
>
|
||||
<span className="text-scale-1200 text-sm">{schema.name}</span>
|
||||
<span className="text-scale-1200 text-xs">{schema.name}</span>
|
||||
</Listbox.Option>
|
||||
))}
|
||||
<Listbox.Option
|
||||
@@ -140,16 +140,16 @@ const TableEditorMenu: FC<Props> = ({
|
||||
value="protected-schemas"
|
||||
label="Protected schemas"
|
||||
>
|
||||
<p className="text-sm">Protected schemas</p>
|
||||
<p className="text-xs text-scale-1100">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>}
|
||||
addOnBefore={() => <span className="text-scale-900 text-xs">schema</span>}
|
||||
>
|
||||
<span className="text-scale-1200 text-sm">{schema.name}</span>
|
||||
<span className="text-scale-1200 text-xs">{schema.name}</span>
|
||||
</Listbox.Option>
|
||||
))}
|
||||
</Listbox>
|
||||
|
||||
@@ -290,7 +290,7 @@ const UtilityTabResults = observer(() => {
|
||||
<div className="bg-table-header-light dark:bg-table-header-dark">
|
||||
<p className="m-0 border-0 px-6 py-4 text-sm text-scale-1000">
|
||||
Click <code>RUN</code> or hit{' '}
|
||||
<code>{window.navigator.platform.match(/^Mac/) ? 'cmd' : 'ctrl'} + ⏎</code> to execute
|
||||
<code>{window.navigator.platform.match(/^Mac/) ? '⌘' : 'Ctrl'} + Enter</code> to execute
|
||||
your query.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -16,11 +16,13 @@ export function usePermissions(profile?: any, returning?: 'minimal') {
|
||||
data: data,
|
||||
error,
|
||||
mutate,
|
||||
} = useSWR<any>(profile !== undefined ? url : null, get, { loadingTimeout: 10000 })
|
||||
} = useSWR<any>(profile !== undefined && IS_PLATFORM ? url : null, get, {
|
||||
loadingTimeout: 10000,
|
||||
})
|
||||
const anyError = data?.error || error
|
||||
|
||||
return {
|
||||
permissions: anyError ? undefined : data,
|
||||
permissions: IS_PLATFORM ? (anyError ? undefined : data) : [],
|
||||
isLoading: !anyError && !data,
|
||||
isError: !!anyError,
|
||||
mutate,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import apiWrapper from 'lib/api/apiWrapper'
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
|
||||
import apiWrapper from 'lib/api/apiWrapper'
|
||||
|
||||
export default (req: NextApiRequest, res: NextApiResponse) => apiWrapper(req, res, handler)
|
||||
|
||||
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
@@ -20,7 +21,7 @@ const handleGetAll = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const response = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Default Organization',
|
||||
name: process.env.DEFAULT_ORGANIZATION_NAME || 'Default Organization',
|
||||
slug: 'default-org-slug',
|
||||
billing_email: 'billing@supabase.co',
|
||||
},
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
|
||||
import { IS_PLATFORM } from 'lib/constants'
|
||||
import { createEncryptedDbConnectionString } from 'lib/api/apiHelpers'
|
||||
import apiWrapper from 'lib/api/apiWrapper'
|
||||
import { createEncryptedDbConnectionString } from 'lib/api/apiHelpers'
|
||||
|
||||
export default (req: NextApiRequest, res: NextApiResponse) => apiWrapper(req, res, handler)
|
||||
|
||||
@@ -28,14 +29,14 @@ const handleGetAll = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
organizations: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Default Organization',
|
||||
name: process.env.DEFAULT_ORGANIZATION_NAME || 'Default Organization',
|
||||
slug: 'default-org-slug',
|
||||
billing_email: 'billing@supabase.co',
|
||||
projects: [
|
||||
{
|
||||
id: 1,
|
||||
ref: 'default',
|
||||
name: 'Default Project',
|
||||
name: process.env.DEFAULT_PROJECT_NAME || 'Default Project',
|
||||
organization_id: 1,
|
||||
cloud_provider: 'localhost',
|
||||
status: 'ACTIVE_HEALTHY',
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import apiWrapper from 'lib/api/apiWrapper'
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
|
||||
import apiWrapper from 'lib/api/apiWrapper'
|
||||
|
||||
export default (req: NextApiRequest, res: NextApiResponse) => apiWrapper(req, res, handler)
|
||||
|
||||
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
@@ -20,7 +21,7 @@ const handleGet = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const response = {
|
||||
id: 1,
|
||||
ref: 'default',
|
||||
name: 'Default Project',
|
||||
name: process.env.DEFAULT_PROJECT_NAME || 'Default Project',
|
||||
organization_id: 1,
|
||||
cloud_provider: 'localhost',
|
||||
status: 'ACTIVE_HEALTHY',
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import apiWrapper from 'lib/api/apiWrapper'
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
|
||||
import apiWrapper from 'lib/api/apiWrapper'
|
||||
|
||||
export default (req: NextApiRequest, res: NextApiResponse) => apiWrapper(req, res, handler)
|
||||
|
||||
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
@@ -21,7 +22,7 @@ const handleGetAll = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
{
|
||||
id: 1,
|
||||
ref: 'default',
|
||||
name: 'Default Project',
|
||||
name: process.env.DEFAULT_PROJECT_NAME || 'Default Project',
|
||||
organization_id: 1,
|
||||
cloud_provider: 'localhost',
|
||||
status: 'ACTIVE_HEALTHY',
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
|
||||
import apiWrapper from 'lib/api/apiWrapper'
|
||||
|
||||
export default (req: NextApiRequest, res: NextApiResponse) => apiWrapper(req, res, handler)
|
||||
@@ -27,7 +28,7 @@ const handleGetAll = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
db_user: 'postgres',
|
||||
id: 1,
|
||||
inserted_at: undefined,
|
||||
name: 'Default Project',
|
||||
name: process.env.DEFAULT_PROJECT_NAME || 'Default Project',
|
||||
ref: 'default',
|
||||
status: 'ACTIVE_HEALTHY',
|
||||
services: [
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
|
||||
import apiWrapper from 'lib/api/apiWrapper'
|
||||
|
||||
export default (req: NextApiRequest, res: NextApiResponse) => apiWrapper(req, res, handler)
|
||||
@@ -21,7 +22,7 @@ const handleGetAll = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
project: {
|
||||
id: 1,
|
||||
ref: 'default',
|
||||
name: 'Default Project',
|
||||
name: process.env.DEFAULT_PROJECT_NAME || 'Default Project',
|
||||
status: 'ACTIVE_HEALTHY',
|
||||
organization_id: 1,
|
||||
inserted_at: '2021-08-02T06:40:40.646362',
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
|
||||
import apiWrapper from 'lib/api/apiWrapper'
|
||||
|
||||
export default (req: NextApiRequest, res: NextApiResponse) => apiWrapper(req, res, handler)
|
||||
@@ -29,7 +30,7 @@ const handleGetAll = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
id: 1,
|
||||
inserted_at: undefined,
|
||||
jwt_secret: '-',
|
||||
name: 'Default Project',
|
||||
name: process.env.DEFAULT_PROJECT_NAME || 'Default Project',
|
||||
ref: 'default',
|
||||
region: 'ap-southeast-1',
|
||||
status: 'ACTIVE_HEALTHY',
|
||||
|
||||
@@ -16,8 +16,10 @@ const DatabaseTables: NextPageWithLayout = () => {
|
||||
const { meta, ui } = useStore()
|
||||
|
||||
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>()
|
||||
|
||||
@@ -121,6 +123,8 @@ const DatabaseTables: NextPageWithLayout = () => {
|
||||
<div className="p-4">
|
||||
{isUndefined(selectedTable) ? (
|
||||
<TableList
|
||||
selectedSchema={selectedSchema}
|
||||
onSelectSchema={setSelectedSchema}
|
||||
onAddTable={onAddTable}
|
||||
onEditTable={onEditTable}
|
||||
onDeleteTable={onDeleteTable}
|
||||
|
||||
@@ -8,7 +8,7 @@ import { PostgresTable, PostgresColumn } from '@supabase/postgres-meta'
|
||||
|
||||
import Base64 from 'lib/base64'
|
||||
import { tryParseJson } from 'lib/helpers'
|
||||
import { useStore, withAuth } from 'hooks'
|
||||
import { useStore, withAuth, useUrlState } from 'hooks'
|
||||
import { Dictionary } from 'components/grid'
|
||||
import { TableEditorLayout } from 'components/layouts'
|
||||
import { TableGridEditor } from 'components/interfaces'
|
||||
@@ -17,6 +17,7 @@ import ConfirmationModal from 'components/ui/ConfirmationModal'
|
||||
const TableEditorPage: NextPage = () => {
|
||||
const router = useRouter()
|
||||
const { id }: any = router.query
|
||||
const [_, setParams] = useUrlState({ arrayKeys: ['filter', 'sort'] })
|
||||
|
||||
const { meta, ui } = useStore()
|
||||
const [selectedSchema, setSelectedSchema] = useState<string>()
|
||||
@@ -98,11 +99,32 @@ const TableEditorPage: NextPage = () => {
|
||||
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 {
|
||||
const response: any = await meta.columns.del(selectedColumnToDelete!.id)
|
||||
if (response.error) throw response.error
|
||||
|
||||
removeDeletedColumnFromFiltersAndSorts(selectedColumnToDelete!.name)
|
||||
|
||||
await meta.tables.loadById(selectedColumnToDelete!.table_id)
|
||||
} catch (error: any) {
|
||||
ui.setNotification({
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Project } from 'types'
|
||||
import { IRootStore } from '../RootStore'
|
||||
import { constructHeaders } from 'lib/api/apiHelpers'
|
||||
import { get } from 'lib/common/fetch'
|
||||
import { PROJECT_STATUS } from 'lib/constants'
|
||||
import { IS_PLATFORM, PROJECT_STATUS } from 'lib/constants'
|
||||
import PostgresMetaInterface, { IPostgresMetaInterface } from '../common/PostgresMetaInterface'
|
||||
|
||||
import pingPostgrest from 'lib/pingPostgrest'
|
||||
@@ -57,7 +57,9 @@ export default class ProjectStore extends PostgresMetaInterface<Project> {
|
||||
this.data[project.id] = project
|
||||
|
||||
// lazy fetches
|
||||
this.fetchSubscriptionTier(project)
|
||||
if (IS_PLATFORM) {
|
||||
this.fetchSubscriptionTier(project)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -84,12 +84,17 @@ export interface IMetaStore {
|
||||
|
||||
/** The methods below involve several contexts due to the UI flow of the
|
||||
* dashboard and hence do not sit within their own stores */
|
||||
createColumn: (payload: CreateColumnPayload, foreignKey?: Partial<PostgresRelationship>) => any
|
||||
createColumn: (
|
||||
payload: CreateColumnPayload,
|
||||
selectedTable: PostgresTable,
|
||||
foreignKey?: Partial<PostgresRelationship>
|
||||
) => any
|
||||
updateColumn: (
|
||||
id: string,
|
||||
payload: UpdateColumnPayload,
|
||||
selectedTable: PostgresTable,
|
||||
foreignKey?: Partial<PostgresRelationship>
|
||||
foreignKey?: Partial<PostgresRelationship>,
|
||||
skipPKCreation?: boolean
|
||||
) => any
|
||||
duplicateTable: (
|
||||
payload: any,
|
||||
@@ -314,11 +319,34 @@ export default class MetaStore implements IMetaStore {
|
||||
}
|
||||
}
|
||||
|
||||
async createColumn(payload: CreateColumnPayload, foreignKey?: Partial<PostgresRelationship>) {
|
||||
async createColumn(
|
||||
payload: CreateColumnPayload,
|
||||
selectedTable: PostgresTable,
|
||||
foreignKey?: Partial<PostgresRelationship>
|
||||
) {
|
||||
try {
|
||||
const column: any = await this.columns.create(payload)
|
||||
// Once pg-meta supports composite keys, we can remove this logic
|
||||
const { isPrimaryKey, ...formattedPayload } = payload
|
||||
|
||||
const column: any = await this.columns.create(formattedPayload)
|
||||
if (column.error) throw column.error
|
||||
|
||||
// Firing createColumn in createTable will bypass this block
|
||||
if (isPrimaryKey) {
|
||||
// Same logic in createTable: Remove any primary key constraints first (we'll add it back later)
|
||||
// @ts-ignore
|
||||
const existingPrimaryKeys = selectedTable.primary_keys.map((x) => x.name)
|
||||
|
||||
if (existingPrimaryKeys.length > 0) {
|
||||
const removePK = await this.removePrimaryKey(column.schema, column.table)
|
||||
if (removePK.error) throw removePK.error
|
||||
}
|
||||
|
||||
const primaryKeyColumns = existingPrimaryKeys.concat([column.name])
|
||||
const addPK = await this.addPrimaryKey(column.schema, column.table, primaryKeyColumns)
|
||||
if (addPK.error) throw addPK.error
|
||||
}
|
||||
|
||||
if (!isUndefined(foreignKey)) {
|
||||
const relation: any = await this.addForeignKey(foreignKey)
|
||||
if (relation.error) throw relation.error
|
||||
@@ -332,10 +360,12 @@ export default class MetaStore implements IMetaStore {
|
||||
id: string,
|
||||
payload: UpdateColumnPayload,
|
||||
selectedTable: PostgresTable,
|
||||
foreignKey?: Partial<PostgresRelationship>
|
||||
foreignKey?: Partial<PostgresRelationship>,
|
||||
skipPKCreation?: boolean
|
||||
) {
|
||||
try {
|
||||
const column: any = await this.columns.update(id, payload)
|
||||
const { isPrimaryKey, ...formattedPayload } = payload
|
||||
const column: any = await this.columns.update(id, formattedPayload)
|
||||
if (column.error) throw column.error
|
||||
|
||||
const originalColumn = find(selectedTable.columns, { id })
|
||||
@@ -343,6 +373,21 @@ export default class MetaStore implements IMetaStore {
|
||||
source_column_name: originalColumn!.name,
|
||||
})
|
||||
|
||||
if (!skipPKCreation && isPrimaryKey !== undefined) {
|
||||
const existingPrimaryKeys = selectedTable.primary_keys.map((x) => x.name)
|
||||
|
||||
// Primary key is getting updated for the column
|
||||
if (existingPrimaryKeys.length > 0) {
|
||||
const removePK = await this.removePrimaryKey(column.schema, column.table)
|
||||
if (removePK.error) throw removePK.error
|
||||
}
|
||||
const primaryKeyColumns = isPrimaryKey
|
||||
? existingPrimaryKeys.concat([column.name])
|
||||
: existingPrimaryKeys.filter((x) => x !== column.name)
|
||||
const addPK = await this.addPrimaryKey(column.schema, column.table, primaryKeyColumns)
|
||||
if (addPK.error) throw addPK.error
|
||||
}
|
||||
|
||||
// For updating of foreign key relationship, we remove the original one by default
|
||||
// Then just add whatever was in foreignKey - simplicity over trying to derive whether to update or not
|
||||
if (!isUndefined(existingForeignKey)) {
|
||||
@@ -623,11 +668,11 @@ export default class MetaStore implements IMetaStore {
|
||||
...column,
|
||||
isPrimaryKey: false,
|
||||
})
|
||||
await this.createColumn(columnPayload, column.foreignKey)
|
||||
await this.createColumn(columnPayload, updatedTable, column.foreignKey)
|
||||
} else {
|
||||
const originalColumn = find(originalColumns, { id: column.id })
|
||||
if (originalColumn) {
|
||||
const columnPayload = generateUpdateColumnPayload(originalColumn, column)
|
||||
const columnPayload = generateUpdateColumnPayload(originalColumn, updatedTable, column)
|
||||
const originalForeignKey = find(table.relationships, {
|
||||
source_schema: originalColumn.schema,
|
||||
source_table_name: originalColumn.table,
|
||||
@@ -640,11 +685,13 @@ export default class MetaStore implements IMetaStore {
|
||||
category: 'loading',
|
||||
message: `Updating column ${column.name} from ${updatedTable.name}`,
|
||||
})
|
||||
const skipPKCreation = true
|
||||
const res: any = await this.updateColumn(
|
||||
column.id,
|
||||
columnPayload,
|
||||
updatedTable,
|
||||
column.foreignKey
|
||||
column.foreignKey,
|
||||
skipPKCreation
|
||||
)
|
||||
if (res?.error) {
|
||||
hasError = true
|
||||
|
||||
Reference in new issue
Block a user